diff --git a/doc/daikon.texinfo b/doc/daikon.texinfo index b18c50d3d..dddc8d1a2 100644 --- a/doc/daikon.texinfo +++ b/doc/daikon.texinfo @@ -2789,6 +2789,10 @@ The @var{daikon_options} are the same options that can be provided to the The @var{options} for @command{TraceSelect} are: @table @option +@item -SEED @var{n} +Use the integer @var{n} as the seed for random selection, so that repeated +runs select the same samples. + @item -NOCLEAN Don't delete the temporary trace samples created by the random selection process. This can help for debugging or for using the tool @@ -8649,7 +8653,7 @@ of, or in addition to, referencing this manual and the Daikon website @c LocalWords: fd Linux's GNUC mkdir tmp gunzip xf li dkconfig CreateSpinfo @c LocalWords: ParameterDoclet CreateSpinfoC un biconditional addAll isFull @c LocalWords: smallexample simpleStack currentSize jtb ParseException kmeans -@c LocalWords: runcluster xm ALG xmeans TraceSelect NOCLEAN DIFFS MergeESC pp +@c LocalWords: runcluster xm ALG xmeans TraceSelect NOCLEAN DIFFS SEED MergeESC pp @c LocalWords: escannotated jmlannotated dbcannotated Annnotate xml INVINFO @c LocalWords: ubs Throwable throwable rethrown WriteViolationFile MyProg @c LocalWords: callMethod isInstrumented getInvariants INVS ijmp exin wp diff --git a/java/daikon/Daikon.java b/java/daikon/Daikon.java index a7c4dcce5..a912dea39 100644 --- a/java/daikon/Daikon.java +++ b/java/daikon/Daikon.java @@ -518,7 +518,7 @@ private Daikon() { public static final Logger debugStats = Logger.getLogger("daikon.stats"); /** The usage message for this program. */ - static String usage = + public static final String usage = StringsPlume.joinLines( release_string, // "Uses the Java port of GNU getopt, copyright (c) 1998 Aaron M. Renn", @@ -1005,8 +1005,18 @@ public FileOptions(Set decls, Set dtrace, Set spinfo, SetThese tests do not call {@code Daikon.mainHelper}, because it resets Daikon's global state, - * which other tests in the same JVM depend on. + *

These tests do not call {@code Daikon.mainHelper}, which runs Daikon. However, {@link + * #testHelpOption} passes Daikon arguments to TraceSelect, which checks them by calling {@code + * Daikon.cleanup} and {@code Daikon.read_options}. Those calls reset Daikon's global state. */ public class BadOptionTest { @@ -35,6 +38,17 @@ public class BadOptionTest { new LongOpt("verbose", LongOpt.NO_ARGUMENT, null, 'v'), }; + /** + * Resets the Daikon option that {@link #testTraceSelectBadArguments} sets. TraceSelect checks its + * Daikon arguments by calling {@code Daikon.read_options}, so its {@code --ppt-select-pattern} + * argument sets {@link Daikon#ppt_regexp}, which would make later tests in the same JVM omit + * their program points. + */ + @After + public void resetDaikonOptions() { + Daikon.ppt_regexp = null; + } + /** The suffix of every description of a bad command-line option. */ private static final String USAGE_HINT = "; run with -h for usage"; @@ -149,11 +163,81 @@ public void testPrintInvariantsUnrecognizedOption() { () -> PrintInvariants.mainHelper(new String[] {"--bogus", "foo.inv.gz"})); } + /** Tests that TraceSelect rejects bad arguments before doing any sampling. */ + @Test + public void testTraceSelectBadArguments() { + assertBadOption( + "num_reps must be a positive integer, not 0", + () -> TraceSelect.mainHelper(new String[] {"0", "10", "x.dtrace"})); + assertBadOption( + "num_reps must be a positive integer, not -5", + () -> TraceSelect.mainHelper(new String[] {"-5", "10", "x.dtrace"})); + assertBadOption( + "sample_size must be a positive integer, not -5", + () -> TraceSelect.mainHelper(new String[] {"20", "-5", "x.dtrace"})); + assertBadOption( + "sample_size must be a positive integer, not ten", + () -> TraceSelect.mainHelper(new String[] {"20", "ten", "x.dtrace"})); + Daikon.UserError e = + assertThrows( + Daikon.UserError.class, + () -> TraceSelect.mainHelper(new String[] {"20", "10", "-NOCLAEN", "x.dtrace"})); + assertEquals( + "Unrecognized TraceSelect option -NOCLAEN; the options are -SEED, -NOCLEAN," + + " -INCLUDE_UNRETURNED, and -DO_DIFFS", + String.valueOf(e.getMessage())); + // TraceSelect options must precede the Daikon arguments. + e = + assertThrows( + Daikon.UserError.class, + () -> TraceSelect.mainHelper(new String[] {"20", "10", "x.dtrace", "-NOCLEAN"})); + assertEquals( + "TraceSelect option -NOCLEAN must precede the Daikon arguments, such as the .dtrace file", + String.valueOf(e.getMessage())); + // Daikon would read a compressed trace in full, in every sample run. + e = + assertThrows( + Daikon.UserError.class, + () -> TraceSelect.mainHelper(new String[] {"20", "10", "x.dtrace", "y.dtrace.gz"})); + assertEquals( + "TraceSelect cannot sample y.dtrace.gz; the trace file must be uncompressed and its name" + + " must end with \".dtrace\"", + String.valueOf(e.getMessage())); + // TraceSelect passes -o to Daikon itself. + e = + assertThrows( + Daikon.UserError.class, + () -> TraceSelect.mainHelper(new String[] {"1", "10", "-o", "r.inv", "x.dtrace"})); + assertEquals( + "TraceSelect does not accept Daikon's -o option: it writes the invariants of each sample" + + " to the sample's file name plus \".inv\"", + String.valueOf(e.getMessage())); + // An option's value is not the trace file, even if it ends with ".dtrace". + e = + assertThrows( + Daikon.UserError.class, + () -> + TraceSelect.mainHelper( + new String[] {"20", "10", "--ppt-select-pattern", "x.dtrace"})); + assertTrue(String.valueOf(e.getMessage()).startsWith("No .dtrace file name specified")); + e = + assertThrows( + Daikon.UserError.class, + () -> + TraceSelect.mainHelper(new String[] {"20", "10", "--ppt-select-pattern=x.dtrace"})); + assertTrue(String.valueOf(e.getMessage()).startsWith("No .dtrace file name specified")); + } + /** Tests that tools accept the {@code --help} option that their usage messages document. */ @Test public void testHelpOption() { assertPrintsUsage(() -> DtraceDiff.mainHelper(new String[] {"--help"})); assertPrintsUsage(() -> UnionInvariants.mainHelper(new String[] {"--help"})); + assertPrintsUsage(() -> TraceSelect.mainHelper(new String[] {"--help"})); + assertPrintsUsage(() -> TraceSelect.mainHelper(new String[] {"20", "10", "-h", "x.dtrace"})); + assertPrintsUsage(() -> TraceSelect.mainHelper(new String[] {"20", "10", "--help"})); + assertPrintsUsage( + () -> TraceSelect.mainHelper(new String[] {"20", "10", "-NOCLEAN", "x.dtrace", "--he"})); assertPrintsUsage(() -> SplitDtrace.mainHelper(new String[] {"-h", "x"})); } diff --git a/java/daikon/test/TraceSelectTest.java b/java/daikon/test/TraceSelectTest.java new file mode 100644 index 000000000..fde50b8cb --- /dev/null +++ b/java/daikon/test/TraceSelectTest.java @@ -0,0 +1,61 @@ +package daikon.test; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertEquals; + +import daikon.tools.TraceSelect; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Random; +import org.junit.Test; + +/** Tests the TraceSelect tool. */ +public class TraceSelectTest { + + /** The number of invocations in the trace file created by {@link #writeTrace}. */ + private static final int NUM_INVOCATIONS = 20; + + /** + * Writes a trace file with {@link #NUM_INVOCATIONS} invocations of one method. + * + * @return the trace file + * @throws IOException if the file cannot be written + */ + private static Path writeTrace() throws IOException { + StringBuilder sb = new StringBuilder(); + for (int nonce = 1; nonce <= NUM_INVOCATIONS; nonce++) { + for (String kind : new String[] {"ENTER", "EXIT1"}) { + sb.append("foo.Bar.baz():::") + .append(kind) + .append("\nthis_invocation_nonce\n") + .append(nonce) + .append("\nx\n") + .append(nonce) + .append("\n1\n\n"); + } + } + Path trace = Files.createTempFile("TraceSelectTest", ".dtrace"); + Files.writeString(trace, sb, UTF_8); + return trace; + } + + /** Tests that sampling twice with the same seed selects the same invocations. */ + @Test + public void testSameSeedSameSample() throws IOException { + Path trace = writeTrace(); + try { + int sampleSize = 5; + List sample1 = + TraceSelect.selectSample(trace.toString(), sampleSize, new Random(1000), false); + List sample2 = + TraceSelect.selectSample(trace.toString(), sampleSize, new Random(1000), false); + // The sample is a proper subset of the invocations, so the test is not vacuous. + assertEquals(sampleSize, sample1.size()); + assertEquals(sample1, sample2); + } finally { + Files.delete(trace); + } + } +} diff --git a/java/daikon/tools/DtracePartitioner.java b/java/daikon/tools/DtracePartitioner.java index ae82a139b..5fe23e8ff 100644 --- a/java/daikon/tools/DtracePartitioner.java +++ b/java/daikon/tools/DtracePartitioner.java @@ -36,17 +36,11 @@ public class DtracePartitioner implements Closeable, Partitioner /** * @param filename the Daikon trace file to be partitioned + * @throws IOException if the file cannot be opened */ - public DtracePartitioner(String filename) { - try { - this.filename = filename; - // System.out.printf("trying with file %s%n", filename); - br = FilesPlume.newBufferedFileReader(filename); - - } catch (IOException e) { - e.printStackTrace(); - throw new Error(e); - } + public DtracePartitioner(String filename) throws IOException { + this.filename = filename; + br = FilesPlume.newBufferedFileReader(filename); } /** Releases resources held by this. */ @@ -61,8 +55,7 @@ public boolean hasNext(@GuardSatisfied DtracePartitioner this) { try { return br.ready(); } catch (IOException e) { - e.printStackTrace(); - return false; + throw new UncheckedIOException(e); } } diff --git a/java/daikon/tools/TraceSelect.java b/java/daikon/tools/TraceSelect.java index 762e4716c..93e9be649 100644 --- a/java/daikon/tools/TraceSelect.java +++ b/java/daikon/tools/TraceSelect.java @@ -1,9 +1,12 @@ // TraceSelect.java package daikon.tools; -import java.io.File; +import static java.nio.charset.StandardCharsets.UTF_8; + +import daikon.DaikonGetopt; import java.io.IOException; import java.io.PrintWriter; +import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; @@ -13,11 +16,10 @@ import java.util.List; import java.util.Locale; import java.util.Random; +import java.util.Set; import java.util.StringTokenizer; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -import org.checkerframework.checker.nullness.qual.RequiresNonNull; +import java.util.regex.Pattern; import org.checkerframework.dataflow.qual.Pure; -import org.plumelib.util.FilesPlume; import org.plumelib.util.MultiRandSelector; import org.plumelib.util.StringsPlume; @@ -32,39 +34,53 @@ private TraceSelect() { throw new UnsupportedOperationException("Do not instantiate"); } - public static boolean CLEAN = true; - public static boolean INCLUDE_UNRETURNED = false; - public static boolean DO_DIFFS = false; - - private static int num_reps; - - private static @MonotonicNonNull String fileName = null; - - // Just a quick command line cache - // ... but I think it would it be better to pass args to invokeDaikon - // rather than introducing this variable. - private static String @MonotonicNonNull [] argles; - // // stores the invocations in Strings - // private static ArrayList invokeBuffer; - - private static int numPerSample; - - // always set to non-null by mainHelper - private static @MonotonicNonNull Random randObj; + /** + * Matches an argument that has the form of a TraceSelect option: a hyphen followed by capital + * letters and underscores. No Daikon argument has this form. + */ + private static final Pattern TRACESELECT_OPTION = Pattern.compile("-[A-Z_]{2,}"); - private static int daikonArgStart = 0; + /** Matches a negative integer, which is not an option. */ + private static final Pattern NEGATIVE_INTEGER = Pattern.compile("-[0-9]+"); - // This allows us to simply call MultiDiff - // with the same files we just created. - // Always set to non-null by mainHelper - private static String @MonotonicNonNull [] sampleNames; + /** + * Daikon command-line options whose value can be the next command-line argument. This must be + * kept in sync with the options that {@link daikon.Daikon} declares with {@code + * LongOpt.REQUIRED_ARGUMENT}. It omits "-o", which TraceSelect rejects. + */ + private static final Set DAIKON_OPTIONS_WITH_ARGUMENT = + Set.of( + "--" + daikon.Daikon.format_SWITCH, + "--" + daikon.Daikon.files_from_SWITCH, + "--" + daikon.Daikon.omit_from_output_SWITCH, + "--" + daikon.Daikon.conf_limit_SWITCH, + "--" + daikon.Daikon.list_type_SWITCH, + "--" + daikon.Daikon.user_defined_invariant_SWITCH, + "--" + daikon.Daikon.ppt_regexp_SWITCH, + "--" + daikon.Daikon.ppt_omit_regexp_SWITCH, + "--" + daikon.Daikon.var_regexp_SWITCH, + "--" + daikon.Daikon.var_omit_regexp_SWITCH, + "--" + daikon.Daikon.server_SWITCH, + "--" + daikon.Daikon.config_SWITCH, + "--" + daikon.Daikon.config_option_SWITCH, + "--" + daikon.Daikon.debug_SWITCH, + "--" + daikon.Daikon.track_SWITCH, + "--" + daikon.Daikon.disc_reason_SWITCH); /** The usage message for this program. */ private static final String usage = StringsPlume.joinLines( "USAGE: TraceSelect num_reps sample_size [options] [Daikon-args]...", - "Example: java TraceSelect 20 10 -NOCLEAN -INCLUDE_UNRETURNED-SEED 1000 foo.dtrace" - + " foo2.dtrace foo.decls RatPoly.decls foo3.dtrace"); + "num_reps and sample_size must be positive integers.", + "The options are -SEED n, -NOCLEAN, -INCLUDE_UNRETURNED, and -DO_DIFFS.", + "The Daikon-args start with the first argument that is not one of those options.", + "Exactly one of the Daikon-args must be a .dtrace file; it is the file to sample.", + "The Daikon-args must not include -o, which TraceSelect supplies for each sample.", + "Example: java TraceSelect 20 10 -NOCLEAN -INCLUDE_UNRETURNED -SEED 1000 foo.dtrace" + + " foo.decls RatPoly.decls", + "", + "The Daikon-args are:", + daikon.Daikon.usage); /** * The entry point of TraceSelect. @@ -85,172 +101,242 @@ public static void main(String[] args) { * * @param args command-line arguments, like those of {@link #main} */ - public static void mainHelper(final String[] args) { - argles = args; - if (args.length == 0) { - throw new daikon.Daikon.UserError("No arguments found." + daikon.Daikon.lineSep + usage); + public static void mainHelper(String[] args) { + // Handles -h and --help before num_reps. A negative num_reps is left for parsePositiveInt to + // report. + if (args.length == 0 || !NEGATIVE_INTEGER.matcher(args[0]).matches()) { + args = DaikonGetopt.argsAfterLeadingOptions(args, usage); } - num_reps = Integer.parseInt(args[0]); - numPerSample = Integer.parseInt(args[1]); + if (args.length < 2) { + throw new daikon.Daikon.UserError("Too few arguments; run with -h for usage"); + } - // process optional switches - // also deduce index of arg for Daikon - boolean knowArgStart = false; - for (int i = 2; i < args.length; i++) { - // allows seed setting - if (args[i].toUpperCase(Locale.ENGLISH).equals("-SEED")) { + int numReps = parsePositiveInt("num_reps", args[0]); + int numPerSample = parsePositiveInt("sample_size", args[1]); + + // If true, delete the trace samples after the invariants from them have been generated. + boolean clean = true; + // If true, select method invocations that entered the method successfully but did not exit + // normally, either from a thrown Exception or abnormal termination. + boolean includeUnreturned = false; + // If true, create a spinfo file for generating conditional invariants and implications by + // running daikon.diff.MultiDiff over each of the samples and finding properties that appear + // in some but not all of the samples. + boolean doDiffs = false; + Random rand = new Random(); + + // Process the TraceSelect options, which precede the Daikon arguments. + int i = 2; + for (; i < args.length; i++) { + String option = args[i].toUpperCase(Locale.ENGLISH); + if (option.equals("-SEED")) { if (i + 1 >= args.length) { - throw new daikon.Daikon.UserError("-SEED options requires argument"); + throw new daikon.Daikon.UserError("-SEED requires an argument"); } - randObj = new Random(Long.parseLong(args[++i])); - daikonArgStart = i + 1; - } - - // NOCLEAN argument will leave the trace samples even after - // the invariants from these samples have been generated - else if (args[i].toUpperCase(Locale.ENGLISH).equals("-NOCLEAN")) { - CLEAN = false; - daikonArgStart = i + 1; - } - - // INCLUDE_UNRETURNED option will allow selecting method invocations - // that entered the method successfully but did not exit normally; - // either from a thrown Exception or abnormal termination. - else if (args[i].toUpperCase(Locale.ENGLISH).equals("-INCLUDE_UNRETURNED")) { - INCLUDE_UNRETURNED = true; - daikonArgStart = i + 1; - } - - // DO_DIFFS will create a spinfo file for generating - // conditional invariants and implications by running - // daikon.diff.Diff over each of the samples and finding - // properties that appear in some but not all of the - // samples. - else if (args[i].toUpperCase(Locale.ENGLISH).equals("-DO_DIFFS")) { - DO_DIFFS = true; - daikonArgStart = i + 1; + String seed = args[++i]; + try { + rand = new Random(Long.parseLong(seed)); + } catch (NumberFormatException e) { + throw new daikon.Daikon.UserError("-SEED requires an integer argument, not " + seed); + } + } else if (option.equals("-NOCLEAN")) { + clean = false; + } else if (option.equals("-INCLUDE_UNRETURNED")) { + includeUnreturned = true; + } else if (option.equals("-DO_DIFFS")) { + doDiffs = true; + } else if (TRACESELECT_OPTION.matcher(args[i]).matches()) { + throw new daikon.Daikon.UserError( + "Unrecognized TraceSelect option " + + args[i] + + "; the options are -SEED, -NOCLEAN, -INCLUDE_UNRETURNED, and -DO_DIFFS"); + } else { + break; } + } - // TODO: The current implementation assumes that a decls - // or dtrace file will be the first of the Daikon arguments, - // marking the end of the TraceSelect arguments. That is - // not necessarily true, especially in cases when someone - // uses a Daikon argument such as "--nohierarchy" or "--format java" - // and the manual examples place the arguments before any dtrace - // or decls arguments. - - // For now, only the first dtrace file will be sampled - else if (args[i].endsWith(".dtrace")) { - if (fileName == null) { - fileName = args[i]; - } else { - throw new daikon.Daikon.UserError("Only 1 dtrace file for input allowed"); + // The remaining arguments are the Daikon arguments, such as .dtrace and .decls files, + // "--nohierarchy", or "--format java". Only the .dtrace file is sampled. Daikon treats any + // file whose name contains ".dtrace" as a trace file, so every such file must be the one that + // is sampled. An option or an option's value, such as "--config foo.dtrace" or + // "--config=foo.dtrace", is not a file, even if it contains ".dtrace". + String inputFile = null; + List daikonArgs = new ArrayList<>(); + for (; i < args.length; i++) { + if (args[i].startsWith("-o")) { + // "-o foo.inv" or "-ofoo.inv". Daikon rejects a second -o. + throw new daikon.Daikon.UserError( + "TraceSelect does not accept Daikon's -o option: it writes the invariants of each" + + " sample to the sample's file name plus \".inv\""); + } else if (TRACESELECT_OPTION.matcher(args[i]).matches()) { + // A TraceSelect option after a Daikon argument, as in "foo.dtrace -NOCLEAN". Daikon + // would reject it with a message that does not mention TraceSelect. + throw new daikon.Daikon.UserError( + "TraceSelect option " + + args[i] + + " must precede the Daikon arguments, such as the .dtrace file"); + } else if (DAIKON_OPTIONS_WITH_ARGUMENT.contains(args[i])) { + daikonArgs.add(args[i]); + if (i + 1 < args.length) { + daikonArgs.add(args[++i]); } - - if (!knowArgStart) { - daikonArgStart = i; - knowArgStart = true; + } else if (args[i].startsWith("-")) { + daikonArgs.add(args[i]); + } else if (args[i].contains(".dtrace")) { + if (!args[i].endsWith(".dtrace")) { + throw new daikon.Daikon.UserError( + "TraceSelect cannot sample " + + args[i] + + "; the trace file must be uncompressed and its name must end with" + + " \".dtrace\""); } - } else if (args[i].endsWith(".decls")) { - if (!knowArgStart) { - daikonArgStart = i; - knowArgStart = true; + if (inputFile != null) { + throw new daikon.Daikon.UserError("Only 1 dtrace file for input allowed"); } + inputFile = args[i]; + } else { + daikonArgs.add(args[i]); } } - // if no seed provided, use default Random() constructor - if (randObj == null) { - randObj = new Random(); + // Check the Daikon arguments before doing any sampling, so that a bad argument is reported + // immediately. This also handles -h and --help. When there is a trace file, the arguments + // are the same as those of a sample's Daikon run, except for the .dtrace file, which does not + // exist yet. + if (inputFile != null || !daikonArgs.isEmpty()) { + checkDaikonArgs( + (inputFile == null) + ? daikonArgs.toArray(new String[0]) + : daikonCommandLine(inputFile, daikonArgs)); } - - sampleNames = new String[num_reps + 1]; - sampleNames[0] = "-p"; - - if (fileName == null) { - throw new daikon.Daikon.UserError("No .dtrace file name specified"); + if (inputFile == null) { + throw new daikon.Daikon.UserError( + "No .dtrace file name specified (the trace file must be uncompressed and its name must" + + " end with \".dtrace\")"); } - try { - - // invokeBuffer = new ArrayList(); - // fileName = args[1]; - - System.out.println("*******Processing********"); - - // Have to call the DtraceNonceDoctor - // to avoid the broken Dtrace from - // using a command-line 'cat' that - // results in repeat nonces - /* - String[] doctorArgs = new String[1]; - doctorArgs[0] = fileName; - DtraceNonceDoctor.main (doctorArgs ); - Runtime.getRuntime().exec ("mv " + doctorArgs[0] + "_fixed " + - doctorArgs[0]); - */ + // The .inv file for each sample, from the first sample to the last. + List sampleNames = new ArrayList<>(); - while (num_reps > 0) { + System.out.println("*******Processing********"); - List al = new ArrayList<>(); - try (DtracePartitioner dec = new DtracePartitioner(fileName)) { - MultiRandSelector mrs = new MultiRandSelector<>(numPerSample, dec); + try { + for (int rep = 1; rep <= numReps; rep++) { + + List al; + try { + al = selectSample(inputFile, numPerSample, rand, includeUnreturned); + } catch (IOException e) { + throw new daikon.Daikon.UserError( + e, "Cannot read trace file " + inputFile + ": " + e.getMessage()); + } - while (dec.hasNext()) { - mrs.accept(dec.next()); + String filePrefix = calcOut(inputFile, rep); + + sampleNames.add(filePrefix + ".inv"); + + try { + // Files.newBufferedWriter truncates an existing file, such as a sample from a previous + // run with -NOCLEAN. + try (PrintWriter pwOut = + new PrintWriter(Files.newBufferedWriter(Path.of(filePrefix), UTF_8))) { + for (String toPrint : al) { + pwOut.println(toPrint); + } + } catch (IOException e) { + throw new daikon.Daikon.UserError( + "Cannot write sample file " + filePrefix + ": " + e.getMessage()); } - for (Iterator i = mrs.valuesIter(); i.hasNext(); ) { - al.add(i.next()); + invokeDaikon(filePrefix, daikonArgs); + } finally { + // Clean up the mess, even if Daikon failed. A failed cleanup is not fatal. + if (clean) { + deleteQuietly(filePrefix); } - - List al_tmp = dec.patchValues(al, INCLUDE_UNRETURNED); - al = al_tmp; } + } - String filePrefix = calcOut(fileName); - - // must do num_reps - 1 because of "off by one" - // but now add a '-p' in the front so it's all good - sampleNames[num_reps] = filePrefix + ".inv"; + if (doDiffs) { + // spinfo format. The arguments are "-p" followed by the samples. + List multiDiffArgs = new ArrayList<>(); + multiDiffArgs.add("-p"); + multiDiffArgs.addAll(sampleNames); + daikon.diff.MultiDiff.mainHelper(multiDiffArgs.toArray(new String[0])); + } - try (PrintWriter pwOut = new PrintWriter(FilesPlume.newBufferedFileWriter(filePrefix))) { - for (String toPrint : al) { - pwOut.println(toPrint); - } - pwOut.flush(); + } catch (IOException e) { + throw new daikon.Daikon.UserError(e, "I/O error while processing samples: " + e.getMessage()); + } catch (ReflectiveOperationException e) { + throw new daikon.Daikon.BugInDaikon(e); + } finally { + // Clean up the mess! A failed cleanup is not fatal. + if (clean) { + for (String sampleName : sampleNames) { + deleteQuietly(sampleName); } + } + } + } - invokeDaikon(filePrefix); - - // Clean up the mess. A failed cleanup is not fatal. - if (CLEAN) { - deleteQuietly(filePrefix); + /** + * Randomly selects invocations from a trace file. + * + * @param inputFile the trace file to sample + * @param numPerSample the number of invocations to select for each program point + * @param rand the source of randomness + * @param includeUnreturned if true, also select invocations that entered a method but did not + * exit it normally + * @return the selected invocations, each followed by its matching exit, in no particular order + * @throws IOException if the trace file cannot be read + */ + public static List selectSample( + String inputFile, int numPerSample, Random rand, boolean includeUnreturned) + throws IOException { + try (DtracePartitioner dec = new DtracePartitioner(inputFile)) { + MultiRandSelector mrs = new MultiRandSelector<>(numPerSample, rand, dec); + + while (dec.hasNext()) { + // DtracePartitioner returns an empty string for a blank line or for a trailing EXIT. + String invocation = dec.next(); + if (!invocation.isEmpty()) { + mrs.accept(invocation); } - - num_reps--; } - if (DO_DIFFS) { - // histograms - // daikon.diff.Diff.main (sampleNames); - - // spinfo format - daikon.diff.MultiDiff.mainHelper(sampleNames); + List al = new ArrayList<>(); + for (Iterator iter = mrs.valuesIter(); iter.hasNext(); ) { + al.add(iter.next()); } - // Clean up the mess! A failed cleanup is not fatal. - // Start at index 1: sampleNames[0] is the "-p" sentinel, not a file. - if (CLEAN) { - for (int j = 1; j < sampleNames.length; j++) { - deleteQuietly(sampleNames[j]); - } - } + return dec.patchValues(al, includeUnreturned); + } catch (UncheckedIOException e) { + // DtracePartitioner's Iterator methods cannot throw IOException. + throw e.getCause(); + } + } - } catch (IOException | ReflectiveOperationException e) { - throw new Error(e); + /** + * Parses a command-line argument that must be a positive integer. + * + * @param name the name of the argument, for use in error messages + * @param arg the command-line argument + * @return the integer value of the argument + * @throws daikon.Daikon.UserError if the argument is not a positive integer + */ + private static int parsePositiveInt(String name, String arg) { + int result; + try { + result = Integer.parseInt(arg); + } catch (NumberFormatException e) { + result = 0; + } + if (result <= 0) { + throw new daikon.Daikon.UserError( + name + " must be a positive integer, not " + arg + "; run with -h for usage"); } + return result; } /** @@ -267,59 +353,71 @@ private static void deleteQuietly(String fileName) { } } - @RequiresNonNull("argles") - private static void invokeDaikon(String dtraceName) throws IOException { + /** + * Checks Daikon command-line arguments by passing them to {@link daikon.Daikon#read_options}. + * Handles -h and --help. + * + *

Like {@link daikon.Daikon#mainHelper}, this first calls {@link daikon.Daikon#cleanup}, + * because {@code read_options} rejects some options, such as {@code -o}, if the corresponding + * static field is already set. + * + * @param args the Daikon command-line arguments + * @throws daikon.Daikon.NormalTermination after printing the usage message, if an argument + * requests it + * @throws daikon.Daikon.UserError if an argument is bad + */ + private static void checkDaikonArgs(String[] args) { + daikon.Daikon.cleanup(); + daikon.Daikon.read_options(args, usage); + } - System.out.println("Created file: " + dtraceName); + /** + * Returns the command line for running Daikon on a sample. + * + * @param dtraceName the sample .dtrace file + * @param daikonArgs the arguments to pass to Daikon, other than the .dtrace file + * @return the command line for running Daikon on the sample + */ + private static String[] daikonCommandLine(String dtraceName, List daikonArgs) { + List result = new ArrayList<>(); + result.add(dtraceName); + result.add("-o"); + result.add(dtraceName + ".inv"); + result.addAll(daikonArgs); + return result.toArray(new String[0]); + } - // this part adds on the rest of the decls files - ArrayList daikonArgsList = new ArrayList<>(); - daikonArgsList.add(dtraceName); - daikonArgsList.add("-o"); - daikonArgsList.add(dtraceName + ".inv"); - - // find all the Daikon args except for the original - // single dtrace file. - for (int i = daikonArgStart; i < argles.length; i++) { - if (argles[i].endsWith(".dtrace")) { - continue; - } - daikonArgsList.add(argles[i]); - } + /** + * Runs Daikon on a sample, then runs PrintInvariants on the result. + * + * @param dtraceName the sample .dtrace file + * @param daikonArgs the arguments to pass to Daikon, other than the .dtrace file + * @throws IOException if PrintInvariants cannot read or write a file + * @throws ClassNotFoundException if PrintInvariants cannot deserialize the invariants + */ + private static void invokeDaikon(String dtraceName, List daikonArgs) + throws IOException, ClassNotFoundException { - // create an array to store the Strings in daikonArgsList - String[] daikonArgs = daikonArgsList.toArray(new String[0]); - - daikon.Daikon.mainHelper(daikonArgs); - // Run: java daikon.PrintInvariants dtraceName.inv > dtraceName.txt - ProcessBuilder pb = new ProcessBuilder("java", "daikon.PrintInvariants", dtraceName + ".inv"); - pb.redirectOutput(new File(dtraceName + ".txt")); - // In Java 26, `Process` implements `AutoCloseable`, so use try-with-resources. - @SuppressWarnings({ - "resourceleak:required.method.not.called", - "resourceleak:unneeded.suppression" - }) - Process p = pb.start(); - try { - p.waitFor(); - } catch (InterruptedException e) { - // do nothing - } + System.out.println("Created file: " + dtraceName); + + daikon.Daikon.mainHelper(daikonCommandLine(dtraceName, daikonArgs)); + // Reset the Daikon options, such as --format, so that PrintInvariants uses its defaults. + daikon.Daikon.cleanup(); + daikon.PrintInvariants.mainHelper( + new String[] {"--output", dtraceName + ".txt", dtraceName + ".inv"}); } - private static String calcOut(String strFileName) { - StringBuilder product = new StringBuilder(); - int index = strFileName.indexOf('.'); - if (index >= 0) { - product.append(strFileName.substring(0, index)); - product.append(num_reps); - if (index != strFileName.length()) { - product.append(strFileName.substring(index)); - } - } else { - product.append(strFileName).append("2"); - } - return product.toString(); + /** + * Returns the name of the file that holds the given sample of the given trace file. + * + * @param strFileName the name of the trace file; ends with ".dtrace" + * @param rep the number of the sample + * @return the name of the file that holds the sample + */ + private static String calcOut(String strFileName, int rep) { + // Insert the sample number into the file's name, not into the name of a directory. + String base = strFileName.substring(0, strFileName.length() - ".dtrace".length()); + return base + rep + ".dtrace"; } }