From a95b881bd126714aae5bd562ec4bd532768ebe30 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Tue, 6 Oct 2026 17:06:41 -0700 Subject: [PATCH 01/30] Reject bad command-line options with a non-zero exit status Add DaikonGetopt, a Getopt subclass that throws a UserError describing an unrecognized, ambiguous, or malformed option and handles -h and --help. Use it in Daikon and in the other tools that use Getopt. Remove Diff's -u and -z options, which had no effect. Co-Authored-By: Claude Opus 5.5 --- doc/CHANGELOG.md | 9 + doc/daikon.texinfo | 3 - java/Makefile | 2 +- java/daikon/Daikon.java | 18 +- java/daikon/DaikonGetopt.java | 279 ++++++++++++++++++ java/daikon/MergeInvariants.java | 21 +- java/daikon/PrintInvariants.java | 19 +- java/daikon/UnionInvariants.java | 18 +- java/daikon/diff/Diff.java | 40 +-- java/daikon/test/AllTestsSuite.java | 1 + java/daikon/test/BadOptionTest.java | 199 +++++++++++++ java/daikon/test/SampleTester.java | 47 +-- .../split/SplitterFactoryTestUpdater.java | 50 ++-- java/daikon/tools/DtraceDiff.java | 20 +- java/daikon/tools/ExtractConsequent.java | 18 +- java/daikon/tools/InvariantChecker.java | 19 +- java/daikon/tools/compare/LogicalCompare.java | 19 +- java/daikon/tools/jtb/Annotate.java | 24 +- java/daikon/tools/jtb/CreateSpinfo.java | 18 +- tests/diff-tests/Makefile.common | 4 +- tests/diff-tests/StackAr/StackAr-u.txt.goal | 105 ------- .../diff-tests/schedule2/schedule2-u.txt.goal | 77 ----- 22 files changed, 610 insertions(+), 400 deletions(-) create mode 100644 java/daikon/DaikonGetopt.java create mode 100644 java/daikon/test/BadOptionTest.java delete mode 100644 tests/diff-tests/StackAr/StackAr-u.txt.goal delete mode 100644 tests/diff-tests/schedule2/schedule2-u.txt.goal diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 6db6daed42..6aa334f3e8 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -21,6 +21,15 @@ Further documentation can be found in: `make -C $DAIKONDIR/java javadoc` It is also available at . +## Version 5.9.1 (???, 2026) + +* Daikon and its tools now reject an unrecognized, ambiguous, or malformed + command-line option: they describe the problem and exit with a non-zero + status. Previously, Daikon printed its usage message and exited with + status 0, and most other tools ignored the bad option and continued. +* `daikon.diff.Diff` no longer accepts the `-u` and `-z` options, which had + no effect. + ## Version 5.9.0 (September 1, 2026) * All Daikon tools now work with Java 8-26. diff --git a/doc/daikon.texinfo b/doc/daikon.texinfo index 2c7d2f72e6..63824ae74e 100644 --- a/doc/daikon.texinfo +++ b/doc/daikon.texinfo @@ -6749,9 +6749,6 @@ invariants must be justified. Does not print ``uninteresting'' invariants Display the tree of all invariants. Includes invariants that are the same in @var{file1} and @var{file2}, and unjustified invariants. -@item -u -Include ``uninteresting'' invariants in the tree of differing invariants. - @item -y @itemx --ignore_unjustified Include (statistically) unjustified invariants. diff --git a/java/Makefile b/java/Makefile index a259f02945..675725091f 100644 --- a/java/Makefile +++ b/java/Makefile @@ -181,7 +181,7 @@ JAVADOC_COMMAND := ${JAVADOC} -cp ${DAIKON_CLASSPATH} JAVAC_COMMAND := ${JAVAC} -cp ${DAIKON_CLASSPATH} -Xlint -J-Xmx7g ${JAVAC_ARGS} JAVA_COMMAND := ${JAVA} -cp ${DAIKON_CLASSPATH} -DOCLETPATH := ${DAIKONDIR}/java:$(wildcard ${DAIKONDIR}/java/lib/bcel-util-*.jar):$(wildcard ${DAIKONDIR}/java/lib/plume-util-*.jar):$(wildcard ${DAIKONDIR}/java/lib/reflection-util-*.jar):$(wildcard ${DAIKONDIR}/java/lib/hashmap-util-*.jar):${DAIKONDIR}/java/lib/daikon-plumelib.jar +DOCLETPATH := ${DAIKONDIR}/java:$(wildcard ${DAIKONDIR}/java/lib/java-getopt-*.jar):$(wildcard ${DAIKONDIR}/java/lib/bcel-util-*.jar):$(wildcard ${DAIKONDIR}/java/lib/plume-util-*.jar):$(wildcard ${DAIKONDIR}/java/lib/reflection-util-*.jar):$(wildcard ${DAIKONDIR}/java/lib/hashmap-util-*.jar):${DAIKONDIR}/java/lib/daikon-plumelib.jar DOCLET_FLAGS := LINKOFFLINE := https://docs.oracle.com/javase/8/docs/api/ diff --git a/java/daikon/Daikon.java b/java/daikon/Daikon.java index 1e7c1e1b73..aaa2b3f3d6 100644 --- a/java/daikon/Daikon.java +++ b/java/daikon/Daikon.java @@ -1022,7 +1022,6 @@ static FileOptions read_options(String[] args, String usage) { LongOpt[] longopts = new LongOpt[] { // Control output - new LongOpt(help_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(no_text_output_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(format_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(show_progress_SWITCH, LongOpt.NO_ARGUMENT, null, 0), @@ -1055,7 +1054,7 @@ static FileOptions read_options(String[] args, String usage) { new LongOpt(disc_reason_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(mem_stat_SWITCH, LongOpt.NO_ARGUMENT, null, 0), }; - Getopt g = new Getopt("daikon.Daikon", args, "ho:", longopts); + DaikonGetopt g = new DaikonGetopt(args, "o:", longopts, usage); int c; while ((c = g.getopt()) != -1) { @@ -1065,10 +1064,7 @@ static FileOptions read_options(String[] args, String usage) { String option_name = longopts[g.getLongind()].getName(); // Control output - if (help_SWITCH.equals(option_name)) { - System.out.println(usage); - throw new Daikon.NormalTermination(); - } else if (no_text_output_SWITCH.equals(option_name)) { + if (no_text_output_SWITCH.equals(option_name)) { no_text_output = true; } else if (format_SWITCH.equals(option_name)) { String format_name = getOptarg(g); @@ -1367,12 +1363,9 @@ else if (ppt_regexp_SWITCH.equals(option_name)) { } else if (mem_stat_SWITCH.equals(option_name)) { use_mem_monitor = true; } else { - throw new Daikon.UserError("Unknown option " + option_name + " on command line"); + throw new Daikon.BugInDaikon("Unhandled long option " + option_name); } break; - case 'h': - System.out.println(usage); - throw new Daikon.NormalTermination(); case 'o': String inv_filename = getOptarg(g); @@ -1391,11 +1384,6 @@ else if (ppt_regexp_SWITCH.equals(option_name)) { } break; // - case '?': - // break; // getopt() already printed an error - System.out.println(usage); - throw new Daikon.NormalTermination(); - // default: throw new Daikon.BugInDaikon("getopt() returned " + c); } diff --git a/java/daikon/DaikonGetopt.java b/java/daikon/DaikonGetopt.java new file mode 100644 index 0000000000..0f71e31406 --- /dev/null +++ b/java/daikon/DaikonGetopt.java @@ -0,0 +1,279 @@ +package daikon; + +import gnu.getopt.Getopt; +import gnu.getopt.LongOpt; +import java.util.Arrays; +import java.util.function.Supplier; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.plumelib.util.ArraysPlume; + +/** + * A command-line option processor that throws a {@link Daikon.UserError} that describes a bad + * command-line option, instead of printing a message and returning {@code '?'} or {@code ':'}. + * + *

If constructed with a usage message, it also handles {@code -h} and {@code --help}: it prints + * the usage message and throws {@link Daikon.NormalTermination}. + */ +public class DaikonGetopt extends Getopt { + + /** + * The name passed to {@link Getopt}, which uses it only in messages that DaikonGetopt suppresses. + */ + private static final String PROGNAME = "DaikonGetopt"; + + /** + * Computes the usage message printed for {@code -h} and {@code --help}, or null if the caller + * handles them. + */ + private final @Nullable Supplier usage; + + /** Text appended to the description of a bad command-line option, or null to append nothing. */ + private @Nullable String usageHint = "run with -h for usage"; + + /** + * Creates a command-line option processor that recognizes short and long options. + * + * @param argv the command-line arguments + * @param optstring the short options, in the format of {@link Getopt} + * @param longopts the long options + */ + public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts) { + super(PROGNAME, argv, optstring, longopts); + this.usage = null; + opterr = false; + } + + /** + * Creates a command-line option processor that recognizes short and long options, plus {@code -h} + * and {@code --help}, which print the usage message and throw {@link Daikon.NormalTermination}. + * + * @param argv the command-line arguments + * @param optstring the short options, in the format of {@link Getopt}; must not contain {@code h} + * @param longopts the long options; must not contain {@code help} + * @param usage the usage message to print for {@code -h} and {@code --help} + */ + public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts, String usage) { + this(argv, optstring, longopts, () -> usage); + } + + /** + * Creates a command-line option processor that recognizes short and long options, plus {@code -h} + * and {@code --help}, which print the usage message and throw {@link Daikon.NormalTermination}. + * The usage message is computed only if it is needed. + * + * @param argv the command-line arguments + * @param optstring the short options, in the format of {@link Getopt}; must not contain {@code h} + * @param longopts the long options; must not contain {@code help} + * @param usage computes the usage message to print for {@code -h} and {@code --help} + */ + public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts, Supplier usage) { + super(PROGNAME, argv, withH(optstring), withHelp(longopts)); + this.usage = usage; + opterr = false; + } + + /** + * Returns the given short options plus {@code -h}. + * + * @param optstring short options, in the format of {@link Getopt}; must not contain {@code h} + * @return {@code optstring} plus {@code -h} + */ + private static String withH(String optstring) { + if (optstring.indexOf('h') != -1) { + throw new IllegalArgumentException("optstring already contains h: " + optstring); + } + return optstring + "h"; + } + + /** + * Returns the given long options plus {@code --help}. + * + * @param longopts long options; must not contain {@code help} + * @return {@code longopts} plus {@code --help} + */ + private static LongOpt[] withHelp(LongOpt[] longopts) { + for (LongOpt longopt : longopts) { + if (longopt.getName().equals(Daikon.help_SWITCH)) { + throw new IllegalArgumentException("longopts already contains " + Daikon.help_SWITCH); + } + } + return ArraysPlume.append( + longopts, new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 'h')); + } + + /** + * Processes the command-line arguments of a program that takes no options other than {@code -h} + * and {@code --help}. + * + * @param argv the command-line arguments + * @param usage the usage message to print for {@code -h} and {@code --help} + * @return the arguments that are not options + * @throws Daikon.NormalTermination after printing the usage message, if an argument requests it + * @throws Daikon.UserError if an argument is any other option + */ + public static String[] nonOptionArgs(String[] argv, String usage) { + return argsAfterOptions(argv, "", () -> usage); + } + + /** + * Processes the leading command-line arguments of a program that takes no options other than + * {@code -h} and {@code --help}. Processing stops at the first argument that is not an option, so + * later arguments may be options for some other program. + * + * @param argv the command-line arguments + * @param usage the usage message to print for {@code -h} and {@code --help} + * @return the arguments from the first one that is not an option onward + * @throws Daikon.NormalTermination after printing the usage message, if a leading argument + * requests it + * @throws Daikon.UserError if a leading argument is any other option + */ + public static String[] argsAfterLeadingOptions(String[] argv, String usage) { + return argsAfterLeadingOptions(argv, () -> usage); + } + + /** + * Like {@link #argsAfterLeadingOptions(String[], String)}, but computes the usage message only if + * it is needed. + * + * @param argv the command-line arguments + * @param usage computes the usage message to print for {@code -h} and {@code --help} + * @return the arguments from the first one that is not an option onward + * @throws Daikon.NormalTermination after printing the usage message, if a leading argument + * requests it + * @throws Daikon.UserError if a leading argument is any other option + */ + public static String[] argsAfterLeadingOptions(String[] argv, Supplier usage) { + // A leading "+" tells Getopt to stop at the first argument that is not an option. + return argsAfterOptions(argv, "+", usage); + } + + /** + * Processes command-line arguments, permitting no options other than {@code -h} and {@code + * --help}. + * + * @param argv the command-line arguments + * @param optstring "" to process all the arguments, or "+" to stop at the first non-option + * @param usage computes the usage message to print for {@code -h} and {@code --help} + * @return the arguments that Getopt did not process as options + * @throws Daikon.NormalTermination after printing the usage message, if an argument requests it + * @throws Daikon.UserError if an argument is any other option + */ + private static String[] argsAfterOptions( + String[] argv, String optstring, Supplier usage) { + // Getopt permutes its argument array, so give it a copy to avoid modifying the caller's array. + String[] argvCopy = argv.clone(); + DaikonGetopt g = new DaikonGetopt(argvCopy, optstring, new LongOpt[0], usage); + // Every option other than -h and --help is bad, so getopt() returns only -1 or throws. + int c = g.getopt(); + if (c != -1) { + throw new Daikon.BugInDaikon("getopt() returned " + c); + } + return Arrays.copyOfRange(argvCopy, g.getOptind(), argvCopy.length); + } + + /** + * Sets the text that is appended to the description of a bad command-line option. The default + * tells the user to run the program with -h. + * + * @param usageHint text appended to the description of a bad command-line option, or null to + * append nothing + */ + public void setUsageHint(@Nullable String usageHint) { + this.usageHint = usageHint; + } + + /** + * Like {@link Getopt#getopt()}, but never returns {@code '?'} or {@code ':'}. Getopt returns + * {@code ':'} for a missing argument if the optstring starts with {@code ':'}. If this was + * constructed with a usage message, also never returns {@code 'h'}. + * + * @return the next option, as described in {@link Getopt#getopt()} + * @throws Daikon.UserError if the next option is unrecognized, ambiguous, or malformed + * @throws Daikon.NormalTermination after printing the usage message, if the next option is {@code + * -h} or {@code --help} and this was constructed with a usage message + */ + @Override + public int getopt() { + int c = super.getopt(); + if (c == '?' || c == ':') { + throw badOptionError(); + } + if (c == 'h' && usage != null) { + System.out.println(usage.get()); + throw new Daikon.NormalTermination(); + } + return c; + } + + /** + * Returns an exception that describes the bad command-line option that {@link Getopt#getopt()} + * just rejected. + * + * @return an exception that describes the bad command-line option + */ + private Daikon.UserError badOptionError() { + String message = badOptionMessage(); + if (usageHint != null) { + message += "; " + usageHint; + } + return new Daikon.UserError(message); + } + + /** + * Returns a description of the bad command-line option that {@link Getopt#getopt()} just + * rejected. + * + * @return a description of the bad command-line option + */ + private String badOptionMessage() { + // For every bad long option, Getopt clears nextchar and advances optind past the option. A + // bad short option has no such guarantee, because it may be followed by other short options in + // the same argument, as in "-xh". + if ("".equals(nextchar) && argv[optind - 1].startsWith("--")) { + return longOptionMessage(argv[optind - 1]); + } + char c = (char) optopt; + if (c == ':' || optstring.indexOf(c) == -1) { + return "Unrecognized command-line option -" + c; + } else { + return "Command-line option -" + c + " requires an argument"; + } + } + + /** + * Returns a description of a bad long command-line option. Uses {@code longind}, which Getopt + * sets to the index of the long option that matched the argument, or to -1 if none matched. + * + * @param arg the bad command-line argument, which starts with "--" + * @return a description of the bad command-line option + */ + private String longOptionMessage(String arg) { + int equalsPos = arg.indexOf('='); + String name = (equalsPos == -1) ? arg.substring(2) : arg.substring(2, equalsPos); + LongOpt[] longopts = long_options; + // Getopt treats an empty name, as in "--=foo", as a prefix of every long option. + if (name.isEmpty()) { + return "Unrecognized command-line option " + arg; + } + if (longopts == null || longind == -1) { + return "Unrecognized command-line option --" + name; + } + LongOpt match = longopts[longind]; + // For an inexact match, Getopt sets longind to the first long option that has the given + // prefix. The match is ambiguous if a later long option also has the prefix. + if (!match.getName().equals(name)) { + for (int i = longind + 1; i < longopts.length; i++) { + if (longopts[i].getName().startsWith(name)) { + return "Ambiguous command-line option --" + name; + } + } + } + if (equalsPos != -1 && match.getHasArg() == LongOpt.NO_ARGUMENT) { + return "Command-line option --" + match.getName() + " does not take an argument"; + } else if (equalsPos == -1 && match.getHasArg() == LongOpt.REQUIRED_ARGUMENT) { + return "Command-line option --" + match.getName() + " requires an argument"; + } else { + return "Bad command-line option " + arg; + } + } +} diff --git a/java/daikon/MergeInvariants.java b/java/daikon/MergeInvariants.java index bc2009da5d..8a814a80fb 100644 --- a/java/daikon/MergeInvariants.java +++ b/java/daikon/MergeInvariants.java @@ -5,7 +5,6 @@ import daikon.split.PptSplitter; import daikon.suppress.NIS; -import gnu.getopt.Getopt; import gnu.getopt.LongOpt; import java.io.File; import java.io.FileNotFoundException; @@ -97,14 +96,13 @@ public static void mainHelper(String[] args) LongOpt[] longopts = new LongOpt[] { - new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.config_option_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(Daikon.debugAll_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.debug_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(Daikon.track_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), }; - Getopt g = new Getopt("daikon.MergeInvariants", args, "ho:", longopts); + DaikonGetopt g = new DaikonGetopt(args, "o:", longopts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { @@ -112,10 +110,7 @@ public static void mainHelper(String[] args) // long option case 0: String option_name = longopts[g.getLongind()].getName(); - if (Daikon.help_SWITCH.equals(option_name)) { - System.out.println(usage); - throw new Daikon.NormalTermination(); - } else if (Daikon.config_option_SWITCH.equals(option_name)) { + if (Daikon.config_option_SWITCH.equals(option_name)) { String item = Daikon.getOptarg(g); daikon.config.Configuration.getInstance().apply(item); break; @@ -133,14 +128,10 @@ public static void mainHelper(String[] args) "Error parsing track argument '" + Daikon.getOptarg(g) + "' - " + error); } } else { - throw new Daikon.UserError("Unknown long option received: " + option_name); + throw new Daikon.BugInDaikon("Unhandled long option " + option_name); } break; - case 'h': - System.out.println(usage); - throw new Daikon.NormalTermination(); - case 'o': String output_inv_filename = Daikon.getOptarg(g); @@ -160,12 +151,8 @@ public static void mainHelper(String[] args) } break; - case '?': - break; // getopt() already printed an error - default: - System.out.println("getopt() returned " + c); - break; + throw new Daikon.BugInDaikon("getopt() returned " + c); } } diff --git a/java/daikon/PrintInvariants.java b/java/daikon/PrintInvariants.java index f7bc9121c3..57f483661d 100644 --- a/java/daikon/PrintInvariants.java +++ b/java/daikon/PrintInvariants.java @@ -24,7 +24,6 @@ import daikon.split.PptSplitter; import daikon.suppress.NIS; import daikon.suppress.NISuppressionSet; -import gnu.getopt.Getopt; import gnu.getopt.LongOpt; import java.io.BufferedWriter; import java.io.File; @@ -329,7 +328,6 @@ public static void mainHelper(String[] args) LongOpt[] longopts = new LongOpt[] { - new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.format_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(Daikon.suppress_redundant_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.output_num_samples_SWITCH, LongOpt.NO_ARGUMENT, null, 0), @@ -344,17 +342,14 @@ public static void mainHelper(String[] args) new LongOpt( PrintInvariants.print_csharp_metadata_SWITCH, LongOpt.OPTIONAL_ARGUMENT, null, 0), }; - Getopt g = new Getopt("daikon.PrintInvariants", args, "h", longopts); + DaikonGetopt g = new DaikonGetopt(args, "", longopts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { case 0: // got a long option String option_name = longopts[g.getLongind()].getName(); - if (Daikon.help_SWITCH.equals(option_name)) { - System.out.println(usage); - throw new Daikon.NormalTermination(); - } else if (Daikon.ppt_regexp_SWITCH.equals(option_name)) { + if (Daikon.ppt_regexp_SWITCH.equals(option_name)) { if (ppt_regexp != null) { throw new Error( "multiple --" @@ -430,17 +425,11 @@ public static void mainHelper(String[] args) } else if (Daikon.wrap_xml_SWITCH.equals(option_name)) { wrap_xml = true; } else { - throw new RuntimeException("Unknown long option received: " + option_name); + throw new Daikon.BugInDaikon("Unhandled long option " + option_name); } break; - case 'h': - System.out.println(usage); - throw new Daikon.NormalTermination(); - case '?': - break; // getopt() already printed an error default: - System.out.println("getopt() returned " + c); - break; + throw new Daikon.BugInDaikon("getopt() returned " + c); } } diff --git a/java/daikon/UnionInvariants.java b/java/daikon/UnionInvariants.java index ea34b0f075..c07e19d7ce 100644 --- a/java/daikon/UnionInvariants.java +++ b/java/daikon/UnionInvariants.java @@ -1,6 +1,5 @@ package daikon; -import gnu.getopt.Getopt; import gnu.getopt.LongOpt; import java.io.File; import org.plumelib.util.FilesPlume; @@ -52,25 +51,19 @@ public static void mainHelper(String[] args) throws Exception { new LongOpt[] { new LongOpt(Daikon.suppress_redundant_SWITCH, LongOpt.NO_ARGUMENT, null, 0), }; - Getopt g = new Getopt("daikon.UnionInvariants", args, "ho:", longopts); + DaikonGetopt g = new DaikonGetopt(args, "o:", longopts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { case 0: // got a long option String option_name = longopts[g.getLongind()].getName(); - if (Daikon.help_SWITCH.equals(option_name)) { - System.out.println(usage); - throw new Daikon.NormalTermination(); - } else if (Daikon.suppress_redundant_SWITCH.equals(option_name)) { + if (Daikon.suppress_redundant_SWITCH.equals(option_name)) { Daikon.suppress_redundant_invariants_with_simplify = true; } else { - throw new Daikon.UserError("Unknown option received: " + option_name); + throw new Daikon.BugInDaikon("Unhandled long option " + option_name); } break; - case 'h': - System.out.println(usage); - throw new Daikon.NormalTermination(); case 'o': String inv_filename = Daikon.getOptarg(g); @@ -90,11 +83,8 @@ public static void mainHelper(String[] args) throws Exception { } break; // - case '?': - break; // getopt() already printed an error default: - System.out.println("getopt() returned " + c); - break; + throw new Daikon.BugInDaikon("getopt() returned " + c); } } diff --git a/java/daikon/diff/Diff.java b/java/daikon/diff/Diff.java index 0019d1e935..6355ef0892 100644 --- a/java/daikon/diff/Diff.java +++ b/java/daikon/diff/Diff.java @@ -4,13 +4,13 @@ import static java.util.logging.Level.INFO; import daikon.Daikon; +import daikon.DaikonGetopt; import daikon.FileIO; import daikon.Ppt; import daikon.PptConditional; import daikon.PptMap; import daikon.PptTopLevel; import daikon.inv.Invariant; -import gnu.getopt.Getopt; import gnu.getopt.LongOpt; import java.io.File; import java.io.FileNotFoundException; @@ -68,13 +68,19 @@ public final class Diff { " For a list of flags, see the Daikon manual, which appears in the ", " Daikon distribution and also at http://plse.cs.washington.edu/daikon/."); - /** The long command line options. */ - private static final String HELP_SWITCH = "help"; - + /** The long command-line option that specifies the comparator to sort the first set of invs. */ private static final String INV_SORT_COMPARATOR1_SWITCH = "invSortComparator1"; + + /** The long command-line option that specifies the comparator to sort the second set of invs. */ private static final String INV_SORT_COMPARATOR2_SWITCH = "invSortComparator2"; + + /** The long command-line option that specifies the comparator to pair up invs. */ private static final String INV_PAIR_COMPARATOR_SWITCH = "invPairComparator"; + + /** The long command-line option that is a synonym for -y. */ private static final String IGNORE_UNJUSTIFIED_SWITCH = "ignore_unjustified"; + + /** The long command-line option that ignores numbered exit points. */ private static final String IGNORE_NUMBERED_EXITS_SWITCH = "ignore_exitNN"; /** Determine which ppts should be paired together in the tree. */ @@ -193,7 +199,6 @@ public static void mainHelper(final String[] args) LongOpt[] longOpts = new LongOpt[] { - new LongOpt(HELP_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(INV_SORT_COMPARATOR1_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(INV_SORT_COMPARATOR2_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(INV_PAIR_COMPARATOR_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), @@ -201,20 +206,14 @@ public static void mainHelper(final String[] args) new LongOpt(IGNORE_NUMBERED_EXITS_SWITCH, LongOpt.NO_ARGUMENT, null, 0), }; - Getopt g = - new Getopt( - "daikon.diff.Diff", args, - "Hhyduastmxno:jzpevl", longOpts); + DaikonGetopt g = new DaikonGetopt(args, "Hydastmxno:jpevl", longOpts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { case 0: // got a long option String optionName = longOpts[g.getLongind()].getName(); - if (Daikon.help_SWITCH.equals(optionName)) { - System.out.println(usage); - throw new Daikon.NormalTermination(); - } else if (INV_SORT_COMPARATOR1_SWITCH.equals(optionName)) { + if (INV_SORT_COMPARATOR1_SWITCH.equals(optionName)) { if (invSortComparator1Classname != null) { throw new Error( "multiple --" @@ -252,12 +251,9 @@ public static void mainHelper(final String[] args) ignoreNumberedExits = true; break; } else { - throw new RuntimeException("Unknown long option received: " + optionName); + throw new Daikon.BugInDaikon("Unhandled long option " + optionName); } break; - case 'h': - System.out.println(usage); - throw new Daikon.NormalTermination(); case 'H': PrintAllVisitor.HUMAN_OUTPUT = true; break; @@ -318,13 +314,8 @@ public static void mainHelper(final String[] args) case 'l': logging = true; break; - case '?': - // getopt() already printed an error - System.out.println(usage); - throw new Daikon.UserError("Bad argument"); default: - System.out.println("getopt() returned " + c); - break; + throw new Daikon.BugInDaikon("getopt() returned " + c); } } @@ -422,8 +413,7 @@ public static void mainHelper(final String[] args) v1.printAll(); return; } else { - System.out.println(usage); - throw new Daikon.NormalTermination(); + throw new Daikon.UserError("No invariant files specified" + Daikon.lineSep + usage); } if (logging) { diff --git a/java/daikon/test/AllTestsSuite.java b/java/daikon/test/AllTestsSuite.java index 7d74e7e7ac..9a817b266f 100644 --- a/java/daikon/test/AllTestsSuite.java +++ b/java/daikon/test/AllTestsSuite.java @@ -7,6 +7,7 @@ @RunWith(Suite.class) @Suite.SuiteClasses({ daikon.chicory.RuntimeTest.class, + daikon.test.BadOptionTest.class, daikon.test.TestClassOrInterfaceTypeDecorateVisitor.class, daikon.test.TestAst.class, daikon.test.config.ConfigurationTest.class, diff --git a/java/daikon/test/BadOptionTest.java b/java/daikon/test/BadOptionTest.java new file mode 100644 index 0000000000..af9b745d24 --- /dev/null +++ b/java/daikon/test/BadOptionTest.java @@ -0,0 +1,199 @@ +package daikon.test; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThrows; + +import daikon.Daikon; +import daikon.DaikonGetopt; +import daikon.PrintInvariants; +import daikon.UnionInvariants; +import daikon.tools.DtraceDiff; +import gnu.getopt.LongOpt; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import org.junit.Test; +import org.junit.function.ThrowingRunnable; + +/** + * Tests that Daikon tools reject bad command-line options with a UserError. + * + *

These tests do not call {@code Daikon.mainHelper}, because it resets Daikon's global state, + * which other tests in the same JVM depend on. + */ +public class BadOptionTest { + + /** The long options used by {@link #parse}. */ + private static final LongOpt[] longopts = + new LongOpt[] { + new LongOpt("help", LongOpt.NO_ARGUMENT, null, 0), + new LongOpt("config", LongOpt.REQUIRED_ARGUMENT, null, 0), + new LongOpt("config_option", LongOpt.REQUIRED_ARGUMENT, null, 0), + new LongOpt("debug", LongOpt.REQUIRED_ARGUMENT, null, 'd'), + new LongOpt("verbose", LongOpt.NO_ARGUMENT, null, 'v'), + }; + + /** + * Parses the given command-line arguments, which must contain a bad option, and returns the + * resulting error message. + * + * @param args command-line arguments + * @return the message of the UserError for the bad option + */ + private static String parse(String... args) { + DaikonGetopt g = new DaikonGetopt(args, "ho:", longopts); + Daikon.UserError e = assertThrows(Daikon.UserError.class, () -> consumeAll(g)); + return String.valueOf(e.getMessage()); + } + + /** + * Calls {@code getopt()} until all command-line arguments have been processed. + * + * @param g the command-line parser + */ + private static void consumeAll(DaikonGetopt g) { + while (g.getopt() != -1) {} + } + + /** + * Returns the expected message for a bad option. + * + * @param description a description of the bad option + * @return the expected message for a bad option + */ + private static String expected(String description) { + return description + "; run with -h for usage"; + } + + /** Tests an unrecognized long option. */ + @Test + public void testUnrecognizedLongOption() { + assertEquals(expected("Unrecognized command-line option --bogus"), parse("--bogus")); + assertEquals(expected("Unrecognized command-line option --bogus"), parse("--bogus=3")); + assertEquals(expected("Unrecognized command-line option --=3"), parse("--=3")); + assertEquals( + expected("Unrecognized command-line option --bogus"), parse("-h", "--bogus", "file")); + } + + /** Tests an unrecognized short option, alone and combined with a valid one. */ + @Test + public void testUnrecognizedShortOption() { + assertEquals(expected("Unrecognized command-line option -x"), parse("-x")); + assertEquals(expected("Unrecognized command-line option -x"), parse("-xh")); + assertEquals(expected("Unrecognized command-line option -x"), parse("-hx")); + assertEquals(expected("Unrecognized command-line option -:"), parse("-:")); + } + + /** Tests an ambiguous abbreviation of a long option. */ + @Test + public void testAmbiguousLongOption() { + assertEquals(expected("Ambiguous command-line option --conf"), parse("--conf=x")); + assertEquals(expected("Ambiguous command-line option --conf"), parse("--conf")); + } + + /** Tests that a usage hint of null appends nothing to the description of a bad option. */ + @Test + public void testNoUsageHint() { + DaikonGetopt g = new DaikonGetopt(new String[] {"--bogus"}, "h", longopts); + g.setUsageHint(null); + Daikon.UserError e = assertThrows(Daikon.UserError.class, g::getopt); + assertEquals("Unrecognized command-line option --bogus", String.valueOf(e.getMessage())); + } + + /** Tests options that are missing their required argument. */ + @Test + public void testMissingArgument() { + assertEquals(expected("Command-line option --config requires an argument"), parse("--config")); + assertEquals(expected("Command-line option --debug requires an argument"), parse("--deb")); + assertEquals(expected("Command-line option -o requires an argument"), parse("-o")); + assertEquals(expected("Command-line option -o requires an argument"), parse("-ho")); + } + + /** Tests options that are missing their required argument, when the optstring starts with ':'. */ + @Test + public void testMissingArgumentLeadingColon() { + DaikonGetopt g = new DaikonGetopt(new String[] {"-o"}, ":ho:", longopts); + Daikon.UserError e = assertThrows(Daikon.UserError.class, g::getopt); + assertEquals( + expected("Command-line option -o requires an argument"), String.valueOf(e.getMessage())); + g = new DaikonGetopt(new String[] {"--config"}, ":ho:", longopts); + e = assertThrows(Daikon.UserError.class, g::getopt); + assertEquals( + expected("Command-line option --config requires an argument"), + String.valueOf(e.getMessage())); + } + + /** Tests long options that are given an argument they do not take. */ + @Test + public void testUnexpectedArgument() { + assertEquals( + expected("Command-line option --help does not take an argument"), parse("--help=x")); + assertEquals( + expected("Command-line option --verbose does not take an argument"), parse("--verb=x")); + } + + /** + * Asserts that running {@code mainCall} throws a UserError with the given message. + * + * @param description a description of the bad option + * @param mainCall a call to a tool's {@code mainHelper} method + */ + private static void assertBadOption(String description, ThrowingRunnable mainCall) { + Daikon.UserError e = assertThrows(Daikon.UserError.class, mainCall); + assertEquals(expected(description), String.valueOf(e.getMessage())); + } + + /** Tests an unrecognized long option in a tool. */ + @Test + public void testPrintInvariantsUnrecognizedOption() { + assertBadOption( + "Unrecognized command-line option --bogus", + () -> PrintInvariants.mainHelper(new String[] {"--bogus", "foo.inv.gz"})); + } + + /** 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"})); + } + + /** + * Asserts that the given code prints a usage message to standard output and throws {@link + * Daikon.NormalTermination}. Captures the usage message so that it does not clutter the test + * output. + * + * @param code the code to run + */ + private static void assertPrintsUsage(ThrowingRunnable code) { + PrintStream oldOut = System.out; + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + System.setOut(new PrintStream(bytes, true, UTF_8)); + try { + assertThrows(Daikon.NormalTermination.class, code); + } finally { + System.setOut(oldOut); + } + assertNotEquals("", bytes.toString(UTF_8).trim()); + } + + /** Tests {@link DaikonGetopt#nonOptionArgs}. */ + @Test + public void testNonOptionArgs() { + assertArrayEquals( + new String[] {"a.dtrace", "b.dtrace"}, + DaikonGetopt.nonOptionArgs(new String[] {"a.dtrace", "b.dtrace"}, "u")); + assertArrayEquals( + new String[] {"-a.dtrace"}, + DaikonGetopt.nonOptionArgs(new String[] {"--", "-a.dtrace"}, "u")); + assertBadOption( + "Unrecognized command-line option --bogus", + () -> DaikonGetopt.nonOptionArgs(new String[] {"x", "--bogus"}, "u")); + assertBadOption( + "Unrecognized command-line option -x", + () -> DaikonGetopt.nonOptionArgs(new String[] {"-x"}, "u")); + assertPrintsUsage(() -> DaikonGetopt.nonOptionArgs(new String[] {"x", "--help"}, "u")); + } +} diff --git a/java/daikon/test/SampleTester.java b/java/daikon/test/SampleTester.java index 40919edb6b..b984841454 100644 --- a/java/daikon/test/SampleTester.java +++ b/java/daikon/test/SampleTester.java @@ -8,6 +8,7 @@ import static org.junit.Assert.fail; import daikon.Daikon; +import daikon.DaikonGetopt; import daikon.Debug; import daikon.FileIO; import daikon.Global; @@ -18,7 +19,6 @@ import daikon.ValueTuple; import daikon.VarInfo; import daikon.inv.Invariant; -import gnu.getopt.Getopt; import gnu.getopt.LongOpt; import java.io.File; import java.io.IOException; @@ -67,7 +67,7 @@ public class SampleTester { /** The usage message for this program. */ private static String usage = StringsPlume.joinLines( - "Usage: java daikon.PrintInvariants [OPTION]... FILE", + "Usage: java daikon.test.SampleTester [OPTION]...", " -h, --" + Daikon.help_SWITCH, " Display this usage message", " --" + Daikon.config_option_SWITCH, @@ -78,8 +78,29 @@ public class SampleTester { " Specify a class, varinfos, and ppt to debug track.", " Format is class@ppt"); - @SuppressWarnings("JUnitMethodInvoked") // main() runs the tests without JUnit + /** + * Runs the tests without JUnit. + * + * @param args command-line arguments, like those of {@link #mainHelper} + * @throws IOException if there is a problem reading a test file + */ public static void main(String[] args) throws IOException { + try { + mainHelper(args); + } catch (Daikon.DaikonTerminationException e) { + Daikon.handleDaikonTerminationException(e); + } + } + + /** + * This does the work of {@link #main(String[])}, but it never calls System.exit, so it is + * appropriate to be called programmatically. + * + * @param args command-line arguments, like those of {@link #main} + * @throws IOException if there is a problem reading a test file + */ + @SuppressWarnings("JUnitMethodInvoked") // mainHelper() runs the tests without JUnit + public static void mainHelper(String[] args) throws IOException { LongOpt[] longopts = new LongOpt[] { @@ -89,7 +110,7 @@ public static void main(String[] args) throws IOException { new LongOpt(Daikon.track_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), }; - Getopt g = new Getopt("daikon.test.SampleTester", args, "h:", longopts); + DaikonGetopt g = new DaikonGetopt(args, "", longopts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { @@ -97,11 +118,7 @@ public static void main(String[] args) throws IOException { // long option case 0: String option_name = longopts[g.getLongind()].getName(); - if (Daikon.help_SWITCH.equals(option_name)) { - System.out.println(usage); - throw new Daikon.NormalTermination(); - - } else if (Daikon.config_option_SWITCH.equals(option_name)) { + if (Daikon.config_option_SWITCH.equals(option_name)) { String item = Daikon.getOptarg(g); daikon.config.Configuration.getInstance().apply(item); break; @@ -119,20 +136,12 @@ public static void main(String[] args) throws IOException { "Error parsing track argument '" + Daikon.getOptarg(g) + "' - " + error); } } else { - throw new RuntimeException("Unknown long option received: " + option_name); + throw new Daikon.BugInDaikon("Unhandled long option " + option_name); } break; - case 'h': - System.out.println(usage); - throw new Daikon.NormalTermination(); - - case '?': - break; // getopt() already printed an error - default: - System.out.println("getopt() returned " + c); - break; + throw new Daikon.BugInDaikon("getopt() returned " + c); } } diff --git a/java/daikon/test/split/SplitterFactoryTestUpdater.java b/java/daikon/test/split/SplitterFactoryTestUpdater.java index 0c806f6071..c5be608fc7 100644 --- a/java/daikon/test/split/SplitterFactoryTestUpdater.java +++ b/java/daikon/test/split/SplitterFactoryTestUpdater.java @@ -195,7 +195,11 @@ private static String getTestClassText() { ps.println("import java.io.*;"); ps.println("import java.util.*;"); ps.println("import junit.framework.*;"); + ps.println("import org.junit.BeforeClass;"); ps.println("import org.junit.Test;"); + ps.println("import org.junit.runner.JUnitCore;"); + ps.println("import org.junit.runner.Result;"); + ps.println("import org.junit.runner.notification.Failure;"); ps.println("import org.plumelib.util.FilesPlume;"); ps.println("import org.plumelib.util.StringsPlume;"); ps.println("import org.plumelib.util.UtilPlume;"); @@ -229,11 +233,6 @@ private static String getTestClassText() { ps.println(" // java files it produces, changing the order that the setUpTests"); ps.println(" // commands are run will cause the tests to fail."); ps.println(); - ps.println(" /** Do not instantiate. */"); - ps.println(" private SplitterFactoryTest() {"); - ps.println(" throw new Error(\"Do not instantiate\");"); - ps.println(" }"); - ps.println(); ps.println(" private static String targetDir = \"" + targetDir + "\";"); ps.println(); ps.println(" private static @Nullable String tempDir = null;"); @@ -242,31 +241,41 @@ private static String getTestClassText() { ps.println(); ps.println(" private static String usage ="); ps.println(" StringsPlume.joinLines("); - ps.println(" \"Usage: java daikon.tools.CreateSpinfo FILE.java ...\","); + ps.println(" \"Usage: java daikon.test.split.SplitterFactoryTest [OPTION]...\","); ps.println( - " \" -s Save (do not delete) the splitter java files in the temp directory\","); - ps.println(" \" -h Display this usage message\");"); + " \" -s Save (do not delete) the splitter java files in the temp" + + " directory\","); + ps.println(" \" -h, --help Display this usage message\");"); ps.println(); ps.println(" public static void main(String[] args) {"); - ps.println( - " Getopt g = new Getopt(\"daikon.test.split.SplitterFactoryTest\", args, \"hs\");"); + ps.println(" try {"); + ps.println(" mainHelper(args);"); + ps.println(" } catch (Daikon.DaikonTerminationException e) {"); + ps.println(" Daikon.handleDaikonTerminationException(e);"); + ps.println(" }"); + ps.println(" }"); + ps.println(); + ps.println(" public static void mainHelper(String[] args) {"); + ps.println(" DaikonGetopt g = new DaikonGetopt(args, \"s\", new LongOpt[0], usage);"); ps.println(" int c;"); ps.println(" while ((c = g.getopt()) != -1) {"); ps.println(" switch (c) {"); ps.println(" case 's':"); ps.println(" saveFiles = true;"); ps.println(" break;"); - ps.println(" case 'h':"); - ps.println(" System.out.println(usage);"); - ps.println(" System.exit(1);"); - ps.println(" break;"); - ps.println(" case '?':"); - ps.println(" break;"); ps.println(" default:"); - ps.println(" System.out.println(\"getopt() returned \" + c);"); - ps.println(" break;"); + ps.println(" throw new Daikon.BugInDaikon(\"getopt() returned \" + c);"); ps.println(" }"); ps.println(" }"); + ps.println(" Result result = JUnitCore.runClasses(SplitterFactoryTest.class);"); + ps.println(" for (Failure failure : result.getFailures()) {"); + ps.println(" System.out.println(failure.getTrace());"); + ps.println(" }"); + ps.println(" if (!result.wasSuccessful()) {"); + ps.println( + " throw new Daikon.UserError(StringsPlume.nPlural(result.getFailureCount(), \"test\")" + + " + \" failed\");"); + ps.println(" }"); ps.println(" }"); ps.println(); @@ -320,7 +329,8 @@ private static String getTestClassText() { * SplitterFactoryTest to set up the needed files to run the tests on. */ public static void appendSetUpTest(PrintStream ps) { - ps.println(" private static void setUpTests() {"); + ps.println(" @BeforeClass"); + ps.println(" public static void setUpTests() {"); ps.println(" List spinfoFiles;"); ps.println(" List declsFiles;"); for (int i = 0; i < spinfoFileLists.size(); i++) { @@ -350,7 +360,7 @@ private static void appendTests(PrintStream ps) { ps.println(); for (String className : classNames) { ps.println(" @Test"); - ps.println(" public static void test" + className + "() {"); + ps.println(" public void test" + className + "() {"); ps.println(" assertEqualFiles(\"" + className + ".java\");"); ps.println(" }"); ps.println(); diff --git a/java/daikon/tools/DtraceDiff.java b/java/daikon/tools/DtraceDiff.java index 7d158ccc29..e1c7bbce3f 100644 --- a/java/daikon/tools/DtraceDiff.java +++ b/java/daikon/tools/DtraceDiff.java @@ -6,6 +6,7 @@ import static daikon.tools.nullness.NullnessUtil.*; import daikon.Daikon; +import daikon.DaikonGetopt; import daikon.FileIO; import daikon.Global; import daikon.PptMap; @@ -124,7 +125,7 @@ public static void mainHelper(final String[] args) { new LongOpt(Daikon.config_option_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), }; - Getopt g = new Getopt("daikon.tools.DtraceDiff", args, "h:", longopts); + DaikonGetopt g = new DaikonGetopt(args, "", longopts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { @@ -132,10 +133,7 @@ public static void mainHelper(final String[] args) { // long option case 0: String option_name = longopts[g.getLongind()].getName(); - if (Daikon.help_SWITCH.equals(option_name)) { - System.out.println(usage); - throw new Daikon.NormalTermination(); - } else if (Daikon.ppt_regexp_SWITCH.equals(option_name)) { + if (Daikon.ppt_regexp_SWITCH.equals(option_name)) { if (Daikon.ppt_regexp != null) { throw new Error( "multiple --" @@ -224,20 +222,12 @@ public static void mainHelper(final String[] args) { Configuration.getInstance().apply(item); break; } else { - throw new RuntimeException("Unknown long option received: " + option_name); + throw new Daikon.BugInDaikon("Unhandled long option " + option_name); } // short options - case 'h': - System.out.println(usage); - throw new Daikon.NormalTermination(); - - case '?': - break; // getopt() already printed an error - default: - System.out.println("getopt() returned " + c); - break; + throw new Daikon.BugInDaikon("getopt() returned " + c); } } diff --git a/java/daikon/tools/ExtractConsequent.java b/java/daikon/tools/ExtractConsequent.java index f2ecf3caf3..d4f94d9f49 100644 --- a/java/daikon/tools/ExtractConsequent.java +++ b/java/daikon/tools/ExtractConsequent.java @@ -5,6 +5,7 @@ import static java.util.logging.Level.INFO; import daikon.Daikon; +import daikon.DaikonGetopt; import daikon.FileIO; import daikon.Global; import daikon.Ppt; @@ -118,17 +119,14 @@ public static void mainHelper(final String[] args) throws IOException { new LongOpt(Daikon.debugAll_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.debug_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), }; - Getopt g = new Getopt("daikon.ExtractConsequent", args, "h", longopts); + DaikonGetopt g = new DaikonGetopt(args, "", longopts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { case 0: // got a long option String option_name = longopts[g.getLongind()].getName(); - if (Daikon.help_SWITCH.equals(option_name)) { - System.out.println(usage); - throw new Daikon.NormalTermination(); - } else if (Daikon.suppress_redundant_SWITCH.equals(option_name)) { + if (Daikon.suppress_redundant_SWITCH.equals(option_name)) { Daikon.suppress_redundant_invariants_with_simplify = true; } else if (Daikon.config_option_SWITCH.equals(option_name)) { String item = Daikon.getOptarg(g); @@ -139,17 +137,11 @@ public static void mainHelper(final String[] args) throws IOException { } else if (Daikon.debug_SWITCH.equals(option_name)) { daikon.LogHelper.setLevel(Daikon.getOptarg(g), FINE); } else { - throw new RuntimeException("Unknown long option received: " + option_name); + throw new Daikon.BugInDaikon("Unhandled long option " + option_name); } break; - case 'h': - System.out.println(usage); - throw new Daikon.NormalTermination(); - case '?': - break; // getopt() already printed an error default: - System.out.println("getopt() returned " + c); - break; + throw new Daikon.BugInDaikon("getopt() returned " + c); } } // The index of the first non-option argument -- the name of the file diff --git a/java/daikon/tools/InvariantChecker.java b/java/daikon/tools/InvariantChecker.java index 70a3c804d1..3f8c7c56b7 100644 --- a/java/daikon/tools/InvariantChecker.java +++ b/java/daikon/tools/InvariantChecker.java @@ -4,6 +4,7 @@ import static java.util.logging.Level.INFO; import daikon.Daikon; +import daikon.DaikonGetopt; import daikon.Debug; import daikon.FileIO; import daikon.Global; @@ -135,20 +136,16 @@ public static void mainHelper(final String[] args) new LongOpt(verbose_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.debugAll_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.debug_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), - new LongOpt(Daikon.ppt_regexp_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(Daikon.track_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), }; - Getopt g = new Getopt("daikon.tools.InvariantChecker", args, "h", longopts); + DaikonGetopt g = new DaikonGetopt(args, "", longopts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { case 0: // got a long option String option_name = longopts[g.getLongind()].getName(); - if (Daikon.help_SWITCH.equals(option_name)) { - System.out.println(usage); - throw new Daikon.NormalTermination(); - } else if (conf_SWITCH.equals(option_name)) { + if (conf_SWITCH.equals(option_name)) { doConf = true; } else if (filter_SWITCH.equals(option_name)) { doFilter = true; @@ -179,17 +176,11 @@ public static void mainHelper(final String[] args) "Error parsing track argument '" + Daikon.getOptarg(g) + "' - " + error); } } else { - throw new RuntimeException("Unknown long option received: " + option_name); + throw new Daikon.BugInDaikon("Unhandled long option " + option_name); } break; - case 'h': - System.out.println(usage); - throw new Daikon.NormalTermination(); - case '?': - break; // getopt() already printed an error default: - System.out.println("getopt() returned " + c); - break; + throw new Daikon.BugInDaikon("getopt() returned " + c); } } diff --git a/java/daikon/tools/compare/LogicalCompare.java b/java/daikon/tools/compare/LogicalCompare.java index bbbc6df993..371d26ea25 100644 --- a/java/daikon/tools/compare/LogicalCompare.java +++ b/java/daikon/tools/compare/LogicalCompare.java @@ -5,6 +5,7 @@ import static java.util.logging.Level.INFO; import daikon.Daikon; +import daikon.DaikonGetopt; import daikon.FileIO; import daikon.Global; import daikon.PptMap; @@ -658,7 +659,6 @@ public static void mainHelper(final String[] args) new LongOpt(Daikon.debugAll_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.debug_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt("filters", LongOpt.REQUIRED_ARGUMENT, null, 0), - new LongOpt("help", LongOpt.NO_ARGUMENT, null, 0), new LongOpt("minimize-classes", LongOpt.NO_ARGUMENT, null, 0), new LongOpt("no-post-after-pre-failure", LongOpt.NO_ARGUMENT, null, 0), new LongOpt("post-after-pre-failure", LongOpt.NO_ARGUMENT, null, 0), @@ -675,7 +675,7 @@ public static void mainHelper(final String[] args) extra_assumptions = new LinkedHashMap<>(); - Getopt g = new Getopt("daikon.tools.compare.LogicalCompare", args, "h", longopts); + DaikonGetopt g = new DaikonGetopt(args, "", longopts, usage); int c; boolean user_filters = false; while ((c = g.getopt()) != -1) { @@ -683,10 +683,7 @@ public static void mainHelper(final String[] args) case 0: // got a long option String option_name = longopts[g.getLongind()].getName(); - if (Daikon.help_SWITCH.equals(option_name)) { - System.out.println(usage); - throw new Daikon.NormalTermination(); - } else if (option_name.equals("config-file")) { + if (option_name.equals("config-file")) { String config_file = Daikon.getOptarg(g); try (InputStream stream = new FileInputStream(config_file)) { Configuration.getInstance().apply(stream); @@ -727,15 +724,11 @@ public static void mainHelper(final String[] args) } else if (option_name.equals("minimize-classes")) { opt_minimize_classes = true; } else { - throw new Error(); + throw new Daikon.BugInDaikon("Unhandled long option " + option_name); } break; - case 'h': - System.out.println(usage); - throw new Daikon.NormalTermination(); - case '?': - // getopt() already printed an error - throw new Daikon.UserError("Bad argument"); + default: + throw new Daikon.BugInDaikon("getopt() returned " + c); } } diff --git a/java/daikon/tools/jtb/Annotate.java b/java/daikon/tools/jtb/Annotate.java index 9635cc5912..cee7292928 100644 --- a/java/daikon/tools/jtb/Annotate.java +++ b/java/daikon/tools/jtb/Annotate.java @@ -24,8 +24,7 @@ * *

The first argument is a Daikon {@code .inv} file -- a serialized file of Invariant objects. * All subsequent arguments are {@code Foo.java} files that are rewritten into {@code - * Foo.java-jmlannotated} versions; alternately, use the {@code -r} flag to process every {@code - * .java} file under the current directory. + * Foo.java-jmlannotated} versions. */ public class Annotate { @@ -88,10 +87,9 @@ private Annotate() { private static String usage = StringsPlume.joinLines( "Usage: java daikon.tools.Annotate FILE.inv FILE.java ...", - " -h Display this usage message", + " -h, --help Display this usage message", " -i Insert invariants not supported by ESC with \"!\" instead of \"@\";", " by default these \"inexpressible\" invariants are simply omitted", - " -r Use all .java files under the current directory as arguments", " -s Use // comments rather than /* comments", " --format name Insert specifications in the given format: DBC, ESC, JML, Java", " --wrap_xml Wrap each annotation and auxiliary information in XML tags", @@ -132,7 +130,6 @@ public static void mainHelper(final String[] args) throws Exception { Daikon.output_format = OutputFormat.ESCJAVA; LongOpt[] longopts = new LongOpt[] { - new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.debugAll_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.debug_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(Daikon.format_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), @@ -140,7 +137,7 @@ public static void mainHelper(final String[] args) throws Exception { new LongOpt(max_invariants_pp_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(no_reflection_SWITCH, LongOpt.NO_ARGUMENT, null, 0) }; - Getopt g = new Getopt("daikon.tools.jtb.Annotate", args, "hs", longopts); + DaikonGetopt g = new DaikonGetopt(args, "is", longopts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { @@ -148,10 +145,7 @@ public static void mainHelper(final String[] args) throws Exception { // got a long option String option_name = longopts[g.getLongind()].getName(); - if (Daikon.help_SWITCH.equals(option_name)) { - System.out.println(usage); - throw new Daikon.NormalTermination(); - } else if (no_reflection_SWITCH.equals(option_name)) { + if (no_reflection_SWITCH.equals(option_name)) { useReflection = false; } else if (max_invariants_pp_SWITCH.equals(option_name)) { try { @@ -181,12 +175,9 @@ public static void mainHelper(final String[] args) throws Exception { setLightweight = true; } } else { - throw new Daikon.UserError("Unknown long option received: " + option_name); + throw new Daikon.BugInDaikon("Unhandled long option " + option_name); } break; - case 'h': - System.out.println(usage); - throw new Daikon.NormalTermination(); case 'i': insert_inexpressible = true; break; @@ -200,11 +191,8 @@ public static void mainHelper(final String[] args) throws Exception { case 's': slashslash = true; break; - case '?': - break; // getopt() already printed an error default: - System.out.println("getopt() returned " + c); - break; + throw new Daikon.BugInDaikon("getopt() returned " + c); } } diff --git a/java/daikon/tools/jtb/CreateSpinfo.java b/java/daikon/tools/jtb/CreateSpinfo.java index 56f1b154e2..76d4b23b05 100644 --- a/java/daikon/tools/jtb/CreateSpinfo.java +++ b/java/daikon/tools/jtb/CreateSpinfo.java @@ -83,40 +83,30 @@ public static void mainHelper(final String[] args) throws IOException { daikon.LogHelper.setupLogs(INFO); LongOpt[] longopts = new LongOpt[] { - new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.debugAll_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(Daikon.debug_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), }; - Getopt g = new Getopt("daikon.tools.jtb.CreateSpinfo", args, "ho:", longopts); + DaikonGetopt g = new DaikonGetopt(args, "o:", longopts, usage); int c; while ((c = g.getopt()) != -1) { switch (c) { case 0: // got a long option String option_name = longopts[g.getLongind()].getName(); - if (Daikon.help_SWITCH.equals(option_name)) { - System.out.println(usage); - throw new Daikon.NormalTermination(); - } else if (Daikon.debugAll_SWITCH.equals(option_name)) { + if (Daikon.debugAll_SWITCH.equals(option_name)) { Global.debugAll = true; } else if (Daikon.debug_SWITCH.equals(option_name)) { daikon.LogHelper.setLevel(Daikon.getOptarg(g), FINE); } else { - throw new RuntimeException("Unknown long option received: " + option_name); + throw new Daikon.BugInDaikon("Unhandled long option " + option_name); } break; case 'o': outputfilename = Daikon.getOptarg(g); break; - case 'h': - System.out.println(usage); - throw new Daikon.NormalTermination(); - case '?': - break; // getopt() already printed an error default: - System.out.println("getopt() returned " + c); - break; + throw new Daikon.BugInDaikon("getopt() returned " + c); } } diff --git a/tests/diff-tests/Makefile.common b/tests/diff-tests/Makefile.common index 7a22461dfd..014605b8b4 100644 --- a/tests/diff-tests/Makefile.common +++ b/tests/diff-tests/Makefile.common @@ -20,10 +20,10 @@ INV_FILES := $(PROGRAM)-1.inv.gz $(PROGRAM)-2.inv.gz # An output file must satisfy the pattern program%.txt, where the % is # the options passed to Diff ## Must reinstate -a and -ap and -p in V3 as soon as possible. -MDE 6/10/2003 -# OUTPUT := $(PROGRAM).txt $(PROGRAM)-u.txt $(PROGRAM)-a.txt $(PROGRAM)-p.txt \ +# OUTPUT := $(PROGRAM).txt $(PROGRAM)-a.txt $(PROGRAM)-p.txt \ # $(PROGRAM)-e.txt $(PROGRAM)-ap.txt $(PROGRAM)-t.txt \ # $(PROGRAM)-jt.txt -OUTPUT := $(PROGRAM).txt $(PROGRAM)-u.txt \ +OUTPUT := $(PROGRAM).txt \ $(PROGRAM)-e.txt $(PROGRAM)-t.txt \ $(PROGRAM)-jt.txt # Must be handled differently than OUTPUT, because map files are diff --git a/tests/diff-tests/StackAr/StackAr-u.txt.goal b/tests/diff-tests/StackAr/StackAr-u.txt.goal deleted file mode 100644 index f3164de375..0000000000 --- a/tests/diff-tests/StackAr/StackAr-u.txt.goal +++ /dev/null @@ -1,105 +0,0 @@ -(I)V:::ENTER> - = 0 {0+}, capacity >= 2 {1-}> (UInt,DJU) - (U!Int,DUU) -(I)V:::EXIT> - (Bin,MJ) - (Bin,MJ) - = -1 {0.8136-}, this.topOfStack >= -1 {0.8333-}> (U!Int,SUU) - (UInt,SJU) - (U!Int,DUU) - (U!Int,SUU) - (UInt,SJU) - (UInt,MJ) - - = -1 {0+}, this.topOfStack >= -1 {0+}> (U!Int,SJJ) - (U!Int,DJU) - (U!Int,DUU) - (UInt,MJ) - - = -1 {0+}, this.topOfStack >= -1 {0+}> (U!Int,SJJ) - (U!Int,DJU) - (U!Int,DJU) - (U!Int,DUU) - (UInt,MJ) - - (Bin,MJ) - = -1 {0+}, this.topOfStack >= -1 {0+}> (U!Int,SJJ) - (U!Int,DJJ) - (U!Int,DUU) - - (Bin,MJ) - = -1 {0+}, this.topOfStack >= -1 {0+}> (U!Int,SJJ) - (UInt,MJ) - (U!Int,DJJ) - (U!Int,DJJ) - (U!Int,DUU) - - (Bin,JM) - = -1 {0+}, this.topOfStack >= 9223372036854775807 {1-}> (U!Int,DJU) - (U!Int,UM) - (U!Int,DUU) - (UInt,JM) - (UInt,JM) - (UInt,DJU) - - (Bin,JM) - (Bin,JM) - (Bin,JM) - (Bin,JM) - (Bin,JM) - = -1 {0+}, null> (U!Int,JM) - = -1 {0.8205-}, this.topOfStack >= 9223372036854775807 {1-}> (U!Int,DUU) - (UInt,DJU) - (U!Int,UM) - (U!Int,UM) - (U!Int,UM) - (U!Int,DUU) - (UInt,DJU) - (UInt,JM) - (UInt,JM) - (UInt,DJU) - (UInt,DJU) - - = 9223372036854775807 {1-}, this.topOfStack >= 9223372036854775807 {1-}> (U!Int,SUU) - (U!Int,SUU) - - = 9223372036854775807 {1-}, this.topOfStack >= 9223372036854775807 {1-}> (U!Int,SUU) - (U!Int,SUU) - - = -1 {0+}, this.topOfStack >= -1 {0+}> (U!Int,SJJ) - (U!Int,DJJ) - (U!Int,DUU) - - = -1 {0+}, orig(this.topOfStack) >= -1 {0+}> (U!Int,SJJ) - (U!Int,DUU) - (U!Int,DJJ) - (U!Int,DJJ) - (U!Int,DUU) - - = -1 {0+}, this.topOfStack >= 0 {0+}> (UInt,DJJ) - (U!Int,DJJ) - (U!Int,DUU) - - (Bin,MJ) - (Bin,MJ) - (Bin,MJ) - (Bin,MJ) - = -1 {0+}, this.topOfStack >= 0 {0+}> (UInt,DJJ) - (UInt,MJ) - (U!Int,DJJ) - (U!Int,DJJ) - (U!Int,DUU) - - = -1 {0+}, this.topOfStack >= -1 {0+}> (U!Int,SJJ) - (U!Int,DJU) - (U!Int,DUU) - - (Bin,MJ) - (Bin,MJ) - (Bin,MJ) - = -1 {0+}, orig(this.topOfStack) >= -1 {0+}> (U!Int,SJJ) - = -1 {0+}, this.topOfStack >= -1 {0+}> (U!Int,SJJ) - (U!Int,DUU) - (U!Int,DJU) - (U!Int,DJU) - (U!Int,DUU) diff --git a/tests/diff-tests/schedule2/schedule2-u.txt.goal b/tests/diff-tests/schedule2/schedule2-u.txt.goal deleted file mode 100644 index ef71a1c0d9..0000000000 --- a/tests/diff-tests/schedule2/schedule2-u.txt.goal +++ /dev/null @@ -1,77 +0,0 @@ -getopt() returned 117 - - <::next_pid >= 0 {0-}, ::next_pid >= 0 {1+}> (Una,SUJ) - - <::next_pid >= 0 {0-}, ::next_pid >= 0 {1+}> (Una,SUJ) - - = 0 {1+}, prio >= -1 {1+}> (Una,DJJ) - - (return is a pointer) {1+}> (Nul,MJ) - (prio >= -1) {1+}> (Nul,MJ) - (prio >= 0) {1+}> (Nul,MJ) - (prio != 0) {0.9999+}> (Nul,MJ) - = 0 {1+}, prio >= -1 {1+}> (Una,DJJ) - (Una,DJJ) - - <*prio >= -1 {1+}, *prio >= -2 {0-}> (Una,DJU) - <*command != 0 {1+}, null> (Una,JM) - - (*prio >= -1) {1+}> (Nul,MJ) - (orig(*prio) >= -1) {1+}> (Nul,MJ) - <(return == 1) ==> (*prio != 0) {1+}, null> (Nul,JM) - (*command == 0) {1+}> (Bin,MJ) - <(*prio == 0) ==> (orig(*prio) == 0) {1+}, null> (Bin,JM) - <*prio >= -1 {1+}, *prio >= -2 {0-}> (Una,DJU) - <*command != 0 {1+}, null> (Una,JM) - - <::next_pid >= 0 {0.4444-}, ::next_pid >= 0 {1+}> (Una,SUJ) - - <::next_pid >= 0 {0.4444-}, ::next_pid >= 0 {1+}> (Una,SUJ) - - = 0.0 {1+}, ratio >= -1.0 {0.0721-}> (Una,DJU) - - = 0.0 {1+}, ratio >= -1.0 {0.0721-}> (Una,DJU) - (Una,DJJ) - - (Una,JM) - - = orig(::next_pid) {1+}> (Bin,MJ) - <::next_pid - orig(::next_pid) - 1 == 0 {1+}, null> (Bin,JM) - (orig(::next_pid) == 0) {1+}> (Bin,MJ) - <::next_pid >= 1 {1+}, ::next_pid >= 0 {0.3333-}> (Una,DJU) - (Una,DUJ) - (Una,JM) - (Una,DJJ) - - = 0 {1+}, prio >= -1 {1+}> (Una,DJJ) - - (return is a pointer) {1+}> (Nul,MJ) - (prio >= -1) {1+}> (Nul,MJ) - (prio >= 0) {1+}> (Nul,MJ) - (prio != 0) {0.9999+}> (Nul,MJ) - = 0 {1+}, prio >= -1 {1+}> (Una,DJJ) - (Una,DJJ) - - = -1 {1+}, prio >= -2 {0-}> (Una,DJU) - (Una,JM) - (Una,JM) - - = -1 {1+}, prio >= -2 {0-}> (Una,DJU) - (Una,JM) - (Una,JM) - (Una,JM) - (Una,JM) - - (Una,SUJ) - - (Una,SUJ) - (Una,DJJ) - - <::next_pid >= 0 {1+}, ::next_pid >= 0 {0.7779-}> (Una,SJU) - (Una,SUJ) - (Una,JM) - - <::next_pid >= 0 {1+}, ::next_pid >= 0 {0.7779-}> (Una,SJU) - (Una,SUJ) - (Una,JM) - (Una,DJJ) From 235621312353dda3e417d469dc7357521d68274f Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Tue, 6 Oct 2026 17:07:04 -0700 Subject: [PATCH 02/30] TraceSelect: reject bad arguments, accept -h and --help, reset state per call Report a UserError for missing or non-integer num_reps and sample_size and for a malformed -SEED. Pass every argument that is not a TraceSelect option to Daikon rather than silently dropping some. mainHelper resets its static state on each call. Co-Authored-By: Claude Opus 5.5 --- java/daikon/test/BadOptionTest.java | 3 + java/daikon/tools/TraceSelect.java | 107 +++++++++++++++------------- 2 files changed, 59 insertions(+), 51 deletions(-) diff --git a/java/daikon/test/BadOptionTest.java b/java/daikon/test/BadOptionTest.java index af9b745d24..c2e93fb169 100644 --- a/java/daikon/test/BadOptionTest.java +++ b/java/daikon/test/BadOptionTest.java @@ -11,6 +11,7 @@ import daikon.PrintInvariants; import daikon.UnionInvariants; import daikon.tools.DtraceDiff; +import daikon.tools.TraceSelect; import gnu.getopt.LongOpt; import java.io.ByteArrayOutputStream; import java.io.PrintStream; @@ -158,6 +159,8 @@ public void testPrintInvariantsUnrecognizedOption() { 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"})); } /** diff --git a/java/daikon/tools/TraceSelect.java b/java/daikon/tools/TraceSelect.java index 22f181956b..7bf9c29bd3 100644 --- a/java/daikon/tools/TraceSelect.java +++ b/java/daikon/tools/TraceSelect.java @@ -1,6 +1,7 @@ // TraceSelect.java package daikon.tools; +import daikon.DaikonGetopt; import java.io.File; import java.io.IOException; import java.io.PrintWriter; @@ -15,7 +16,6 @@ import java.util.Random; import java.util.StringTokenizer; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -import org.checkerframework.checker.nullness.qual.RequiresNonNull; import org.checkerframework.dataflow.qual.Pure; import org.plumelib.util.FilesPlume; import org.plumelib.util.MultiRandSelector; @@ -40,10 +40,6 @@ private TraceSelect() { 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; @@ -52,7 +48,8 @@ private TraceSelect() { // always set to non-null by mainHelper private static @MonotonicNonNull Random randObj; - private static int daikonArgStart = 0; + /** The arguments to pass to Daikon, other than the .dtrace file. */ + private static List daikonArgs = new ArrayList<>(); // This allows us to simply call MultiDiff // with the same files we just created. @@ -63,7 +60,9 @@ private TraceSelect() { 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" + "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.", + "Example: java TraceSelect 20 10 -NOCLEAN -INCLUDE_UNRETURNED -SEED 1000 foo.dtrace" + " foo2.dtrace foo.decls RatPoly.decls foo3.dtrace"); /** @@ -85,14 +84,28 @@ 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. + args = DaikonGetopt.argsAfterLeadingOptions(args, usage); + // Reset the state from any previous call. + CLEAN = true; + INCLUDE_UNRETURNED = false; + DO_DIFFS = false; + daikonArgs = new ArrayList<>(); + String inputFile = null; + Random rand = null; + + if (args.length < 2) { + throw new daikon.Daikon.UserError("Too few arguments." + daikon.Daikon.lineSep + usage); } - num_reps = Integer.parseInt(args[0]); - numPerSample = Integer.parseInt(args[1]); + try { + num_reps = Integer.parseInt(args[0]); + numPerSample = Integer.parseInt(args[1]); + } catch (NumberFormatException e) { + throw new daikon.Daikon.UserError( + "num_reps and sample_size must be integers." + daikon.Daikon.lineSep + usage); + } // process optional switches // also deduce index of arg for Daikon @@ -101,17 +114,20 @@ public static void mainHelper(final String[] args) { // allows seed setting if (args[i].toUpperCase(Locale.ENGLISH).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"); + } + 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); } - 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 @@ -119,7 +135,6 @@ else if (args[i].toUpperCase(Locale.ENGLISH).equals("-NOCLEAN")) { // 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 @@ -129,48 +144,45 @@ else if (args[i].toUpperCase(Locale.ENGLISH).equals("-INCLUDE_UNRETURNED")) { // samples. else if (args[i].toUpperCase(Locale.ENGLISH).equals("-DO_DIFFS")) { DO_DIFFS = true; - daikonArgStart = i + 1; } - // 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. + // The Daikon arguments start with the first argument that is not a TraceSelect switch, + // such as a .dtrace or .decls file, or "--nohierarchy" or "--format java". + + // -h and --help before the Daikon arguments print the usage message. + else if (!knowArgStart + && (args[i].equals("-h") || args[i].equals("--" + daikon.Daikon.help_SWITCH))) { + System.out.println(usage); + throw new daikon.Daikon.NormalTermination(); + } // For now, only the first dtrace file will be sampled else if (args[i].endsWith(".dtrace")) { - if (fileName == null) { - fileName = args[i]; + if (inputFile == null) { + inputFile = args[i]; } else { throw new daikon.Daikon.UserError("Only 1 dtrace file for input allowed"); } - if (!knowArgStart) { - daikonArgStart = i; - knowArgStart = true; - } - } else if (args[i].endsWith(".decls")) { - if (!knowArgStart) { - daikonArgStart = i; - knowArgStart = true; - } + knowArgStart = true; + } else { + knowArgStart = true; + daikonArgs.add(args[i]); } } // if no seed provided, use default Random() constructor - if (randObj == null) { - randObj = new Random(); - } + randObj = (rand != null) ? rand : new Random(); 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\")"); } + fileName = inputFile; try { @@ -267,7 +279,6 @@ private static void deleteQuietly(String fileName) { } } - @RequiresNonNull("argles") private static void invokeDaikon(String dtraceName) throws IOException { System.out.println("Created file: " + dtraceName); @@ -278,14 +289,8 @@ private static void invokeDaikon(String dtraceName) throws IOException { 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]); - } + // all the Daikon args except for the original single dtrace file + daikonArgsList.addAll(daikonArgs); // create an array to store the Strings in daikonArgsList String[] daikonArgs = daikonArgsList.toArray(new String[0]); From 6380a6a04889ac022cda575bf663d013acd32109 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Tue, 6 Oct 2026 18:06:28 -0700 Subject: [PATCH 03/30] Remove stale comment in DtraceDiff --- java/daikon/tools/DtraceDiff.java | 1 - 1 file changed, 1 deletion(-) diff --git a/java/daikon/tools/DtraceDiff.java b/java/daikon/tools/DtraceDiff.java index e1c7bbce3f..0fe001621e 100644 --- a/java/daikon/tools/DtraceDiff.java +++ b/java/daikon/tools/DtraceDiff.java @@ -225,7 +225,6 @@ public static void mainHelper(final String[] args) { throw new Daikon.BugInDaikon("Unhandled long option " + option_name); } - // short options default: throw new Daikon.BugInDaikon("getopt() returned " + c); } From 23bf08dae7ae957058512bd2f82270a41ddaf5ad Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Tue, 6 Oct 2026 18:51:13 -0700 Subject: [PATCH 04/30] Address code review: reject "- " with no short options, report instrumenter argument errors on stderr, simplify ambiguity check --- java/daikon/DaikonGetopt.java | 136 ++++-------------- java/daikon/test/BadOptionTest.java | 93 +++++------- .../runtimechecker/InstrumentHandler.java | 73 +++++----- 3 files changed, 101 insertions(+), 201 deletions(-) diff --git a/java/daikon/DaikonGetopt.java b/java/daikon/DaikonGetopt.java index 0f71e31406..a2a196f920 100644 --- a/java/daikon/DaikonGetopt.java +++ b/java/daikon/DaikonGetopt.java @@ -1,9 +1,9 @@ package daikon; +import static daikon.tools.nullness.NullnessUtil.castNonNull; + import gnu.getopt.Getopt; import gnu.getopt.LongOpt; -import java.util.Arrays; -import java.util.function.Supplier; import org.checkerframework.checker.nullness.qual.Nullable; import org.plumelib.util.ArraysPlume; @@ -22,23 +22,22 @@ public class DaikonGetopt extends Getopt { private static final String PROGNAME = "DaikonGetopt"; /** - * Computes the usage message printed for {@code -h} and {@code --help}, or null if the caller - * handles them. + * The usage message printed for {@code -h} and {@code --help}, or null if the caller handles + * them. */ - private final @Nullable Supplier usage; - - /** Text appended to the description of a bad command-line option, or null to append nothing. */ - private @Nullable String usageHint = "run with -h for usage"; + private final @Nullable String usage; /** - * Creates a command-line option processor that recognizes short and long options. + * Creates a command-line option processor that recognizes short and long options. Its description + * of a bad option does not suggest running with {@code -h}, because it does not handle {@code + * -h}. * * @param argv the command-line arguments * @param optstring the short options, in the format of {@link Getopt} * @param longopts the long options */ public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts) { - super(PROGNAME, argv, optstring, longopts); + super(PROGNAME, argv, nonEmpty(optstring), longopts); this.usage = null; opterr = false; } @@ -53,23 +52,22 @@ public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts) { * @param usage the usage message to print for {@code -h} and {@code --help} */ public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts, String usage) { - this(argv, optstring, longopts, () -> usage); + super(PROGNAME, argv, withH(optstring), withHelp(longopts)); + this.usage = usage; + opterr = false; } /** - * Creates a command-line option processor that recognizes short and long options, plus {@code -h} - * and {@code --help}, which print the usage message and throw {@link Daikon.NormalTermination}. - * The usage message is computed only if it is needed. + * Returns short options that are equivalent to the given ones but are not empty. Getopt replaces + * an empty optstring by " ", which makes "- " a valid option. * - * @param argv the command-line arguments - * @param optstring the short options, in the format of {@link Getopt}; must not contain {@code h} - * @param longopts the long options; must not contain {@code help} - * @param usage computes the usage message to print for {@code -h} and {@code --help} + * @param optstring short options, in the format of {@link Getopt} + * @return {@code optstring}, or ":" if {@code optstring} is empty */ - public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts, Supplier usage) { - super(PROGNAME, argv, withH(optstring), withHelp(longopts)); - this.usage = usage; - opterr = false; + private static String nonEmpty(String optstring) { + // A leading ':' only makes Getopt return ':' rather than '?' for a missing argument, which + // getopt() treats identically. + return optstring.isEmpty() ? ":" : optstring; } /** @@ -101,87 +99,6 @@ private static LongOpt[] withHelp(LongOpt[] longopts) { longopts, new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 'h')); } - /** - * Processes the command-line arguments of a program that takes no options other than {@code -h} - * and {@code --help}. - * - * @param argv the command-line arguments - * @param usage the usage message to print for {@code -h} and {@code --help} - * @return the arguments that are not options - * @throws Daikon.NormalTermination after printing the usage message, if an argument requests it - * @throws Daikon.UserError if an argument is any other option - */ - public static String[] nonOptionArgs(String[] argv, String usage) { - return argsAfterOptions(argv, "", () -> usage); - } - - /** - * Processes the leading command-line arguments of a program that takes no options other than - * {@code -h} and {@code --help}. Processing stops at the first argument that is not an option, so - * later arguments may be options for some other program. - * - * @param argv the command-line arguments - * @param usage the usage message to print for {@code -h} and {@code --help} - * @return the arguments from the first one that is not an option onward - * @throws Daikon.NormalTermination after printing the usage message, if a leading argument - * requests it - * @throws Daikon.UserError if a leading argument is any other option - */ - public static String[] argsAfterLeadingOptions(String[] argv, String usage) { - return argsAfterLeadingOptions(argv, () -> usage); - } - - /** - * Like {@link #argsAfterLeadingOptions(String[], String)}, but computes the usage message only if - * it is needed. - * - * @param argv the command-line arguments - * @param usage computes the usage message to print for {@code -h} and {@code --help} - * @return the arguments from the first one that is not an option onward - * @throws Daikon.NormalTermination after printing the usage message, if a leading argument - * requests it - * @throws Daikon.UserError if a leading argument is any other option - */ - public static String[] argsAfterLeadingOptions(String[] argv, Supplier usage) { - // A leading "+" tells Getopt to stop at the first argument that is not an option. - return argsAfterOptions(argv, "+", usage); - } - - /** - * Processes command-line arguments, permitting no options other than {@code -h} and {@code - * --help}. - * - * @param argv the command-line arguments - * @param optstring "" to process all the arguments, or "+" to stop at the first non-option - * @param usage computes the usage message to print for {@code -h} and {@code --help} - * @return the arguments that Getopt did not process as options - * @throws Daikon.NormalTermination after printing the usage message, if an argument requests it - * @throws Daikon.UserError if an argument is any other option - */ - private static String[] argsAfterOptions( - String[] argv, String optstring, Supplier usage) { - // Getopt permutes its argument array, so give it a copy to avoid modifying the caller's array. - String[] argvCopy = argv.clone(); - DaikonGetopt g = new DaikonGetopt(argvCopy, optstring, new LongOpt[0], usage); - // Every option other than -h and --help is bad, so getopt() returns only -1 or throws. - int c = g.getopt(); - if (c != -1) { - throw new Daikon.BugInDaikon("getopt() returned " + c); - } - return Arrays.copyOfRange(argvCopy, g.getOptind(), argvCopy.length); - } - - /** - * Sets the text that is appended to the description of a bad command-line option. The default - * tells the user to run the program with -h. - * - * @param usageHint text appended to the description of a bad command-line option, or null to - * append nothing - */ - public void setUsageHint(@Nullable String usageHint) { - this.usageHint = usageHint; - } - /** * Like {@link Getopt#getopt()}, but never returns {@code '?'} or {@code ':'}. Getopt returns * {@code ':'} for a missing argument if the optstring starts with {@code ':'}. If this was @@ -199,7 +116,7 @@ public int getopt() { throw badOptionError(); } if (c == 'h' && usage != null) { - System.out.println(usage.get()); + System.out.println(usage); throw new Daikon.NormalTermination(); } return c; @@ -213,8 +130,8 @@ public int getopt() { */ private Daikon.UserError badOptionError() { String message = badOptionMessage(); - if (usageHint != null) { - message += "; " + usageHint; + if (usage != null) { + message += "; run with -h for usage"; } return new Daikon.UserError(message); } @@ -250,14 +167,15 @@ private String badOptionMessage() { private String longOptionMessage(String arg) { int equalsPos = arg.indexOf('='); String name = (equalsPos == -1) ? arg.substring(2) : arg.substring(2, equalsPos); - LongOpt[] longopts = long_options; // Getopt treats an empty name, as in "--=foo", as a prefix of every long option. if (name.isEmpty()) { return "Unrecognized command-line option " + arg; } - if (longopts == null || longind == -1) { + if (longind == -1) { return "Unrecognized command-line option --" + name; } + // Every constructor passes long options to Getopt. + LongOpt[] longopts = castNonNull(long_options); LongOpt match = longopts[longind]; // For an inexact match, Getopt sets longind to the first long option that has the given // prefix. The match is ambiguous if a later long option also has the prefix. @@ -273,7 +191,7 @@ private String longOptionMessage(String arg) { } else if (equalsPos == -1 && match.getHasArg() == LongOpt.REQUIRED_ARGUMENT) { return "Command-line option --" + match.getName() + " requires an argument"; } else { - return "Bad command-line option " + arg; + throw new Daikon.BugInDaikon("Getopt rejected " + arg + " for no known reason"); } } } diff --git a/java/daikon/test/BadOptionTest.java b/java/daikon/test/BadOptionTest.java index af9b745d24..78f76e8159 100644 --- a/java/daikon/test/BadOptionTest.java +++ b/java/daikon/test/BadOptionTest.java @@ -1,7 +1,6 @@ package daikon.test; import static java.nio.charset.StandardCharsets.UTF_8; -import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertThrows; @@ -57,58 +56,58 @@ private static void consumeAll(DaikonGetopt g) { while (g.getopt() != -1) {} } - /** - * Returns the expected message for a bad option. - * - * @param description a description of the bad option - * @return the expected message for a bad option - */ - private static String expected(String description) { - return description + "; run with -h for usage"; - } - /** Tests an unrecognized long option. */ @Test public void testUnrecognizedLongOption() { - assertEquals(expected("Unrecognized command-line option --bogus"), parse("--bogus")); - assertEquals(expected("Unrecognized command-line option --bogus"), parse("--bogus=3")); - assertEquals(expected("Unrecognized command-line option --=3"), parse("--=3")); - assertEquals( - expected("Unrecognized command-line option --bogus"), parse("-h", "--bogus", "file")); + assertEquals("Unrecognized command-line option --bogus", parse("--bogus")); + assertEquals("Unrecognized command-line option --bogus", parse("--bogus=3")); + assertEquals("Unrecognized command-line option --=3", parse("--=3")); + assertEquals("Unrecognized command-line option --bogus", parse("-h", "--bogus", "file")); } /** Tests an unrecognized short option, alone and combined with a valid one. */ @Test public void testUnrecognizedShortOption() { - assertEquals(expected("Unrecognized command-line option -x"), parse("-x")); - assertEquals(expected("Unrecognized command-line option -x"), parse("-xh")); - assertEquals(expected("Unrecognized command-line option -x"), parse("-hx")); - assertEquals(expected("Unrecognized command-line option -:"), parse("-:")); + assertEquals("Unrecognized command-line option -x", parse("-x")); + assertEquals("Unrecognized command-line option -x", parse("-xh")); + assertEquals("Unrecognized command-line option -x", parse("-hx")); + assertEquals("Unrecognized command-line option -:", parse("-:")); + } + + /** Tests an unrecognized short option when there are no short options. */ + @Test + public void testNoShortOptions() { + for (String arg : new String[] {"- ", "-x", "-:"}) { + DaikonGetopt g = new DaikonGetopt(new String[] {arg}, "", longopts); + Daikon.UserError e = assertThrows(Daikon.UserError.class, g::getopt); + assertEquals("Unrecognized command-line option " + arg, String.valueOf(e.getMessage())); + } } /** Tests an ambiguous abbreviation of a long option. */ @Test public void testAmbiguousLongOption() { - assertEquals(expected("Ambiguous command-line option --conf"), parse("--conf=x")); - assertEquals(expected("Ambiguous command-line option --conf"), parse("--conf")); + assertEquals("Ambiguous command-line option --conf", parse("--conf=x")); + assertEquals("Ambiguous command-line option --conf", parse("--conf")); } - /** Tests that a usage hint of null appends nothing to the description of a bad option. */ + /** Tests that the description of a bad option suggests -h if DaikonGetopt handles -h. */ @Test - public void testNoUsageHint() { - DaikonGetopt g = new DaikonGetopt(new String[] {"--bogus"}, "h", longopts); - g.setUsageHint(null); + public void testUsageHint() { + DaikonGetopt g = new DaikonGetopt(new String[] {"--bogus"}, "o:", new LongOpt[0], "usage"); Daikon.UserError e = assertThrows(Daikon.UserError.class, g::getopt); - assertEquals("Unrecognized command-line option --bogus", String.valueOf(e.getMessage())); + assertEquals( + "Unrecognized command-line option --bogus; run with -h for usage", + String.valueOf(e.getMessage())); } /** Tests options that are missing their required argument. */ @Test public void testMissingArgument() { - assertEquals(expected("Command-line option --config requires an argument"), parse("--config")); - assertEquals(expected("Command-line option --debug requires an argument"), parse("--deb")); - assertEquals(expected("Command-line option -o requires an argument"), parse("-o")); - assertEquals(expected("Command-line option -o requires an argument"), parse("-ho")); + assertEquals("Command-line option --config requires an argument", parse("--config")); + assertEquals("Command-line option --debug requires an argument", parse("--deb")); + assertEquals("Command-line option -o requires an argument", parse("-o")); + assertEquals("Command-line option -o requires an argument", parse("-ho")); } /** Tests options that are missing their required argument, when the optstring starts with ':'. */ @@ -116,22 +115,18 @@ public void testMissingArgument() { public void testMissingArgumentLeadingColon() { DaikonGetopt g = new DaikonGetopt(new String[] {"-o"}, ":ho:", longopts); Daikon.UserError e = assertThrows(Daikon.UserError.class, g::getopt); - assertEquals( - expected("Command-line option -o requires an argument"), String.valueOf(e.getMessage())); + assertEquals("Command-line option -o requires an argument", String.valueOf(e.getMessage())); g = new DaikonGetopt(new String[] {"--config"}, ":ho:", longopts); e = assertThrows(Daikon.UserError.class, g::getopt); assertEquals( - expected("Command-line option --config requires an argument"), - String.valueOf(e.getMessage())); + "Command-line option --config requires an argument", String.valueOf(e.getMessage())); } /** Tests long options that are given an argument they do not take. */ @Test public void testUnexpectedArgument() { - assertEquals( - expected("Command-line option --help does not take an argument"), parse("--help=x")); - assertEquals( - expected("Command-line option --verbose does not take an argument"), parse("--verb=x")); + assertEquals("Command-line option --help does not take an argument", parse("--help=x")); + assertEquals("Command-line option --verbose does not take an argument", parse("--verb=x")); } /** @@ -142,7 +137,7 @@ public void testUnexpectedArgument() { */ private static void assertBadOption(String description, ThrowingRunnable mainCall) { Daikon.UserError e = assertThrows(Daikon.UserError.class, mainCall); - assertEquals(expected(description), String.valueOf(e.getMessage())); + assertEquals(description + "; run with -h for usage", String.valueOf(e.getMessage())); } /** Tests an unrecognized long option in a tool. */ @@ -178,22 +173,4 @@ private static void assertPrintsUsage(ThrowingRunnable code) { } assertNotEquals("", bytes.toString(UTF_8).trim()); } - - /** Tests {@link DaikonGetopt#nonOptionArgs}. */ - @Test - public void testNonOptionArgs() { - assertArrayEquals( - new String[] {"a.dtrace", "b.dtrace"}, - DaikonGetopt.nonOptionArgs(new String[] {"a.dtrace", "b.dtrace"}, "u")); - assertArrayEquals( - new String[] {"-a.dtrace"}, - DaikonGetopt.nonOptionArgs(new String[] {"--", "-a.dtrace"}, "u")); - assertBadOption( - "Unrecognized command-line option --bogus", - () -> DaikonGetopt.nonOptionArgs(new String[] {"x", "--bogus"}, "u")); - assertBadOption( - "Unrecognized command-line option -x", - () -> DaikonGetopt.nonOptionArgs(new String[] {"-x"}, "u")); - assertPrintsUsage(() -> DaikonGetopt.nonOptionArgs(new String[] {"x", "--help"}, "u")); - } } diff --git a/java/daikon/tools/runtimechecker/InstrumentHandler.java b/java/daikon/tools/runtimechecker/InstrumentHandler.java index ffd4e77a48..b5f2ca3007 100644 --- a/java/daikon/tools/runtimechecker/InstrumentHandler.java +++ b/java/daikon/tools/runtimechecker/InstrumentHandler.java @@ -5,11 +5,11 @@ import static java.util.logging.Level.INFO; import daikon.Daikon; +import daikon.DaikonGetopt; import daikon.FileIO; import daikon.Global; import daikon.PptMap; import daikon.tools.jtb.ParseResults; -import gnu.getopt.Getopt; import gnu.getopt.LongOpt; import java.io.File; import java.io.IOException; @@ -213,59 +213,64 @@ private Arguments readArguments(String[] args) { new LongOpt(directory_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(checkers_directory_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0) }; - Getopt g = new Getopt("daikon.tools.runtimechecker.InstrumentHandler", args, "hs", longopts); - int c; - while ((c = g.getopt()) != -1) { - switch (c) { - case 0: - // got a long option - String option_name = longopts[g.getLongind()].getName(); - - if (create_checker_classes_SWITCH.equals(option_name)) { - createCheckerClasses = true; - } else if (output_only_high_conf_invariants_SWITCH.equals(option_name)) { - InstrumentVisitor.outputOnlyHighConfInvariants = true; - } else if (make_all_fields_public_SWITCH.equals(option_name)) { - InstrumentVisitor.makeAllFieldsPublic = true; - } else if (directory_SWITCH.equals(option_name)) { - instrumented_directory = Daikon.getOptarg(g); - } else if (checkers_directory_SWITCH.equals(option_name)) { - checkersOutputDirName = Daikon.getOptarg(g); - } else if (Daikon.debugAll_SWITCH.equals(option_name)) { - Global.debugAll = true; - } else if (Daikon.debug_SWITCH.equals(option_name)) { - daikon.LogHelper.setLevel(Daikon.getOptarg(g), FINE); - } else { - System.err.println("Unknown long option received: " + option_name); - } - break; - default: - System.out.println("unrecognized option" + c); - return errorWhileReadingArguments; + // Main prints the usage message when this tool fails, so DaikonGetopt need not handle -h. + DaikonGetopt g = new DaikonGetopt(args, "", longopts); + try { + int c; + while ((c = g.getopt()) != -1) { + switch (c) { + case 0: + // got a long option + String option_name = longopts[g.getLongind()].getName(); + + if (create_checker_classes_SWITCH.equals(option_name)) { + createCheckerClasses = true; + } else if (output_only_high_conf_invariants_SWITCH.equals(option_name)) { + InstrumentVisitor.outputOnlyHighConfInvariants = true; + } else if (make_all_fields_public_SWITCH.equals(option_name)) { + InstrumentVisitor.makeAllFieldsPublic = true; + } else if (directory_SWITCH.equals(option_name)) { + instrumented_directory = Daikon.getOptarg(g); + } else if (checkers_directory_SWITCH.equals(option_name)) { + checkersOutputDirName = Daikon.getOptarg(g); + } else if (Daikon.debugAll_SWITCH.equals(option_name)) { + Global.debugAll = true; + } else if (Daikon.debug_SWITCH.equals(option_name)) { + daikon.LogHelper.setLevel(Daikon.getOptarg(g), FINE); + } else { + throw new Daikon.BugInDaikon("Unhandled long option " + option_name); + } + break; + default: + throw new Daikon.BugInDaikon("getopt() returned " + c); + } } + } catch (Daikon.UserError e) { + System.err.println("Error: " + e.getMessage()); + return errorWhileReadingArguments; } // The index of the first non-option argument -- the name of the // invariant file. int argindex = g.getOptind(); if (argindex >= args.length) { - System.out.println("Error: No .inv file or .java file arguments supplied."); + System.err.println("Error: No .inv file or .java file arguments supplied."); return errorWhileReadingArguments; } String invfile = args[argindex]; argindex++; if (!(invfile.endsWith(".inv") || invfile.endsWith(".inv.gz"))) { - System.out.println("Error: first argument must be a file ending in .inv or .inv.gz."); + System.err.println("Error: first argument must be a file ending in .inv or .inv.gz."); return errorWhileReadingArguments; } if (argindex >= args.length) { - System.out.println("Error: No .java file arguments supplied."); + System.err.println("Error: No .java file arguments supplied."); return errorWhileReadingArguments; } List javaFileNames = new ArrayList<>(); for (; argindex < args.length; argindex++) { String javafile = args[argindex]; if (!javafile.endsWith(".java")) { - System.out.println("File does not end in .java: " + javafile); + System.err.println("File does not end in .java: " + javafile); return errorWhileReadingArguments; } javaFileNames.add(javafile); From 0242d85cf45cb882e98c0998966c37fb3fdc74a5 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Tue, 6 Oct 2026 22:19:39 -0700 Subject: [PATCH 05/30] Address code review: recognize -h in instrumenter, fix runcluster.pl option, report SplitterFactoryTest failures on stderr, shorten Diff error, align Annotate usage --- java/daikon/diff/Diff.java | 2 +- .../test/split/SplitterFactoryTestUpdater.java | 5 +++-- java/daikon/tools/jtb/Annotate.java | 8 ++++---- .../tools/runtimechecker/InstrumentHandler.java | 16 +++++++++++++--- scripts/runcluster.pl | 2 +- scripts/trace-purge-vars.pl | 2 +- 6 files changed, 23 insertions(+), 12 deletions(-) diff --git a/java/daikon/diff/Diff.java b/java/daikon/diff/Diff.java index 6355ef0892..324406bafb 100644 --- a/java/daikon/diff/Diff.java +++ b/java/daikon/diff/Diff.java @@ -413,7 +413,7 @@ public static void mainHelper(final String[] args) v1.printAll(); return; } else { - throw new Daikon.UserError("No invariant files specified" + Daikon.lineSep + usage); + throw new Daikon.UserError("No invariant files specified; run with -h for usage"); } if (logging) { diff --git a/java/daikon/test/split/SplitterFactoryTestUpdater.java b/java/daikon/test/split/SplitterFactoryTestUpdater.java index c5be608fc7..7f9e1f1016 100644 --- a/java/daikon/test/split/SplitterFactoryTestUpdater.java +++ b/java/daikon/test/split/SplitterFactoryTestUpdater.java @@ -269,12 +269,13 @@ private static String getTestClassText() { ps.println(" }"); ps.println(" Result result = JUnitCore.runClasses(SplitterFactoryTest.class);"); ps.println(" for (Failure failure : result.getFailures()) {"); - ps.println(" System.out.println(failure.getTrace());"); + ps.println(" System.err.println(failure.getTrace());"); ps.println(" }"); ps.println(" if (!result.wasSuccessful()) {"); ps.println( - " throw new Daikon.UserError(StringsPlume.nPlural(result.getFailureCount(), \"test\")" + " System.err.println(StringsPlume.nPlural(result.getFailureCount(), \"test\")" + " + \" failed\");"); + ps.println(" System.exit(1);"); ps.println(" }"); ps.println(" }"); ps.println(); diff --git a/java/daikon/tools/jtb/Annotate.java b/java/daikon/tools/jtb/Annotate.java index cee7292928..b76c70ef97 100644 --- a/java/daikon/tools/jtb/Annotate.java +++ b/java/daikon/tools/jtb/Annotate.java @@ -87,10 +87,10 @@ private Annotate() { private static String usage = StringsPlume.joinLines( "Usage: java daikon.tools.Annotate FILE.inv FILE.java ...", - " -h, --help Display this usage message", - " -i Insert invariants not supported by ESC with \"!\" instead of \"@\";", - " by default these \"inexpressible\" invariants are simply omitted", - " -s Use // comments rather than /* comments", + " -h, --help Display this usage message", + " -i Insert invariants not supported by ESC with \"!\" instead of \"@\";", + " by default these \"inexpressible\" invariants are simply omitted", + " -s Use // comments rather than /* comments", " --format name Insert specifications in the given format: DBC, ESC, JML, Java", " --wrap_xml Wrap each annotation and auxiliary information in XML tags", " --max_invariants_pp N", diff --git a/java/daikon/tools/runtimechecker/InstrumentHandler.java b/java/daikon/tools/runtimechecker/InstrumentHandler.java index b5f2ca3007..0886230f41 100644 --- a/java/daikon/tools/runtimechecker/InstrumentHandler.java +++ b/java/daikon/tools/runtimechecker/InstrumentHandler.java @@ -89,6 +89,9 @@ public boolean handle(String[] args) { if (arguments == errorWhileReadingArguments) { return false; } + if (arguments == helpRequested) { + return true; + } // Set up debug traces; note this comes after reading command line options. daikon.LogHelper.setupLogs(Global.debugAll ? FINE : INFO); @@ -201,6 +204,9 @@ private static class Arguments { private static Arguments errorWhileReadingArguments = new Arguments("error while reading arguments", new ArrayList()); + /** Indicates that the user requested the usage message, which has already been printed. */ + private static Arguments helpRequested = new Arguments("help requested", new ArrayList()); + private Arguments readArguments(String[] args) { LongOpt[] longopts = @@ -211,10 +217,11 @@ private Arguments readArguments(String[] args) { new LongOpt(make_all_fields_public_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(create_checker_classes_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(directory_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), - new LongOpt(checkers_directory_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0) + new LongOpt(checkers_directory_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), + new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 'h') }; - // Main prints the usage message when this tool fails, so DaikonGetopt need not handle -h. - DaikonGetopt g = new DaikonGetopt(args, "", longopts); + // This class handles -h itself, because its usage message is printed by usageMessage(). + DaikonGetopt g = new DaikonGetopt(args, "h", longopts); try { int c; while ((c = g.getopt()) != -1) { @@ -241,6 +248,9 @@ private Arguments readArguments(String[] args) { throw new Daikon.BugInDaikon("Unhandled long option " + option_name); } break; + case 'h': + usageMessage(); + return helpRequested; default: throw new Daikon.BugInDaikon("getopt() returned " + c); } diff --git a/scripts/runcluster.pl b/scripts/runcluster.pl index bf31f6de12..12559cd84c 100755 --- a/scripts/runcluster.pl +++ b/scripts/runcluster.pl @@ -209,7 +209,7 @@ () } my $invfile = "runcluster_temp_$algorithm-$ncluster.inv"; -$command = "java -cp $SCRIPTDIR/../daikon.jar -Xmx7g daikon.Daikon -o $invfile --config_option daikon.PptTopLevel.pairwise_implications=true --var_omit_pattern=\"class\" --no_text_output --no_show_progress $spinfo_file $decls_new " . join(' ', @new_dtraces) . " 2>&1 > runcluster_temp_Daikon_output.txt"; +$command = "java -cp $SCRIPTDIR/../daikon.jar -Xmx7g daikon.Daikon -o $invfile --config_option daikon.PptTopLevel.pairwise_implications=true --var-omit-pattern=\"class\" --no_text_output --no_show_progress $spinfo_file $decls_new " . join(' ', @new_dtraces) . " 2>&1 > runcluster_temp_Daikon_output.txt"; system_or_die($command, $verbose); $invfile =~ /(.*)\.inv/; diff --git a/scripts/trace-purge-vars.pl b/scripts/trace-purge-vars.pl index f3b95d73d9..c75fdc9a7e 100755 --- a/scripts/trace-purge-vars.pl +++ b/scripts/trace-purge-vars.pl @@ -8,7 +8,7 @@ # regular expression. (Its name is taken from grep.) # This script reduces the size of a trace file, but typically, using -# Daikon's --var_omit_pattern flag is a better option. +# Daikon's --var-omit-pattern flag is a better option. # I'm intentionally not operating by paragraphs, because I want the regexp # to only apply to the variable lines. From 9fc85faa8e21ada15ca6fa8047e3f0a99a173372 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 7 Oct 2026 07:20:27 -0700 Subject: [PATCH 06/30] Address code review: SplitterFactoryTest.mainHelper throws instead of exiting, report instrumenter option errors in one line, make sentinels final --- .../split/SplitterFactoryTestUpdater.java | 3 +- .../runtimechecker/InstrumentHandler.java | 70 +++++++++---------- java/daikon/tools/runtimechecker/Main.java | 6 ++ 3 files changed, 40 insertions(+), 39 deletions(-) diff --git a/java/daikon/test/split/SplitterFactoryTestUpdater.java b/java/daikon/test/split/SplitterFactoryTestUpdater.java index 7f9e1f1016..758c93b4a2 100644 --- a/java/daikon/test/split/SplitterFactoryTestUpdater.java +++ b/java/daikon/test/split/SplitterFactoryTestUpdater.java @@ -273,9 +273,8 @@ private static String getTestClassText() { ps.println(" }"); ps.println(" if (!result.wasSuccessful()) {"); ps.println( - " System.err.println(StringsPlume.nPlural(result.getFailureCount(), \"test\")" + " throw new Daikon.UserError(StringsPlume.nPlural(result.getFailureCount(), \"test\")" + " + \" failed\");"); - ps.println(" System.exit(1);"); ps.println(" }"); ps.println(" }"); ps.println(); diff --git a/java/daikon/tools/runtimechecker/InstrumentHandler.java b/java/daikon/tools/runtimechecker/InstrumentHandler.java index 0886230f41..72597a48a0 100644 --- a/java/daikon/tools/runtimechecker/InstrumentHandler.java +++ b/java/daikon/tools/runtimechecker/InstrumentHandler.java @@ -201,11 +201,12 @@ private static class Arguments { } } - private static Arguments errorWhileReadingArguments = + private static final Arguments errorWhileReadingArguments = new Arguments("error while reading arguments", new ArrayList()); /** Indicates that the user requested the usage message, which has already been printed. */ - private static Arguments helpRequested = new Arguments("help requested", new ArrayList()); + private static final Arguments helpRequested = + new Arguments("help requested", new ArrayList()); private Arguments readArguments(String[] args) { @@ -222,42 +223,37 @@ private Arguments readArguments(String[] args) { }; // This class handles -h itself, because its usage message is printed by usageMessage(). DaikonGetopt g = new DaikonGetopt(args, "h", longopts); - try { - int c; - while ((c = g.getopt()) != -1) { - switch (c) { - case 0: - // got a long option - String option_name = longopts[g.getLongind()].getName(); - - if (create_checker_classes_SWITCH.equals(option_name)) { - createCheckerClasses = true; - } else if (output_only_high_conf_invariants_SWITCH.equals(option_name)) { - InstrumentVisitor.outputOnlyHighConfInvariants = true; - } else if (make_all_fields_public_SWITCH.equals(option_name)) { - InstrumentVisitor.makeAllFieldsPublic = true; - } else if (directory_SWITCH.equals(option_name)) { - instrumented_directory = Daikon.getOptarg(g); - } else if (checkers_directory_SWITCH.equals(option_name)) { - checkersOutputDirName = Daikon.getOptarg(g); - } else if (Daikon.debugAll_SWITCH.equals(option_name)) { - Global.debugAll = true; - } else if (Daikon.debug_SWITCH.equals(option_name)) { - daikon.LogHelper.setLevel(Daikon.getOptarg(g), FINE); - } else { - throw new Daikon.BugInDaikon("Unhandled long option " + option_name); - } - break; - case 'h': - usageMessage(); - return helpRequested; - default: - throw new Daikon.BugInDaikon("getopt() returned " + c); - } + int c; + while ((c = g.getopt()) != -1) { + switch (c) { + case 0: + // got a long option + String option_name = longopts[g.getLongind()].getName(); + + if (create_checker_classes_SWITCH.equals(option_name)) { + createCheckerClasses = true; + } else if (output_only_high_conf_invariants_SWITCH.equals(option_name)) { + InstrumentVisitor.outputOnlyHighConfInvariants = true; + } else if (make_all_fields_public_SWITCH.equals(option_name)) { + InstrumentVisitor.makeAllFieldsPublic = true; + } else if (directory_SWITCH.equals(option_name)) { + instrumented_directory = Daikon.getOptarg(g); + } else if (checkers_directory_SWITCH.equals(option_name)) { + checkersOutputDirName = Daikon.getOptarg(g); + } else if (Daikon.debugAll_SWITCH.equals(option_name)) { + Global.debugAll = true; + } else if (Daikon.debug_SWITCH.equals(option_name)) { + daikon.LogHelper.setLevel(Daikon.getOptarg(g), FINE); + } else { + throw new Daikon.BugInDaikon("Unhandled long option " + option_name); + } + break; + case 'h': + usageMessage(); + return helpRequested; + default: + throw new Daikon.BugInDaikon("getopt() returned " + c); } - } catch (Daikon.UserError e) { - System.err.println("Error: " + e.getMessage()); - return errorWhileReadingArguments; } // The index of the first non-option argument -- the name of the // invariant file. diff --git a/java/daikon/tools/runtimechecker/Main.java b/java/daikon/tools/runtimechecker/Main.java index 9259e07f45..4f401bc165 100644 --- a/java/daikon/tools/runtimechecker/Main.java +++ b/java/daikon/tools/runtimechecker/Main.java @@ -1,5 +1,6 @@ package daikon.tools.runtimechecker; +import daikon.Daikon; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -57,6 +58,11 @@ public void nonStaticMain(String[] args) { } } + } catch (Daikon.UserError e) { + // A user error, such as a bad command-line option, is reported in one line. System.exit + // prevents the finally block from printing generic failure text and the usage message. + System.err.println("Error: " + e.getMessage()); + System.exit(1); } catch (Throwable e) { System.out.println("Throwable thrown while handling command:" + e); e.printStackTrace(); From ada0caf4e0200eefcf333717bba169ab856b951e Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 7 Oct 2026 07:55:12 -0700 Subject: [PATCH 07/30] Provide DaikonGetopt helpers that the per-tool PRs use; leave instrumenter -h to runtimechecker-options DaikonGetopt again provides nonOptionArgs, argsAfterLeadingOptions, a Supplier-based constructor, and setUsageHint, which #885-#888 use. The instrumenter's -h and --help handling belongs to #888. --- java/daikon/DaikonGetopt.java | 135 +++++++++++++++--- .../runtimechecker/InstrumentHandler.java | 16 +-- 2 files changed, 120 insertions(+), 31 deletions(-) diff --git a/java/daikon/DaikonGetopt.java b/java/daikon/DaikonGetopt.java index a2a196f920..11a7807ac3 100644 --- a/java/daikon/DaikonGetopt.java +++ b/java/daikon/DaikonGetopt.java @@ -4,6 +4,8 @@ import gnu.getopt.Getopt; import gnu.getopt.LongOpt; +import java.util.Arrays; +import java.util.function.Supplier; import org.checkerframework.checker.nullness.qual.Nullable; import org.plumelib.util.ArraysPlume; @@ -22,10 +24,13 @@ public class DaikonGetopt extends Getopt { private static final String PROGNAME = "DaikonGetopt"; /** - * The usage message printed for {@code -h} and {@code --help}, or null if the caller handles - * them. + * Computes the usage message printed for {@code -h} and {@code --help}, or null if the caller + * handles them. */ - private final @Nullable String usage; + private final @Nullable Supplier usage; + + /** Text appended to the description of a bad command-line option, or null to append nothing. */ + private @Nullable String usageHint = "run with -h for usage"; /** * Creates a command-line option processor that recognizes short and long options. Its description @@ -39,9 +44,23 @@ public class DaikonGetopt extends Getopt { public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts) { super(PROGNAME, argv, nonEmpty(optstring), longopts); this.usage = null; + usageHint = null; opterr = false; } + /** + * Returns short options that are equivalent to the given ones but are not empty. Getopt replaces + * an empty optstring by " ", which makes "- " a valid option. + * + * @param optstring short options, in the format of {@link Getopt} + * @return {@code optstring}, or ":" if {@code optstring} is empty + */ + private static String nonEmpty(String optstring) { + // A leading ':' only makes Getopt return ':' rather than '?' for a missing argument, which + // getopt() treats identically. + return optstring.isEmpty() ? ":" : optstring; + } + /** * Creates a command-line option processor that recognizes short and long options, plus {@code -h} * and {@code --help}, which print the usage message and throw {@link Daikon.NormalTermination}. @@ -52,22 +71,23 @@ public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts) { * @param usage the usage message to print for {@code -h} and {@code --help} */ public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts, String usage) { - super(PROGNAME, argv, withH(optstring), withHelp(longopts)); - this.usage = usage; - opterr = false; + this(argv, optstring, longopts, () -> usage); } /** - * Returns short options that are equivalent to the given ones but are not empty. Getopt replaces - * an empty optstring by " ", which makes "- " a valid option. + * Creates a command-line option processor that recognizes short and long options, plus {@code -h} + * and {@code --help}, which print the usage message and throw {@link Daikon.NormalTermination}. + * The usage message is computed only if it is needed. * - * @param optstring short options, in the format of {@link Getopt} - * @return {@code optstring}, or ":" if {@code optstring} is empty + * @param argv the command-line arguments + * @param optstring the short options, in the format of {@link Getopt}; must not contain {@code h} + * @param longopts the long options; must not contain {@code help} + * @param usage computes the usage message to print for {@code -h} and {@code --help} */ - private static String nonEmpty(String optstring) { - // A leading ':' only makes Getopt return ':' rather than '?' for a missing argument, which - // getopt() treats identically. - return optstring.isEmpty() ? ":" : optstring; + public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts, Supplier usage) { + super(PROGNAME, argv, withH(optstring), withHelp(longopts)); + this.usage = usage; + opterr = false; } /** @@ -99,6 +119,87 @@ private static LongOpt[] withHelp(LongOpt[] longopts) { longopts, new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 'h')); } + /** + * Processes the command-line arguments of a program that takes no options other than {@code -h} + * and {@code --help}. + * + * @param argv the command-line arguments + * @param usage the usage message to print for {@code -h} and {@code --help} + * @return the arguments that are not options + * @throws Daikon.NormalTermination after printing the usage message, if an argument requests it + * @throws Daikon.UserError if an argument is any other option + */ + public static String[] nonOptionArgs(String[] argv, String usage) { + return argsAfterOptions(argv, "", () -> usage); + } + + /** + * Processes the leading command-line arguments of a program that takes no options other than + * {@code -h} and {@code --help}. Processing stops at the first argument that is not an option, so + * later arguments may be options for some other program. + * + * @param argv the command-line arguments + * @param usage the usage message to print for {@code -h} and {@code --help} + * @return the arguments from the first one that is not an option onward + * @throws Daikon.NormalTermination after printing the usage message, if a leading argument + * requests it + * @throws Daikon.UserError if a leading argument is any other option + */ + public static String[] argsAfterLeadingOptions(String[] argv, String usage) { + return argsAfterLeadingOptions(argv, () -> usage); + } + + /** + * Like {@link #argsAfterLeadingOptions(String[], String)}, but computes the usage message only if + * it is needed. + * + * @param argv the command-line arguments + * @param usage computes the usage message to print for {@code -h} and {@code --help} + * @return the arguments from the first one that is not an option onward + * @throws Daikon.NormalTermination after printing the usage message, if a leading argument + * requests it + * @throws Daikon.UserError if a leading argument is any other option + */ + public static String[] argsAfterLeadingOptions(String[] argv, Supplier usage) { + // A leading "+" tells Getopt to stop at the first argument that is not an option. + return argsAfterOptions(argv, "+", usage); + } + + /** + * Processes command-line arguments, permitting no options other than {@code -h} and {@code + * --help}. + * + * @param argv the command-line arguments + * @param optstring "" to process all the arguments, or "+" to stop at the first non-option + * @param usage computes the usage message to print for {@code -h} and {@code --help} + * @return the arguments that Getopt did not process as options + * @throws Daikon.NormalTermination after printing the usage message, if an argument requests it + * @throws Daikon.UserError if an argument is any other option + */ + private static String[] argsAfterOptions( + String[] argv, String optstring, Supplier usage) { + // Getopt permutes its argument array, so give it a copy to avoid modifying the caller's array. + String[] argvCopy = argv.clone(); + DaikonGetopt g = new DaikonGetopt(argvCopy, optstring, new LongOpt[0], usage); + // Every option other than -h and --help is bad, so getopt() returns only -1 or throws. + int c = g.getopt(); + if (c != -1) { + throw new Daikon.BugInDaikon("getopt() returned " + c); + } + return Arrays.copyOfRange(argvCopy, g.getOptind(), argvCopy.length); + } + + /** + * Sets the text that is appended to the description of a bad command-line option. The default + * tells the user to run the program with -h. + * + * @param usageHint text appended to the description of a bad command-line option, or null to + * append nothing + */ + public void setUsageHint(@Nullable String usageHint) { + this.usageHint = usageHint; + } + /** * Like {@link Getopt#getopt()}, but never returns {@code '?'} or {@code ':'}. Getopt returns * {@code ':'} for a missing argument if the optstring starts with {@code ':'}. If this was @@ -116,7 +217,7 @@ public int getopt() { throw badOptionError(); } if (c == 'h' && usage != null) { - System.out.println(usage); + System.out.println(usage.get()); throw new Daikon.NormalTermination(); } return c; @@ -130,8 +231,8 @@ public int getopt() { */ private Daikon.UserError badOptionError() { String message = badOptionMessage(); - if (usage != null) { - message += "; run with -h for usage"; + if (usageHint != null) { + message += "; " + usageHint; } return new Daikon.UserError(message); } diff --git a/java/daikon/tools/runtimechecker/InstrumentHandler.java b/java/daikon/tools/runtimechecker/InstrumentHandler.java index 72597a48a0..06ed933a62 100644 --- a/java/daikon/tools/runtimechecker/InstrumentHandler.java +++ b/java/daikon/tools/runtimechecker/InstrumentHandler.java @@ -89,9 +89,6 @@ public boolean handle(String[] args) { if (arguments == errorWhileReadingArguments) { return false; } - if (arguments == helpRequested) { - return true; - } // Set up debug traces; note this comes after reading command line options. daikon.LogHelper.setupLogs(Global.debugAll ? FINE : INFO); @@ -204,10 +201,6 @@ private static class Arguments { private static final Arguments errorWhileReadingArguments = new Arguments("error while reading arguments", new ArrayList()); - /** Indicates that the user requested the usage message, which has already been printed. */ - private static final Arguments helpRequested = - new Arguments("help requested", new ArrayList()); - private Arguments readArguments(String[] args) { LongOpt[] longopts = @@ -218,11 +211,9 @@ private Arguments readArguments(String[] args) { new LongOpt(make_all_fields_public_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(create_checker_classes_SWITCH, LongOpt.NO_ARGUMENT, null, 0), new LongOpt(directory_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), - new LongOpt(checkers_directory_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), - new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 'h') + new LongOpt(checkers_directory_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0) }; - // This class handles -h itself, because its usage message is printed by usageMessage(). - DaikonGetopt g = new DaikonGetopt(args, "h", longopts); + DaikonGetopt g = new DaikonGetopt(args, "", longopts); int c; while ((c = g.getopt()) != -1) { switch (c) { @@ -248,9 +239,6 @@ private Arguments readArguments(String[] args) { throw new Daikon.BugInDaikon("Unhandled long option " + option_name); } break; - case 'h': - usageMessage(); - return helpRequested; default: throw new Daikon.BugInDaikon("getopt() returned " + c); } From 7a0aa9280b9e2527f8553e43199f7dee970d8e64 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 7 Oct 2026 14:59:16 -0700 Subject: [PATCH 08/30] TraceSelect: pass the random generator to MultiRandSelector so -SEED takes effect Co-Authored-By: Claude Opus 5.5 --- java/daikon/tools/TraceSelect.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/daikon/tools/TraceSelect.java b/java/daikon/tools/TraceSelect.java index 7bf9c29bd3..d0cad2a467 100644 --- a/java/daikon/tools/TraceSelect.java +++ b/java/daikon/tools/TraceSelect.java @@ -207,7 +207,7 @@ else if (args[i].endsWith(".dtrace")) { List al = new ArrayList<>(); try (DtracePartitioner dec = new DtracePartitioner(fileName)) { - MultiRandSelector mrs = new MultiRandSelector<>(numPerSample, dec); + MultiRandSelector mrs = new MultiRandSelector<>(numPerSample, randObj, dec); while (dec.hasNext()) { mrs.accept(dec.next()); From 7b0bd65d190891ef4ed9becca55fdc6ece7f9e27 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 8 Oct 2026 08:00:41 -0700 Subject: [PATCH 09/30] Address code review: make TraceSelect state local, honor -SEED, check all arguments before sampling - Pass the seeded Random to MultiRandSelector, so -SEED takes effect. - Call Daikon.mainHelper and MultiDiff.mainHelper rather than main, so TraceSelect.mainHelper never calls System.exit; clean up samples in finally. - Validate the Daikon arguments (and -h/--help) with Daikon.read_options before sampling, so a misspelled option is reported immediately. - Require num_reps and sample_size to be positive. - TraceSelect options must precede the Daikon arguments, as documented. - Replace mutable static fields with locals and parameters. - Document -SEED and the Diff exit-status change. --- doc/CHANGELOG.md | 7 +- doc/daikon.texinfo | 6 +- java/daikon/Daikon.java | 16 +- java/daikon/test/BadOptionTest.java | 19 ++ java/daikon/tools/TraceSelect.java | 283 ++++++++++++---------------- 5 files changed, 166 insertions(+), 165 deletions(-) diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 6aa334f3e8..9b5f7ecb5e 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -28,7 +28,12 @@ Further documentation can be found in: status. Previously, Daikon printed its usage message and exited with status 0, and most other tools ignored the bad option and continued. * `daikon.diff.Diff` no longer accepts the `-u` and `-z` options, which had - no effect. + no effect. When run with no invariant files, it reports an error and exits + with a non-zero status, rather than printing its usage message and exiting + with status 0. +* `daikon.tools.TraceSelect` checks all its arguments before sampling. Its + `-SEED` option now takes effect. Its options must precede the arguments + that it passes to Daikon. ## Version 5.9.0 (September 1, 2026) diff --git a/doc/daikon.texinfo b/doc/daikon.texinfo index 63824ae74e..5830476a63 100644 --- a/doc/daikon.texinfo +++ b/doc/daikon.texinfo @@ -2981,6 +2981,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 @@ -9229,7 +9233,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 aaa2b3f3d6..97c6ca0dd3 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", @@ -1003,8 +1003,18 @@ public FileOptions(Set decls, Set dtrace, Set spinfo, Set PrintInvariants.mainHelper(new String[] {"--bogus", "foo.inv.gz"})); } + /** Tests that TraceSelect rejects bad arguments before doing any sampling. */ + @Test + public void testTraceSelectBadArguments() { + assertThrows( + Daikon.UserError.class, () -> TraceSelect.mainHelper(new String[] {"0", "10", "x.dtrace"})); + assertThrows( + Daikon.UserError.class, + () -> TraceSelect.mainHelper(new String[] {"20", "-5", "x.dtrace"})); + assertThrows( + Daikon.UserError.class, + () -> TraceSelect.mainHelper(new String[] {"20", "ten", "x.dtrace"})); + // A misspelled TraceSelect option is a bad Daikon argument. + assertThrows( + Daikon.UserError.class, + () -> TraceSelect.mainHelper(new String[] {"20", "10", "-NOCLAEN", "x.dtrace"})); + } + /** Tests that tools accept the {@code --help} option that their usage messages document. */ @Test public void testHelpOption() { @@ -156,6 +173,8 @@ public void testHelpOption() { 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", "-NOCLEAN", "x.dtrace", "--he"})); } /** diff --git a/java/daikon/tools/TraceSelect.java b/java/daikon/tools/TraceSelect.java index 7bf9c29bd3..b46be80bdd 100644 --- a/java/daikon/tools/TraceSelect.java +++ b/java/daikon/tools/TraceSelect.java @@ -5,6 +5,7 @@ import java.io.File; 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; @@ -15,7 +16,6 @@ import java.util.Locale; import java.util.Random; import java.util.StringTokenizer; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.dataflow.qual.Pure; import org.plumelib.util.FilesPlume; import org.plumelib.util.MultiRandSelector; @@ -32,38 +32,19 @@ 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; - - // // 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; - - /** The arguments to pass to Daikon, other than the .dtrace file. */ - private static List daikonArgs = new ArrayList<>(); - - // 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; - /** The usage message for this program. */ private static final String usage = StringsPlume.joinLines( "USAGE: TraceSelect num_reps sample_size [options] [Daikon-args]...", + "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.", "Example: java TraceSelect 20 10 -NOCLEAN -INCLUDE_UNRETURNED -SEED 1000 foo.dtrace" - + " foo2.dtrace foo.decls RatPoly.decls foo3.dtrace"); + + " foo.decls RatPoly.decls", + "", + "The Daikon-args are:", + daikon.Daikon.usage); /** * The entry point of TraceSelect. @@ -87,32 +68,30 @@ public static void main(String[] args) { public static void mainHelper(String[] args) { // Handles -h and --help before num_reps. args = DaikonGetopt.argsAfterLeadingOptions(args, usage); - // Reset the state from any previous call. - CLEAN = true; - INCLUDE_UNRETURNED = false; - DO_DIFFS = false; - daikonArgs = new ArrayList<>(); - String inputFile = null; - Random rand = null; if (args.length < 2) { throw new daikon.Daikon.UserError("Too few arguments." + daikon.Daikon.lineSep + usage); } - try { - num_reps = Integer.parseInt(args[0]); - numPerSample = Integer.parseInt(args[1]); - } catch (NumberFormatException e) { - throw new daikon.Daikon.UserError( - "num_reps and sample_size must be integers." + daikon.Daikon.lineSep + 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 requires an argument"); } @@ -122,147 +101,129 @@ public static void mainHelper(String[] args) { } 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 { + break; } + } - // 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; - } - - // 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; - } - - // 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; - } - - // The Daikon arguments start with the first argument that is not a TraceSelect switch, - // such as a .dtrace or .decls file, or "--nohierarchy" or "--format java". - - // -h and --help before the Daikon arguments print the usage message. - else if (!knowArgStart - && (args[i].equals("-h") || args[i].equals("--" + daikon.Daikon.help_SWITCH))) { - System.out.println(usage); - throw new daikon.Daikon.NormalTermination(); - } - - // For now, only the first dtrace file will be sampled - else if (args[i].endsWith(".dtrace")) { - if (inputFile == null) { - inputFile = args[i]; - } else { + // The remaining arguments are the Daikon arguments, such as .dtrace and .decls files, + // "--nohierarchy", or "--format java". Only the .dtrace file is sampled. + String inputFile = null; + List daikonArgs = new ArrayList<>(); + for (; i < args.length; i++) { + if (args[i].endsWith(".dtrace")) { + if (inputFile != null) { throw new daikon.Daikon.UserError("Only 1 dtrace file for input allowed"); } - - knowArgStart = true; + inputFile = args[i]; } else { - knowArgStart = true; daikonArgs.add(args[i]); } } - - // if no seed provided, use default Random() constructor - randObj = (rand != null) ? rand : new Random(); - - sampleNames = new String[num_reps + 1]; - sampleNames[0] = "-p"; - 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\")"); } - fileName = inputFile; - try { - - // invokeBuffer = new ArrayList(); - // fileName = args[1]; + // Check the Daikon arguments before doing any sampling, so that a bad argument (including a + // misspelled TraceSelect option) is reported immediately. This also handles -h and --help. + List argsToCheck = new ArrayList<>(daikonArgs); + argsToCheck.add(inputFile); + daikon.Daikon.read_options(argsToCheck.toArray(new String[0]), usage); - System.out.println("*******Processing********"); + // The arguments to daikon.diff.MultiDiff: "-p" followed by the .inv file for each sample. + String[] sampleNames = new String[numReps + 1]; + sampleNames[0] = "-p"; - // 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]); - */ + System.out.println("*******Processing********"); - while (num_reps > 0) { + try { + for (int rep = numReps; rep > 0; rep--) { List al = new ArrayList<>(); - try (DtracePartitioner dec = new DtracePartitioner(fileName)) { - MultiRandSelector mrs = new MultiRandSelector<>(numPerSample, dec); + try (DtracePartitioner dec = new DtracePartitioner(inputFile)) { + MultiRandSelector mrs = new MultiRandSelector<>(numPerSample, rand, dec); while (dec.hasNext()) { mrs.accept(dec.next()); } - for (Iterator i = mrs.valuesIter(); i.hasNext(); ) { - al.add(i.next()); + for (Iterator iter = mrs.valuesIter(); iter.hasNext(); ) { + al.add(iter.next()); } - List al_tmp = dec.patchValues(al, INCLUDE_UNRETURNED); - al = al_tmp; + al = dec.patchValues(al, includeUnreturned); } - String filePrefix = calcOut(fileName); + String filePrefix = calcOut(inputFile, rep); - // 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"; + sampleNames[rep] = filePrefix + ".inv"; - try (PrintWriter pwOut = new PrintWriter(FilesPlume.newBufferedFileWriter(filePrefix))) { - for (String toPrint : al) { - pwOut.println(toPrint); + try { + try (PrintWriter pwOut = new PrintWriter(FilesPlume.newBufferedFileWriter(filePrefix))) { + for (String toPrint : al) { + pwOut.println(toPrint); + } } - pwOut.flush(); - } - invokeDaikon(filePrefix); - - // Clean up the mess. A failed cleanup is not fatal. - if (CLEAN) { - deleteQuietly(filePrefix); + invokeDaikon(filePrefix, daikonArgs); + } finally { + // Clean up the mess, even if Daikon failed. A failed cleanup is not fatal. + if (clean) { + deleteQuietly(filePrefix); + } } - - num_reps--; } - if (DO_DIFFS) { - // histograms - // daikon.diff.Diff.main (sampleNames); - + if (doDiffs) { // spinfo format - daikon.diff.MultiDiff.main(sampleNames); + daikon.diff.MultiDiff.mainHelper(sampleNames); } + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (ReflectiveOperationException e) { + throw new daikon.Daikon.BugInDaikon(e); + } finally { // 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) { + if (clean) { for (int j = 1; j < sampleNames.length; j++) { - deleteQuietly(sampleNames[j]); + if (sampleNames[j] != null) { + deleteQuietly(sampleNames[j]); + } } } + } + } - } catch (Exception 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 + daikon.Daikon.lineSep + usage); } + return result; } /** @@ -279,25 +240,24 @@ private static void deleteQuietly(String fileName) { } } - private static void invokeDaikon(String dtraceName) throws IOException { + /** + * 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 be run + */ + private static void invokeDaikon(String dtraceName, List daikonArgs) throws IOException { System.out.println("Created file: " + dtraceName); - // this part adds on the rest of the decls files - ArrayList daikonArgsList = new ArrayList<>(); + List daikonArgsList = new ArrayList<>(); daikonArgsList.add(dtraceName); daikonArgsList.add("-o"); daikonArgsList.add(dtraceName + ".inv"); - - // all the Daikon args except for the original single dtrace file daikonArgsList.addAll(daikonArgs); - // create an array to store the Strings in daikonArgsList - String[] daikonArgs = daikonArgsList.toArray(new String[0]); - - // initializes daikon again or else an exception is thrown - reinitializeDaikon(); - daikon.Daikon.main(daikonArgs); + daikon.Daikon.mainHelper(daikonArgsList.toArray(new String[0])); // Run: java daikon.PrintInvariants dtraceName.inv > dtraceName.txt ProcessBuilder pb = new ProcessBuilder("java", "daikon.PrintInvariants", dtraceName + ".inv"); pb.redirectOutput(new File(dtraceName + ".txt")); @@ -314,16 +274,19 @@ private static void invokeDaikon(String dtraceName) throws IOException { } } - private static void reinitializeDaikon() { - daikon.Daikon.inv_file = null; - } - - private static String calcOut(String strFileName) { + /** + * Returns the name of the file that holds the given sample of the given trace file. + * + * @param strFileName the name of the trace file + * @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) { StringBuilder product = new StringBuilder(); int index = strFileName.indexOf('.'); if (index >= 0) { product.append(strFileName.substring(0, index)); - product.append(num_reps); + product.append(rep); if (index != strFileName.length()) { product.append(strFileName.substring(index)); } From 1f929b47ba707c1e09fa03099763544a6e56e500 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 8 Oct 2026 08:31:11 -0700 Subject: [PATCH 10/30] Address code review: TraceSelect sample names, -o, in-process PrintInvariants, MultiDiff exit, simplify DaikonGetopt - calcOut inserts the sample number into the file name, not a directory name. - The pre-check uses the same Daikon command line as each sample, so -o is rejected before sampling. - Run PrintInvariants in-process, so failures are reported. - MultiDiff.mainHelper calls Diff.mainHelper, so it never calls System.exit. - Daikon.cleanup() resets userDefinedInvariants; PrintInvariants resets its output stream. - No array preallocation from num_reps; concise argument-error messages. - Remove unused DaikonGetopt API; add DaikonGetopt.USAGE_HINT. --- java/daikon/Daikon.java | 1 + java/daikon/DaikonGetopt.java | 95 ++++----------------------- java/daikon/PrintInvariants.java | 1 + java/daikon/diff/Diff.java | 2 +- java/daikon/diff/MultiDiff.java | 2 +- java/daikon/tools/TraceSelect.java | 102 ++++++++++++++--------------- 6 files changed, 66 insertions(+), 137 deletions(-) diff --git a/java/daikon/Daikon.java b/java/daikon/Daikon.java index 97c6ca0dd3..f9bfb0558b 100644 --- a/java/daikon/Daikon.java +++ b/java/daikon/Daikon.java @@ -981,6 +981,7 @@ public static void cleanup() { var_omit_regexp = null; server_dir = null; use_mem_monitor = false; + userDefinedInvariants.clear(); proto_invs.clear(); } diff --git a/java/daikon/DaikonGetopt.java b/java/daikon/DaikonGetopt.java index 11a7807ac3..e67c99c448 100644 --- a/java/daikon/DaikonGetopt.java +++ b/java/daikon/DaikonGetopt.java @@ -5,7 +5,6 @@ import gnu.getopt.Getopt; import gnu.getopt.LongOpt; import java.util.Arrays; -import java.util.function.Supplier; import org.checkerframework.checker.nullness.qual.Nullable; import org.plumelib.util.ArraysPlume; @@ -24,13 +23,16 @@ public class DaikonGetopt extends Getopt { private static final String PROGNAME = "DaikonGetopt"; /** - * Computes the usage message printed for {@code -h} and {@code --help}, or null if the caller - * handles them. + * Text that is appended to the description of a bad command-line option, for programs that handle + * {@code -h}. */ - private final @Nullable Supplier usage; + public static final String USAGE_HINT = "run with -h for usage"; - /** Text appended to the description of a bad command-line option, or null to append nothing. */ - private @Nullable String usageHint = "run with -h for usage"; + /** + * The usage message printed for {@code -h} and {@code --help}, or null if the caller handles + * them. + */ + private final @Nullable String usage; /** * Creates a command-line option processor that recognizes short and long options. Its description @@ -44,7 +46,6 @@ public class DaikonGetopt extends Getopt { public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts) { super(PROGNAME, argv, nonEmpty(optstring), longopts); this.usage = null; - usageHint = null; opterr = false; } @@ -71,20 +72,6 @@ private static String nonEmpty(String optstring) { * @param usage the usage message to print for {@code -h} and {@code --help} */ public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts, String usage) { - this(argv, optstring, longopts, () -> usage); - } - - /** - * Creates a command-line option processor that recognizes short and long options, plus {@code -h} - * and {@code --help}, which print the usage message and throw {@link Daikon.NormalTermination}. - * The usage message is computed only if it is needed. - * - * @param argv the command-line arguments - * @param optstring the short options, in the format of {@link Getopt}; must not contain {@code h} - * @param longopts the long options; must not contain {@code help} - * @param usage computes the usage message to print for {@code -h} and {@code --help} - */ - public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts, Supplier usage) { super(PROGNAME, argv, withH(optstring), withHelp(longopts)); this.usage = usage; opterr = false; @@ -119,20 +106,6 @@ private static LongOpt[] withHelp(LongOpt[] longopts) { longopts, new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 'h')); } - /** - * Processes the command-line arguments of a program that takes no options other than {@code -h} - * and {@code --help}. - * - * @param argv the command-line arguments - * @param usage the usage message to print for {@code -h} and {@code --help} - * @return the arguments that are not options - * @throws Daikon.NormalTermination after printing the usage message, if an argument requests it - * @throws Daikon.UserError if an argument is any other option - */ - public static String[] nonOptionArgs(String[] argv, String usage) { - return argsAfterOptions(argv, "", () -> usage); - } - /** * Processes the leading command-line arguments of a program that takes no options other than * {@code -h} and {@code --help}. Processing stops at the first argument that is not an option, so @@ -146,41 +119,10 @@ public static String[] nonOptionArgs(String[] argv, String usage) { * @throws Daikon.UserError if a leading argument is any other option */ public static String[] argsAfterLeadingOptions(String[] argv, String usage) { - return argsAfterLeadingOptions(argv, () -> usage); - } - - /** - * Like {@link #argsAfterLeadingOptions(String[], String)}, but computes the usage message only if - * it is needed. - * - * @param argv the command-line arguments - * @param usage computes the usage message to print for {@code -h} and {@code --help} - * @return the arguments from the first one that is not an option onward - * @throws Daikon.NormalTermination after printing the usage message, if a leading argument - * requests it - * @throws Daikon.UserError if a leading argument is any other option - */ - public static String[] argsAfterLeadingOptions(String[] argv, Supplier usage) { - // A leading "+" tells Getopt to stop at the first argument that is not an option. - return argsAfterOptions(argv, "+", usage); - } - - /** - * Processes command-line arguments, permitting no options other than {@code -h} and {@code - * --help}. - * - * @param argv the command-line arguments - * @param optstring "" to process all the arguments, or "+" to stop at the first non-option - * @param usage computes the usage message to print for {@code -h} and {@code --help} - * @return the arguments that Getopt did not process as options - * @throws Daikon.NormalTermination after printing the usage message, if an argument requests it - * @throws Daikon.UserError if an argument is any other option - */ - private static String[] argsAfterOptions( - String[] argv, String optstring, Supplier usage) { // Getopt permutes its argument array, so give it a copy to avoid modifying the caller's array. String[] argvCopy = argv.clone(); - DaikonGetopt g = new DaikonGetopt(argvCopy, optstring, new LongOpt[0], usage); + // A leading "+" tells Getopt to stop at the first argument that is not an option. + DaikonGetopt g = new DaikonGetopt(argvCopy, "+", new LongOpt[0], usage); // Every option other than -h and --help is bad, so getopt() returns only -1 or throws. int c = g.getopt(); if (c != -1) { @@ -189,17 +131,6 @@ private static String[] argsAfterOptions( return Arrays.copyOfRange(argvCopy, g.getOptind(), argvCopy.length); } - /** - * Sets the text that is appended to the description of a bad command-line option. The default - * tells the user to run the program with -h. - * - * @param usageHint text appended to the description of a bad command-line option, or null to - * append nothing - */ - public void setUsageHint(@Nullable String usageHint) { - this.usageHint = usageHint; - } - /** * Like {@link Getopt#getopt()}, but never returns {@code '?'} or {@code ':'}. Getopt returns * {@code ':'} for a missing argument if the optstring starts with {@code ':'}. If this was @@ -217,7 +148,7 @@ public int getopt() { throw badOptionError(); } if (c == 'h' && usage != null) { - System.out.println(usage.get()); + System.out.println(usage); throw new Daikon.NormalTermination(); } return c; @@ -231,8 +162,8 @@ public int getopt() { */ private Daikon.UserError badOptionError() { String message = badOptionMessage(); - if (usageHint != null) { - message += "; " + usageHint; + if (usage != null) { + message += "; " + USAGE_HINT; } return new Daikon.UserError(message); } diff --git a/java/daikon/PrintInvariants.java b/java/daikon/PrintInvariants.java index 57f483661d..3a2c0f9408 100644 --- a/java/daikon/PrintInvariants.java +++ b/java/daikon/PrintInvariants.java @@ -488,6 +488,7 @@ public static void mainHelper(String[] args) assert out_stream != null : "@AssumeAssertion(nullness): flush() does not affect any global variables"; out_stream.close(); + out_stream = null; } } diff --git a/java/daikon/diff/Diff.java b/java/daikon/diff/Diff.java index 324406bafb..13a2315f22 100644 --- a/java/daikon/diff/Diff.java +++ b/java/daikon/diff/Diff.java @@ -413,7 +413,7 @@ public static void mainHelper(final String[] args) v1.printAll(); return; } else { - throw new Daikon.UserError("No invariant files specified; run with -h for usage"); + throw new Daikon.UserError("No invariant files specified; " + DaikonGetopt.USAGE_HINT); } if (logging) { diff --git a/java/daikon/diff/MultiDiff.java b/java/daikon/diff/MultiDiff.java index b54ec2b5c7..fd6c1ddf61 100644 --- a/java/daikon/diff/MultiDiff.java +++ b/java/daikon/diff/MultiDiff.java @@ -55,7 +55,7 @@ public static void mainHelper(final String[] args) catch (IOException e) {e.printStackTrace(); } */ MultiDiffVisitor.setForSpinfoOut(out); - Diff.main(args); + Diff.mainHelper(args); } } } diff --git a/java/daikon/tools/TraceSelect.java b/java/daikon/tools/TraceSelect.java index b46be80bdd..eb29756d0b 100644 --- a/java/daikon/tools/TraceSelect.java +++ b/java/daikon/tools/TraceSelect.java @@ -2,7 +2,6 @@ package daikon.tools; import daikon.DaikonGetopt; -import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.io.UncheckedIOException; @@ -10,6 +9,7 @@ import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; @@ -40,6 +40,7 @@ private TraceSelect() { "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", "", @@ -70,7 +71,7 @@ public static void mainHelper(String[] args) { args = DaikonGetopt.argsAfterLeadingOptions(args, usage); if (args.length < 2) { - throw new daikon.Daikon.UserError("Too few arguments." + daikon.Daikon.lineSep + usage); + throw new daikon.Daikon.UserError("Too few arguments; " + DaikonGetopt.USAGE_HINT); } int numReps = parsePositiveInt("num_reps", args[0]); @@ -134,13 +135,14 @@ public static void mainHelper(String[] args) { // Check the Daikon arguments before doing any sampling, so that a bad argument (including a // misspelled TraceSelect option) is reported immediately. This also handles -h and --help. - List argsToCheck = new ArrayList<>(daikonArgs); - argsToCheck.add(inputFile); - daikon.Daikon.read_options(argsToCheck.toArray(new String[0]), usage); + // The arguments are the same as those of a sample's Daikon run, except for the .dtrace file, + // which does not exist yet. Daikon.mainHelper calls Daikon.cleanup() before each run, which + // undoes the effects of this check. + daikon.Daikon.cleanup(); + daikon.Daikon.read_options(daikonCommandLine(inputFile, daikonArgs), usage); - // The arguments to daikon.diff.MultiDiff: "-p" followed by the .inv file for each sample. - String[] sampleNames = new String[numReps + 1]; - sampleNames[0] = "-p"; + // The .inv file for each sample, from the last sample to the first. + List sampleNames = new ArrayList<>(); System.out.println("*******Processing********"); @@ -164,13 +166,16 @@ public static void mainHelper(String[] args) { String filePrefix = calcOut(inputFile, rep); - sampleNames[rep] = filePrefix + ".inv"; + sampleNames.add(filePrefix + ".inv"); try { try (PrintWriter pwOut = new PrintWriter(FilesPlume.newBufferedFileWriter(filePrefix))) { for (String toPrint : al) { pwOut.println(toPrint); } + } catch (IOException e) { + throw new daikon.Daikon.UserError( + "Cannot write sample file " + filePrefix + ": " + e.getMessage()); } invokeDaikon(filePrefix, daikonArgs); @@ -183,8 +188,11 @@ public static void mainHelper(String[] args) { } if (doDiffs) { - // spinfo format - daikon.diff.MultiDiff.mainHelper(sampleNames); + // spinfo format. The arguments are "-p" followed by the samples, first sample first. + List multiDiffArgs = new ArrayList<>(sampleNames); + multiDiffArgs.add("-p"); + Collections.reverse(multiDiffArgs); + daikon.diff.MultiDiff.mainHelper(multiDiffArgs.toArray(new String[0])); } } catch (IOException e) { @@ -193,12 +201,9 @@ public static void mainHelper(String[] args) { throw new daikon.Daikon.BugInDaikon(e); } finally { // 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++) { - if (sampleNames[j] != null) { - deleteQuietly(sampleNames[j]); - } + for (String sampleName : sampleNames) { + deleteQuietly(sampleName); } } } @@ -221,7 +226,7 @@ private static int parsePositiveInt(String name, String arg) { } if (result <= 0) { throw new daikon.Daikon.UserError( - name + " must be a positive integer, not " + arg + daikon.Daikon.lineSep + usage); + name + " must be a positive integer, not " + arg + "; " + DaikonGetopt.USAGE_HINT); } return result; } @@ -240,60 +245,51 @@ private static void deleteQuietly(String fileName) { } } + /** + * 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]); + } + /** * 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 be run + * @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 { + private static void invokeDaikon(String dtraceName, List daikonArgs) + throws IOException, ClassNotFoundException { System.out.println("Created file: " + dtraceName); - List daikonArgsList = new ArrayList<>(); - daikonArgsList.add(dtraceName); - daikonArgsList.add("-o"); - daikonArgsList.add(dtraceName + ".inv"); - daikonArgsList.addAll(daikonArgs); - - daikon.Daikon.mainHelper(daikonArgsList.toArray(new String[0])); - // 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 - } + daikon.Daikon.mainHelper(daikonCommandLine(dtraceName, daikonArgs)); + daikon.PrintInvariants.mainHelper( + new String[] {"--output", dtraceName + ".txt", dtraceName + ".inv"}); } /** * Returns the name of the file that holds the given sample of the given trace file. * - * @param strFileName the name of the 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) { - StringBuilder product = new StringBuilder(); - int index = strFileName.indexOf('.'); - if (index >= 0) { - product.append(strFileName.substring(0, index)); - product.append(rep); - if (index != strFileName.length()) { - product.append(strFileName.substring(index)); - } - } else { - product.append(strFileName).append("2"); - } - return product.toString(); + // 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"; } } From e82b4b5f488e28f43b87e5aae4b2cbf8559cfa23 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 10 Oct 2026 10:23:37 -0700 Subject: [PATCH 11/30] Add a same-seed TraceSelect sampling test; skip empty invocations when sampling Factor the sampling out of TraceSelect.mainHelper into selectSample, so that it can be tested without running Daikon. DtracePartitioner returns an empty string for a trailing EXIT, which crashed TraceSelect on typical trace files. --- java/daikon/test/AllTestsSuite.java | 1 + java/daikon/test/TraceSelectTest.java | 61 +++++++++++++++++++++++++++ java/daikon/tools/TraceSelect.java | 49 +++++++++++++++------ 3 files changed, 97 insertions(+), 14 deletions(-) create mode 100644 java/daikon/test/TraceSelectTest.java diff --git a/java/daikon/test/AllTestsSuite.java b/java/daikon/test/AllTestsSuite.java index 9a817b266f..4ed7a15a26 100644 --- a/java/daikon/test/AllTestsSuite.java +++ b/java/daikon/test/AllTestsSuite.java @@ -8,6 +8,7 @@ @Suite.SuiteClasses({ daikon.chicory.RuntimeTest.class, daikon.test.BadOptionTest.class, + daikon.test.TraceSelectTest.class, daikon.test.TestClassOrInterfaceTypeDecorateVisitor.class, daikon.test.TestAst.class, daikon.test.config.ConfigurationTest.class, diff --git a/java/daikon/test/TraceSelectTest.java b/java/daikon/test/TraceSelectTest.java new file mode 100644 index 0000000000..fde50b8cbb --- /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/TraceSelect.java b/java/daikon/tools/TraceSelect.java index eb29756d0b..87bb5829ad 100644 --- a/java/daikon/tools/TraceSelect.java +++ b/java/daikon/tools/TraceSelect.java @@ -149,20 +149,7 @@ public static void mainHelper(String[] args) { try { for (int rep = numReps; rep > 0; rep--) { - List al = new ArrayList<>(); - try (DtracePartitioner dec = new DtracePartitioner(inputFile)) { - MultiRandSelector mrs = new MultiRandSelector<>(numPerSample, rand, dec); - - while (dec.hasNext()) { - mrs.accept(dec.next()); - } - - for (Iterator iter = mrs.valuesIter(); iter.hasNext(); ) { - al.add(iter.next()); - } - - al = dec.patchValues(al, includeUnreturned); - } + List al = selectSample(inputFile, numPerSample, rand, includeUnreturned); String filePrefix = calcOut(inputFile, rep); @@ -209,6 +196,40 @@ public static void mainHelper(String[] args) { } } + /** + * 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); + } + } + + List al = new ArrayList<>(); + for (Iterator iter = mrs.valuesIter(); iter.hasNext(); ) { + al.add(iter.next()); + } + + return dec.patchValues(al, includeUnreturned); + } + } + /** * Parses a command-line argument that must be a positive integer. * From 166c4352c2d4cedc0839e56838cf9fb61ab4756d Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 10 Oct 2026 12:02:12 -0700 Subject: [PATCH 12/30] Make PrintInvariants, MultiDiff, and Daikon.cleanup safe to call more than once in a JVM --- java/daikon/Daikon.java | 1 + java/daikon/PrintInvariants.java | 36 +++++++++++++++++++++++++------- java/daikon/diff/MultiDiff.java | 2 +- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/java/daikon/Daikon.java b/java/daikon/Daikon.java index 1e7c1e1b73..df459d8f40 100644 --- a/java/daikon/Daikon.java +++ b/java/daikon/Daikon.java @@ -981,6 +981,7 @@ public static void cleanup() { var_omit_regexp = null; server_dir = null; use_mem_monitor = false; + userDefinedInvariants.clear(); proto_invs.clear(); } diff --git a/java/daikon/PrintInvariants.java b/java/daikon/PrintInvariants.java index f7bc9121c3..a562be317e 100644 --- a/java/daikon/PrintInvariants.java +++ b/java/daikon/PrintInvariants.java @@ -326,6 +326,34 @@ public static void mainHelper(String[] args) OptionalDataException, IOException, ClassNotFoundException { + try { + printInvariantsFromArgs(args); + } finally { + // Close the output stream if --output was specified, even if printing failed. + // print_invariants sets out_stream to System.out if --output was not specified. + if (out_stream != null) { + OutputStream os = out_stream; + out_stream = null; + if (os == System.out) { + os.flush(); + } else { + os.close(); + } + } + } + } + + /** + * Does the work of {@link #mainHelper}, except closing the output stream. + * + * @param args command-line arguments, like those of {@link #main} + */ + private static void printInvariantsFromArgs(String[] args) + throws FileNotFoundException, + StreamCorruptedException, + OptionalDataException, + IOException, + ClassNotFoundException { LongOpt[] longopts = new LongOpt[] { @@ -492,14 +520,6 @@ public static void mainHelper(String[] args) } print_invariants(ppts); - - // Close the output stream if --output was specified. - if (out_stream != null) { - out_stream.flush(); - assert out_stream != null - : "@AssumeAssertion(nullness): flush() does not affect any global variables"; - out_stream.close(); - } } /** diff --git a/java/daikon/diff/MultiDiff.java b/java/daikon/diff/MultiDiff.java index b54ec2b5c7..fd6c1ddf61 100644 --- a/java/daikon/diff/MultiDiff.java +++ b/java/daikon/diff/MultiDiff.java @@ -55,7 +55,7 @@ public static void mainHelper(final String[] args) catch (IOException e) {e.printStackTrace(); } */ MultiDiffVisitor.setForSpinfoOut(out); - Diff.main(args); + Diff.mainHelper(args); } } } From 27efa4f3253371e75b2b9c1fe8c09f33ac9ad538 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 10 Oct 2026 12:03:20 -0700 Subject: [PATCH 13/30] Address code review: TraceSelect argument checks, compressed traces, file truncation; move unrelated changes to other PRs --- doc/CHANGELOG.md | 4 +- java/daikon/DaikonGetopt.java | 95 +++++++++++++++++++---- java/daikon/diff/Diff.java | 2 +- java/daikon/test/BadOptionTest.java | 43 ++++++++--- java/daikon/tools/TraceSelect.java | 116 ++++++++++++++++++++++------ 5 files changed, 206 insertions(+), 54 deletions(-) diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 9b5f7ecb5e..9d60fc8935 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -28,9 +28,7 @@ Further documentation can be found in: status. Previously, Daikon printed its usage message and exited with status 0, and most other tools ignored the bad option and continued. * `daikon.diff.Diff` no longer accepts the `-u` and `-z` options, which had - no effect. When run with no invariant files, it reports an error and exits - with a non-zero status, rather than printing its usage message and exiting - with status 0. + no effect. * `daikon.tools.TraceSelect` checks all its arguments before sampling. Its `-SEED` option now takes effect. Its options must precede the arguments that it passes to Daikon. diff --git a/java/daikon/DaikonGetopt.java b/java/daikon/DaikonGetopt.java index e67c99c448..11a7807ac3 100644 --- a/java/daikon/DaikonGetopt.java +++ b/java/daikon/DaikonGetopt.java @@ -5,6 +5,7 @@ import gnu.getopt.Getopt; import gnu.getopt.LongOpt; import java.util.Arrays; +import java.util.function.Supplier; import org.checkerframework.checker.nullness.qual.Nullable; import org.plumelib.util.ArraysPlume; @@ -23,16 +24,13 @@ public class DaikonGetopt extends Getopt { private static final String PROGNAME = "DaikonGetopt"; /** - * Text that is appended to the description of a bad command-line option, for programs that handle - * {@code -h}. + * Computes the usage message printed for {@code -h} and {@code --help}, or null if the caller + * handles them. */ - public static final String USAGE_HINT = "run with -h for usage"; + private final @Nullable Supplier usage; - /** - * The usage message printed for {@code -h} and {@code --help}, or null if the caller handles - * them. - */ - private final @Nullable String usage; + /** Text appended to the description of a bad command-line option, or null to append nothing. */ + private @Nullable String usageHint = "run with -h for usage"; /** * Creates a command-line option processor that recognizes short and long options. Its description @@ -46,6 +44,7 @@ public class DaikonGetopt extends Getopt { public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts) { super(PROGNAME, argv, nonEmpty(optstring), longopts); this.usage = null; + usageHint = null; opterr = false; } @@ -72,6 +71,20 @@ private static String nonEmpty(String optstring) { * @param usage the usage message to print for {@code -h} and {@code --help} */ public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts, String usage) { + this(argv, optstring, longopts, () -> usage); + } + + /** + * Creates a command-line option processor that recognizes short and long options, plus {@code -h} + * and {@code --help}, which print the usage message and throw {@link Daikon.NormalTermination}. + * The usage message is computed only if it is needed. + * + * @param argv the command-line arguments + * @param optstring the short options, in the format of {@link Getopt}; must not contain {@code h} + * @param longopts the long options; must not contain {@code help} + * @param usage computes the usage message to print for {@code -h} and {@code --help} + */ + public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts, Supplier usage) { super(PROGNAME, argv, withH(optstring), withHelp(longopts)); this.usage = usage; opterr = false; @@ -106,6 +119,20 @@ private static LongOpt[] withHelp(LongOpt[] longopts) { longopts, new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 'h')); } + /** + * Processes the command-line arguments of a program that takes no options other than {@code -h} + * and {@code --help}. + * + * @param argv the command-line arguments + * @param usage the usage message to print for {@code -h} and {@code --help} + * @return the arguments that are not options + * @throws Daikon.NormalTermination after printing the usage message, if an argument requests it + * @throws Daikon.UserError if an argument is any other option + */ + public static String[] nonOptionArgs(String[] argv, String usage) { + return argsAfterOptions(argv, "", () -> usage); + } + /** * Processes the leading command-line arguments of a program that takes no options other than * {@code -h} and {@code --help}. Processing stops at the first argument that is not an option, so @@ -119,10 +146,41 @@ private static LongOpt[] withHelp(LongOpt[] longopts) { * @throws Daikon.UserError if a leading argument is any other option */ public static String[] argsAfterLeadingOptions(String[] argv, String usage) { + return argsAfterLeadingOptions(argv, () -> usage); + } + + /** + * Like {@link #argsAfterLeadingOptions(String[], String)}, but computes the usage message only if + * it is needed. + * + * @param argv the command-line arguments + * @param usage computes the usage message to print for {@code -h} and {@code --help} + * @return the arguments from the first one that is not an option onward + * @throws Daikon.NormalTermination after printing the usage message, if a leading argument + * requests it + * @throws Daikon.UserError if a leading argument is any other option + */ + public static String[] argsAfterLeadingOptions(String[] argv, Supplier usage) { + // A leading "+" tells Getopt to stop at the first argument that is not an option. + return argsAfterOptions(argv, "+", usage); + } + + /** + * Processes command-line arguments, permitting no options other than {@code -h} and {@code + * --help}. + * + * @param argv the command-line arguments + * @param optstring "" to process all the arguments, or "+" to stop at the first non-option + * @param usage computes the usage message to print for {@code -h} and {@code --help} + * @return the arguments that Getopt did not process as options + * @throws Daikon.NormalTermination after printing the usage message, if an argument requests it + * @throws Daikon.UserError if an argument is any other option + */ + private static String[] argsAfterOptions( + String[] argv, String optstring, Supplier usage) { // Getopt permutes its argument array, so give it a copy to avoid modifying the caller's array. String[] argvCopy = argv.clone(); - // A leading "+" tells Getopt to stop at the first argument that is not an option. - DaikonGetopt g = new DaikonGetopt(argvCopy, "+", new LongOpt[0], usage); + DaikonGetopt g = new DaikonGetopt(argvCopy, optstring, new LongOpt[0], usage); // Every option other than -h and --help is bad, so getopt() returns only -1 or throws. int c = g.getopt(); if (c != -1) { @@ -131,6 +189,17 @@ public static String[] argsAfterLeadingOptions(String[] argv, String usage) { return Arrays.copyOfRange(argvCopy, g.getOptind(), argvCopy.length); } + /** + * Sets the text that is appended to the description of a bad command-line option. The default + * tells the user to run the program with -h. + * + * @param usageHint text appended to the description of a bad command-line option, or null to + * append nothing + */ + public void setUsageHint(@Nullable String usageHint) { + this.usageHint = usageHint; + } + /** * Like {@link Getopt#getopt()}, but never returns {@code '?'} or {@code ':'}. Getopt returns * {@code ':'} for a missing argument if the optstring starts with {@code ':'}. If this was @@ -148,7 +217,7 @@ public int getopt() { throw badOptionError(); } if (c == 'h' && usage != null) { - System.out.println(usage); + System.out.println(usage.get()); throw new Daikon.NormalTermination(); } return c; @@ -162,8 +231,8 @@ public int getopt() { */ private Daikon.UserError badOptionError() { String message = badOptionMessage(); - if (usage != null) { - message += "; " + USAGE_HINT; + if (usageHint != null) { + message += "; " + usageHint; } return new Daikon.UserError(message); } diff --git a/java/daikon/diff/Diff.java b/java/daikon/diff/Diff.java index 13a2315f22..324406bafb 100644 --- a/java/daikon/diff/Diff.java +++ b/java/daikon/diff/Diff.java @@ -413,7 +413,7 @@ public static void mainHelper(final String[] args) v1.printAll(); return; } else { - throw new Daikon.UserError("No invariant files specified; " + DaikonGetopt.USAGE_HINT); + throw new Daikon.UserError("No invariant files specified; run with -h for usage"); } if (logging) { diff --git a/java/daikon/test/BadOptionTest.java b/java/daikon/test/BadOptionTest.java index 5f3a72aace..eeec550cbd 100644 --- a/java/daikon/test/BadOptionTest.java +++ b/java/daikon/test/BadOptionTest.java @@ -20,8 +20,9 @@ /** * Tests that Daikon tools reject bad command-line options with a UserError. * - *

These 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} or {@code Daikon.cleanup}, because they + * reset Daikon's global state, which other tests in the same JVM depend on. TraceSelect checks its + * Daikon arguments without calling either of them. */ public class BadOptionTest { @@ -152,18 +153,35 @@ public void testPrintInvariantsUnrecognizedOption() { /** Tests that TraceSelect rejects bad arguments before doing any sampling. */ @Test public void testTraceSelectBadArguments() { - assertThrows( - Daikon.UserError.class, () -> TraceSelect.mainHelper(new String[] {"0", "10", "x.dtrace"})); - assertThrows( - Daikon.UserError.class, + 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"})); - assertThrows( - Daikon.UserError.class, + assertBadOption( + "sample_size must be a positive integer, not ten", () -> TraceSelect.mainHelper(new String[] {"20", "ten", "x.dtrace"})); - // A misspelled TraceSelect option is a bad Daikon argument. - assertThrows( - Daikon.UserError.class, - () -> TraceSelect.mainHelper(new String[] {"20", "10", "-NOCLAEN", "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())); + // 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())); } /** Tests that tools accept the {@code --help} option that their usage messages document. */ @@ -173,6 +191,7 @@ public void testHelpOption() { 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"})); } diff --git a/java/daikon/tools/TraceSelect.java b/java/daikon/tools/TraceSelect.java index 87bb5829ad..0033cc2a25 100644 --- a/java/daikon/tools/TraceSelect.java +++ b/java/daikon/tools/TraceSelect.java @@ -1,23 +1,24 @@ // TraceSelect.java package daikon.tools; +import static java.nio.charset.StandardCharsets.UTF_8; + import daikon.DaikonGetopt; +import java.io.File; 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; import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Random; import java.util.StringTokenizer; +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,6 +33,15 @@ private TraceSelect() { throw new UnsupportedOperationException("Do not instantiate"); } + /** + * 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,}"); + + /** Matches a negative integer, which is not an option. */ + private static final Pattern NEGATIVE_INTEGER = Pattern.compile("-[0-9]+"); + /** The usage message for this program. */ private static final String usage = StringsPlume.joinLines( @@ -67,11 +77,14 @@ public static void main(String[] args) { * @param args command-line arguments, like those of {@link #main} */ public static void mainHelper(String[] args) { - // Handles -h and --help before num_reps. - args = DaikonGetopt.argsAfterLeadingOptions(args, usage); + // 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); + } if (args.length < 2) { - throw new daikon.Daikon.UserError("Too few arguments; " + DaikonGetopt.USAGE_HINT); + throw new daikon.Daikon.UserError("Too few arguments; run with -h for usage"); } int numReps = parsePositiveInt("num_reps", args[0]); @@ -108,17 +121,31 @@ public static void mainHelper(String[] args) { 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; } } // The remaining arguments are the Daikon arguments, such as .dtrace and .decls files, - // "--nohierarchy", or "--format java". Only the .dtrace file is sampled. + // "--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. String inputFile = null; List daikonArgs = new ArrayList<>(); for (; i < args.length; i++) { - if (args[i].endsWith(".dtrace")) { + 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\""); + } if (inputFile != null) { throw new daikon.Daikon.UserError("Only 1 dtrace file for input allowed"); } @@ -127,36 +154,48 @@ public static void mainHelper(String[] args) { daikonArgs.add(args[i]); } } + + // 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)); + } 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\")"); } - // Check the Daikon arguments before doing any sampling, so that a bad argument (including a - // misspelled TraceSelect option) is reported immediately. This also handles -h and --help. - // The arguments are the same as those of a sample's Daikon run, except for the .dtrace file, - // which does not exist yet. Daikon.mainHelper calls Daikon.cleanup() before each run, which - // undoes the effects of this check. - daikon.Daikon.cleanup(); - daikon.Daikon.read_options(daikonCommandLine(inputFile, daikonArgs), usage); - - // The .inv file for each sample, from the last sample to the first. + // The .inv file for each sample, from the first sample to the last. List sampleNames = new ArrayList<>(); System.out.println("*******Processing********"); try { - for (int rep = numReps; rep > 0; rep--) { + for (int rep = 1; rep <= numReps; rep++) { - List al = selectSample(inputFile, numPerSample, rand, includeUnreturned); + 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()); + } String filePrefix = calcOut(inputFile, rep); sampleNames.add(filePrefix + ".inv"); try { - try (PrintWriter pwOut = new PrintWriter(FilesPlume.newBufferedFileWriter(filePrefix))) { + // 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); } @@ -175,15 +214,15 @@ public static void mainHelper(String[] args) { } if (doDiffs) { - // spinfo format. The arguments are "-p" followed by the samples, first sample first. - List multiDiffArgs = new ArrayList<>(sampleNames); + // spinfo format. The arguments are "-p" followed by the samples. + List multiDiffArgs = new ArrayList<>(); multiDiffArgs.add("-p"); - Collections.reverse(multiDiffArgs); + multiDiffArgs.addAll(sampleNames); daikon.diff.MultiDiff.mainHelper(multiDiffArgs.toArray(new String[0])); } } catch (IOException e) { - throw new UncheckedIOException(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 { @@ -247,7 +286,7 @@ private static int parsePositiveInt(String name, String arg) { } if (result <= 0) { throw new daikon.Daikon.UserError( - name + " must be a positive integer, not " + arg + "; " + DaikonGetopt.USAGE_HINT); + name + " must be a positive integer, not " + arg + "; run with -h for usage"); } return result; } @@ -266,6 +305,31 @@ private static void deleteQuietly(String fileName) { } } + /** + * Checks Daikon command-line arguments by passing them to {@link daikon.Daikon#read_options}. + * Handles -h and --help. + * + *

This does not call {@link daikon.Daikon#cleanup}, so that it does not discard global state + * that the caller may depend on. {@code read_options} sets static fields for the options in + * {@code args}; {@link daikon.Daikon#mainHelper} resets them before it runs. An exception is + * {@code -o}, which {@code read_options} rejects if {@link daikon.Daikon#inv_file} is already + * set. This method clears that field during the check and then restores it. + * + * @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) { + File savedInvFile = daikon.Daikon.inv_file; + daikon.Daikon.inv_file = null; + try { + daikon.Daikon.read_options(args, usage); + } finally { + daikon.Daikon.inv_file = savedInvFile; + } + } + /** * Returns the command line for running Daikon on a sample. * @@ -296,6 +360,8 @@ private static void invokeDaikon(String dtraceName, List daikonArgs) 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"}); } From e8abaccb8662c0f04e093571cde06908da1083e6 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 10 Oct 2026 13:07:05 -0700 Subject: [PATCH 14/30] Move the runtimechecker changes to the runtimechecker-options branch --- .../runtimechecker/InstrumentHandler.java | 19 ++++++++++--------- java/daikon/tools/runtimechecker/Main.java | 6 ------ 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/java/daikon/tools/runtimechecker/InstrumentHandler.java b/java/daikon/tools/runtimechecker/InstrumentHandler.java index 06ed933a62..ffd4e77a48 100644 --- a/java/daikon/tools/runtimechecker/InstrumentHandler.java +++ b/java/daikon/tools/runtimechecker/InstrumentHandler.java @@ -5,11 +5,11 @@ import static java.util.logging.Level.INFO; import daikon.Daikon; -import daikon.DaikonGetopt; import daikon.FileIO; import daikon.Global; import daikon.PptMap; import daikon.tools.jtb.ParseResults; +import gnu.getopt.Getopt; import gnu.getopt.LongOpt; import java.io.File; import java.io.IOException; @@ -198,7 +198,7 @@ private static class Arguments { } } - private static final Arguments errorWhileReadingArguments = + private static Arguments errorWhileReadingArguments = new Arguments("error while reading arguments", new ArrayList()); private Arguments readArguments(String[] args) { @@ -213,7 +213,7 @@ private Arguments readArguments(String[] args) { new LongOpt(directory_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt(checkers_directory_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0) }; - DaikonGetopt g = new DaikonGetopt(args, "", longopts); + Getopt g = new Getopt("daikon.tools.runtimechecker.InstrumentHandler", args, "hs", longopts); int c; while ((c = g.getopt()) != -1) { switch (c) { @@ -236,35 +236,36 @@ private Arguments readArguments(String[] args) { } else if (Daikon.debug_SWITCH.equals(option_name)) { daikon.LogHelper.setLevel(Daikon.getOptarg(g), FINE); } else { - throw new Daikon.BugInDaikon("Unhandled long option " + option_name); + System.err.println("Unknown long option received: " + option_name); } break; default: - throw new Daikon.BugInDaikon("getopt() returned " + c); + System.out.println("unrecognized option" + c); + return errorWhileReadingArguments; } } // The index of the first non-option argument -- the name of the // invariant file. int argindex = g.getOptind(); if (argindex >= args.length) { - System.err.println("Error: No .inv file or .java file arguments supplied."); + System.out.println("Error: No .inv file or .java file arguments supplied."); return errorWhileReadingArguments; } String invfile = args[argindex]; argindex++; if (!(invfile.endsWith(".inv") || invfile.endsWith(".inv.gz"))) { - System.err.println("Error: first argument must be a file ending in .inv or .inv.gz."); + System.out.println("Error: first argument must be a file ending in .inv or .inv.gz."); return errorWhileReadingArguments; } if (argindex >= args.length) { - System.err.println("Error: No .java file arguments supplied."); + System.out.println("Error: No .java file arguments supplied."); return errorWhileReadingArguments; } List javaFileNames = new ArrayList<>(); for (; argindex < args.length; argindex++) { String javafile = args[argindex]; if (!javafile.endsWith(".java")) { - System.err.println("File does not end in .java: " + javafile); + System.out.println("File does not end in .java: " + javafile); return errorWhileReadingArguments; } javaFileNames.add(javafile); diff --git a/java/daikon/tools/runtimechecker/Main.java b/java/daikon/tools/runtimechecker/Main.java index 4f401bc165..9259e07f45 100644 --- a/java/daikon/tools/runtimechecker/Main.java +++ b/java/daikon/tools/runtimechecker/Main.java @@ -1,6 +1,5 @@ package daikon.tools.runtimechecker; -import daikon.Daikon; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -58,11 +57,6 @@ public void nonStaticMain(String[] args) { } } - } catch (Daikon.UserError e) { - // A user error, such as a bad command-line option, is reported in one line. System.exit - // prevents the finally block from printing generic failure text and the usage message. - System.err.println("Error: " + e.getMessage()); - System.exit(1); } catch (Throwable e) { System.out.println("Throwable thrown while handling command:" + e); e.printStackTrace(); From bf87643b033f4e32d8a6f4c15bc0b77515ed962c Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 10 Oct 2026 18:32:58 -0700 Subject: [PATCH 15/30] Reset more static state so that mainHelper can be called repeatedly - Add PrintInvariants.cleanup(), called from Daikon.cleanup(); PrintInvariants.mainHelper calls Daikon.cleanup() before parsing options. - Daikon.setup_proto_invs() replaces, rather than appends to, proto_invs. - Daikon.cleanup() clears spinfoFiles. - print_invariants no longer sets out_stream to System.out. - Reject multiple --output options; don't let a close() failure mask the original exception. - MultiDiff resets MultiDiffVisitor's spinfo mode and output stream when done. - TraceSelect calls MultiDiff.mainHelper, not MultiDiff.main. - Add PrintInvariantsRepeatTest. --- java/daikon/Daikon.java | 9 +- java/daikon/PrintInvariants.java | 75 +++++++++---- java/daikon/diff/MultiDiff.java | 6 +- java/daikon/diff/MultiDiffVisitor.java | 6 + java/daikon/test/AllTestsSuite.java | 1 + .../test/PrintInvariantsRepeatTest.java | 104 ++++++++++++++++++ java/daikon/tools/TraceSelect.java | 4 +- 7 files changed, 182 insertions(+), 23 deletions(-) create mode 100644 java/daikon/test/PrintInvariantsRepeatTest.java diff --git a/java/daikon/Daikon.java b/java/daikon/Daikon.java index df459d8f40..b5cc93e8f6 100644 --- a/java/daikon/Daikon.java +++ b/java/daikon/Daikon.java @@ -982,8 +982,11 @@ public static void cleanup() { server_dir = null; use_mem_monitor = false; userDefinedInvariants.clear(); + spinfoFiles.clear(); proto_invs.clear(); + + PrintInvariants.cleanup(); } // Structure for return value of read_options. @@ -1506,11 +1509,13 @@ public static String getOptarg(Getopt g) { new ArrayList<@ClassGetName String>(); /** - * Creates the list of prototype invariants for all Daikon invariants. New invariants must be - * added to this list. + * Creates the list of prototype invariants for all Daikon invariants, replacing any previous + * contents of {@link #proto_invs}. New invariants must be added to this list. */ public static void setup_proto_invs() { + proto_invs.clear(); + // Unary scalar invariants { // OneOf (OneOf.java.jpp) diff --git a/java/daikon/PrintInvariants.java b/java/daikon/PrintInvariants.java index a562be317e..b7387aa5ca 100644 --- a/java/daikon/PrintInvariants.java +++ b/java/daikon/PrintInvariants.java @@ -55,7 +55,6 @@ import java.util.regex.Pattern; import org.checkerframework.checker.mustcall.qual.Owning; import org.checkerframework.checker.nullness.qual.KeyFor; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.RequiresNonNull; import org.plumelib.util.RegexUtil; @@ -261,19 +260,19 @@ public static String addPrestateExpression(String expr) { // even in that case, which means to output a discard-reason for every // invariant. /** Output discard reasons for this class. If null, output discard reasons for all classes. */ - private static @MonotonicNonNull String discClass = null; + private static @Nullable String discClass = null; /** * Comma-separated variable names. Output discard reasons if those are the variables. If null, * output discard reasons for all variable tuples. */ - private static @MonotonicNonNull String discVars = null; + private static @Nullable String discVars = null; /** * Output discard reasons for this program point. If null, output discard reasons for all program * points. */ - private static @MonotonicNonNull String discPpt = null; + private static @Nullable String discPpt = null; /** The usage message for this program. */ private static String usage = @@ -326,20 +325,55 @@ public static void mainHelper(String[] args) OptionalDataException, IOException, ClassNotFoundException { + // Cleanup from any previous runs. This also calls PrintInvariants.cleanup(). + Daikon.cleanup(); + + Throwable failure = null; try { printInvariantsFromArgs(args); + } catch (Throwable t) { + failure = t; + throw t; } finally { - // Close the output stream if --output was specified, even if printing failed. - // print_invariants sets out_stream to System.out if --output was not specified. - if (out_stream != null) { - OutputStream os = out_stream; - out_stream = null; - if (os == System.out) { - os.flush(); - } else { - os.close(); - } + closeOutputStream(failure); + } + } + + /** + * Resets the static variables that are set by command-line options, so that {@link #mainHelper} + * can be called more than once. Does not close the output stream; {@link #mainHelper} does that. + */ + public static void cleanup() { + ppt_regexp = null; + print_discarded_invariants = false; + wrap_xml = false; + print_csharp_metadata = false; + discClass = null; + discVars = null; + discPpt = null; + } + + /** + * Closes the output stream, if --output was specified. + * + * @param failure the exception that is propagating out of {@link #mainHelper}, or null if none. + * An exception thrown while closing is added to it as a suppressed exception, rather than + * replacing it. + * @throws IOException if closing fails and {@code failure} is null + */ + private static void closeOutputStream(@Nullable Throwable failure) throws IOException { + if (out_stream == null) { + return; + } + OutputStream os = out_stream; + out_stream = null; + try { + os.close(); + } catch (IOException e) { + if (failure == null) { + throw e; } + failure.addSuppressed(e); } } @@ -432,6 +466,12 @@ private static void printInvariantsFromArgs(String[] args) } break; } else if (PrintInvariants.output_SWITCH.equals(option_name)) { + if (out_stream != null) { + throw new Daikon.UserError( + "multiple --" + + PrintInvariants.output_SWITCH + + " options supplied on command line"); + } String output_file = Daikon.getOptarg(g); try { out_stream = new FileOutputStream(output_file); @@ -766,11 +806,8 @@ else if (arg.indexOf('<') != -1) { @RequiresNonNull("FileIO.new_decl_format") public static void print_invariants(PptMap all_ppts) { - if (out_stream == null) { - out_stream = System.out; - } - PrintWriter pw = - new PrintWriter(new BufferedWriter(new OutputStreamWriter(out_stream, UTF_8)), true); + OutputStream os = (out_stream != null) ? out_stream : System.out; + PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(os, UTF_8)), true); if (wrap_xml) { pw.println(""); } diff --git a/java/daikon/diff/MultiDiff.java b/java/daikon/diff/MultiDiff.java index fd6c1ddf61..583e676956 100644 --- a/java/daikon/diff/MultiDiff.java +++ b/java/daikon/diff/MultiDiff.java @@ -55,7 +55,11 @@ public static void mainHelper(final String[] args) catch (IOException e) {e.printStackTrace(); } */ MultiDiffVisitor.setForSpinfoOut(out); - Diff.mainHelper(args); + try { + Diff.mainHelper(args); + } finally { + MultiDiffVisitor.resetSpinfoOut(); + } } } } diff --git a/java/daikon/diff/MultiDiffVisitor.java b/java/daikon/diff/MultiDiffVisitor.java index 82d68acb4a..ed2cf5bade 100644 --- a/java/daikon/diff/MultiDiffVisitor.java +++ b/java/daikon/diff/MultiDiffVisitor.java @@ -46,6 +46,12 @@ public static void setForSpinfoOut(OutputStream out_os) { spinfoMode = true; } + /** Undoes {@link #setForSpinfoOut}: subsequent output goes to System.out, not in spinfo mode. */ + public static void resetSpinfoOut() { + MultiDiffVisitor.out = System.out; + spinfoMode = false; + } + @Override public void visit(RootNode node) { diff --git a/java/daikon/test/AllTestsSuite.java b/java/daikon/test/AllTestsSuite.java index 7d74e7e7ac..5cd4e2f1fc 100644 --- a/java/daikon/test/AllTestsSuite.java +++ b/java/daikon/test/AllTestsSuite.java @@ -34,6 +34,7 @@ daikon.test.TestQuant.class, daikon.test.TestAnnotate.class, daikon.test.DtraceDiffTester.class, + daikon.test.PrintInvariantsRepeatTest.class, // , // // I'm having trouble with this; need to fix, reinstate, and not call // // specially from Makefile. -MDE 7/8/2005 diff --git a/java/daikon/test/PrintInvariantsRepeatTest.java b/java/daikon/test/PrintInvariantsRepeatTest.java new file mode 100644 index 0000000000..d58a878a76 --- /dev/null +++ b/java/daikon/test/PrintInvariantsRepeatTest.java @@ -0,0 +1,104 @@ +package daikon.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import daikon.Daikon; +import daikon.PrintInvariants; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** + * Tests that {@link Daikon#mainHelper} and {@link PrintInvariants#mainHelper} can be called more + * than once in a JVM, without one call's command-line options affecting a later call. + */ +public class PrintInvariantsRepeatTest { + + /** A directory for the input and output files. */ + @ClassRule public static TemporaryFolder tmp = new TemporaryFolder(); + + /** The serialized invariants that Daikon produces from the test input. */ + private static String invFile; + + /** Run Daikon (twice) to create the .inv.gz file. */ + @BeforeClass + public static void setUpClass() throws IOException { + File dtrace = new File(tmp.getRoot(), "Hanoi.dtrace.gz"); + try (InputStream in = + ClassLoader.getSystemResourceAsStream("daikon/test/dtracediff/Hanoi.dtrace.gz")) { + if (in == null) { + throw new Error("Cannot find Hanoi.dtrace.gz"); + } + Files.copy(in, dtrace.toPath()); + } + invFile = new File(tmp.getRoot(), "Hanoi.inv.gz").toString(); + String[] daikonArgs = { + "--" + Daikon.no_text_output_SWITCH, + "--" + Daikon.no_show_progress_SWITCH, + "-o", + invFile, + dtrace.toString() + }; + Daikon.mainHelper(daikonArgs); + Daikon.mainHelper(daikonArgs); + } + + /** + * Runs PrintInvariants with the given options on {@link #invFile}, and returns its output. + * + * @param options command-line options, not including {@code --output} or the input file + * @return the output of PrintInvariants + */ + private static String printInvariants(String... options) throws Exception { + File output = tmp.newFile(); + String[] args = new String[options.length + 3]; + System.arraycopy(options, 0, args, 0, options.length); + args[options.length] = "--output"; + args[options.length + 1] = output.toString(); + args[options.length + 2] = invFile; + PrintInvariants.mainHelper(args); + return new String(Files.readAllBytes(output.toPath()), StandardCharsets.UTF_8); + } + + @Test + public void testRepeatedCalls() throws Exception { + String all = printInvariants(); + assertTrue(all, all.contains("moveDisk")); + assertTrue(all, all.contains("moveTower")); + + // A --ppt-select-pattern must neither cause an error nor filter output in a later call. + String moveDisk = printInvariants("--" + Daikon.ppt_regexp_SWITCH, "moveDisk"); + assertTrue(moveDisk, moveDisk.contains("moveDisk")); + assertFalse(moveDisk, moveDisk.contains("moveTower")); + assertEquals(moveDisk, printInvariants("--" + Daikon.ppt_regexp_SWITCH, "moveDisk")); + + // --wrap_xml from one call must not affect later calls. + String xml = printInvariants("--" + Daikon.wrap_xml_SWITCH); + assertNotEquals(all, xml); + + // Repeating a call, after other calls, produces the same output. + assertEquals(all, printInvariants()); + } + + @Test + public void testMultipleOutputOptions() throws Exception { + String out1 = tmp.newFile().toString(); + String out2 = tmp.newFile().toString(); + assertThrows( + Daikon.UserError.class, + () -> + PrintInvariants.mainHelper(new String[] {"--output", out1, "--output", out2, invFile})); + // A failed call does not affect a later one. + assertTrue(printInvariants().contains("moveDisk")); + } +} diff --git a/java/daikon/tools/TraceSelect.java b/java/daikon/tools/TraceSelect.java index 22f181956b..970fffeed7 100644 --- a/java/daikon/tools/TraceSelect.java +++ b/java/daikon/tools/TraceSelect.java @@ -237,7 +237,7 @@ else if (args[i].endsWith(".dtrace")) { // daikon.diff.Diff.main (sampleNames); // spinfo format - daikon.diff.MultiDiff.main(sampleNames); + daikon.diff.MultiDiff.mainHelper(sampleNames); } // Clean up the mess! A failed cleanup is not fatal. @@ -248,6 +248,8 @@ else if (args[i].endsWith(".dtrace")) { } } + } catch (daikon.Daikon.DaikonTerminationException e) { + throw e; } catch (Exception e) { throw new Error(e); } From 2b46b306889dda6ecce10febf65ecfe4a673be59 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 10 Oct 2026 18:37:28 -0700 Subject: [PATCH 16/30] Address code review: remove unused DaikonGetopt helpers, reject stray arguments in SampleTester and SplitterFactoryTest, fix --user-defined-invariant spelling --- doc/developer.texinfo | 2 +- java/daikon/Daikon.java | 2 +- java/daikon/DaikonGetopt.java | 110 ++---------------- java/daikon/test/SampleTester.java | 4 + .../split/SplitterFactoryTestUpdater.java | 6 + 5 files changed, 19 insertions(+), 105 deletions(-) diff --git a/doc/developer.texinfo b/doc/developer.texinfo index 0951936384..52bbd5591a 100644 --- a/doc/developer.texinfo +++ b/doc/developer.texinfo @@ -259,7 +259,7 @@ call the new invariant's @code{get_proto} method and recompile Daikon. @item If you do not wish to edit the Daikon source code, then compile the new invariant and put its @file{.class} file on your classpath. Then, invoke -Daikon with the @option{--user_defined_invariant} command-line argument +Daikon with the @option{--user-defined-invariant} command-line argument (@pxref{Options to control invariant detection,,,daikon,Daikon User Manual}). @end itemize diff --git a/java/daikon/Daikon.java b/java/daikon/Daikon.java index aaa2b3f3d6..5cde424fb4 100644 --- a/java/daikon/Daikon.java +++ b/java/daikon/Daikon.java @@ -1486,7 +1486,7 @@ public static String getOptarg(Getopt g) { } /** - * Invariants passed on the command line with the {@code --user_defined_invariant} option. A list + * Invariants passed on the command line with the {@code --user-defined-invariant} option. A list * of class names in the format required by {@link Class#forName(String)}. */ private static List<@ClassGetName String> userDefinedInvariants = diff --git a/java/daikon/DaikonGetopt.java b/java/daikon/DaikonGetopt.java index 11a7807ac3..47d387c0f9 100644 --- a/java/daikon/DaikonGetopt.java +++ b/java/daikon/DaikonGetopt.java @@ -4,8 +4,6 @@ import gnu.getopt.Getopt; import gnu.getopt.LongOpt; -import java.util.Arrays; -import java.util.function.Supplier; import org.checkerframework.checker.nullness.qual.Nullable; import org.plumelib.util.ArraysPlume; @@ -24,13 +22,13 @@ public class DaikonGetopt extends Getopt { private static final String PROGNAME = "DaikonGetopt"; /** - * Computes the usage message printed for {@code -h} and {@code --help}, or null if the caller - * handles them. + * The usage message printed for {@code -h} and {@code --help}, or null if the caller handles + * them. */ - private final @Nullable Supplier usage; + private final @Nullable String usage; /** Text appended to the description of a bad command-line option, or null to append nothing. */ - private @Nullable String usageHint = "run with -h for usage"; + private final @Nullable String usageHint; /** * Creates a command-line option processor that recognizes short and long options. Its description @@ -44,7 +42,7 @@ public class DaikonGetopt extends Getopt { public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts) { super(PROGNAME, argv, nonEmpty(optstring), longopts); this.usage = null; - usageHint = null; + this.usageHint = null; opterr = false; } @@ -71,22 +69,9 @@ private static String nonEmpty(String optstring) { * @param usage the usage message to print for {@code -h} and {@code --help} */ public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts, String usage) { - this(argv, optstring, longopts, () -> usage); - } - - /** - * Creates a command-line option processor that recognizes short and long options, plus {@code -h} - * and {@code --help}, which print the usage message and throw {@link Daikon.NormalTermination}. - * The usage message is computed only if it is needed. - * - * @param argv the command-line arguments - * @param optstring the short options, in the format of {@link Getopt}; must not contain {@code h} - * @param longopts the long options; must not contain {@code help} - * @param usage computes the usage message to print for {@code -h} and {@code --help} - */ - public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts, Supplier usage) { super(PROGNAME, argv, withH(optstring), withHelp(longopts)); this.usage = usage; + this.usageHint = "run with -h for usage"; opterr = false; } @@ -119,87 +104,6 @@ private static LongOpt[] withHelp(LongOpt[] longopts) { longopts, new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 'h')); } - /** - * Processes the command-line arguments of a program that takes no options other than {@code -h} - * and {@code --help}. - * - * @param argv the command-line arguments - * @param usage the usage message to print for {@code -h} and {@code --help} - * @return the arguments that are not options - * @throws Daikon.NormalTermination after printing the usage message, if an argument requests it - * @throws Daikon.UserError if an argument is any other option - */ - public static String[] nonOptionArgs(String[] argv, String usage) { - return argsAfterOptions(argv, "", () -> usage); - } - - /** - * Processes the leading command-line arguments of a program that takes no options other than - * {@code -h} and {@code --help}. Processing stops at the first argument that is not an option, so - * later arguments may be options for some other program. - * - * @param argv the command-line arguments - * @param usage the usage message to print for {@code -h} and {@code --help} - * @return the arguments from the first one that is not an option onward - * @throws Daikon.NormalTermination after printing the usage message, if a leading argument - * requests it - * @throws Daikon.UserError if a leading argument is any other option - */ - public static String[] argsAfterLeadingOptions(String[] argv, String usage) { - return argsAfterLeadingOptions(argv, () -> usage); - } - - /** - * Like {@link #argsAfterLeadingOptions(String[], String)}, but computes the usage message only if - * it is needed. - * - * @param argv the command-line arguments - * @param usage computes the usage message to print for {@code -h} and {@code --help} - * @return the arguments from the first one that is not an option onward - * @throws Daikon.NormalTermination after printing the usage message, if a leading argument - * requests it - * @throws Daikon.UserError if a leading argument is any other option - */ - public static String[] argsAfterLeadingOptions(String[] argv, Supplier usage) { - // A leading "+" tells Getopt to stop at the first argument that is not an option. - return argsAfterOptions(argv, "+", usage); - } - - /** - * Processes command-line arguments, permitting no options other than {@code -h} and {@code - * --help}. - * - * @param argv the command-line arguments - * @param optstring "" to process all the arguments, or "+" to stop at the first non-option - * @param usage computes the usage message to print for {@code -h} and {@code --help} - * @return the arguments that Getopt did not process as options - * @throws Daikon.NormalTermination after printing the usage message, if an argument requests it - * @throws Daikon.UserError if an argument is any other option - */ - private static String[] argsAfterOptions( - String[] argv, String optstring, Supplier usage) { - // Getopt permutes its argument array, so give it a copy to avoid modifying the caller's array. - String[] argvCopy = argv.clone(); - DaikonGetopt g = new DaikonGetopt(argvCopy, optstring, new LongOpt[0], usage); - // Every option other than -h and --help is bad, so getopt() returns only -1 or throws. - int c = g.getopt(); - if (c != -1) { - throw new Daikon.BugInDaikon("getopt() returned " + c); - } - return Arrays.copyOfRange(argvCopy, g.getOptind(), argvCopy.length); - } - - /** - * Sets the text that is appended to the description of a bad command-line option. The default - * tells the user to run the program with -h. - * - * @param usageHint text appended to the description of a bad command-line option, or null to - * append nothing - */ - public void setUsageHint(@Nullable String usageHint) { - this.usageHint = usageHint; - } - /** * Like {@link Getopt#getopt()}, but never returns {@code '?'} or {@code ':'}. Getopt returns * {@code ':'} for a missing argument if the optstring starts with {@code ':'}. If this was @@ -217,7 +121,7 @@ public int getopt() { throw badOptionError(); } if (c == 'h' && usage != null) { - System.out.println(usage.get()); + System.out.println(usage); throw new Daikon.NormalTermination(); } return c; diff --git a/java/daikon/test/SampleTester.java b/java/daikon/test/SampleTester.java index b984841454..adde5344b0 100644 --- a/java/daikon/test/SampleTester.java +++ b/java/daikon/test/SampleTester.java @@ -144,6 +144,10 @@ public static void mainHelper(String[] args) throws IOException { throw new Daikon.BugInDaikon("getopt() returned " + c); } } + if (g.getOptind() < args.length) { + throw new Daikon.UserError( + "Unexpected argument " + args[g.getOptind()] + "; run with -h for usage"); + } daikon.LogHelper.setupLogs(Global.debugAll ? FINE : INFO); diff --git a/java/daikon/test/split/SplitterFactoryTestUpdater.java b/java/daikon/test/split/SplitterFactoryTestUpdater.java index 758c93b4a2..b5dfc175e3 100644 --- a/java/daikon/test/split/SplitterFactoryTestUpdater.java +++ b/java/daikon/test/split/SplitterFactoryTestUpdater.java @@ -267,6 +267,12 @@ private static String getTestClassText() { ps.println(" throw new Daikon.BugInDaikon(\"getopt() returned \" + c);"); ps.println(" }"); ps.println(" }"); + ps.println(" if (g.getOptind() < args.length) {"); + ps.println(" throw new Daikon.UserError("); + ps.println( + " \"Unexpected argument \" + args[g.getOptind()] + \"; run with -h for" + + " usage\");"); + ps.println(" }"); ps.println(" Result result = JUnitCore.runClasses(SplitterFactoryTest.class);"); ps.println(" for (Failure failure : result.getFailures()) {"); ps.println(" System.err.println(failure.getTrace());"); From a3de9c719e9287507bca109249bd3cf8396de1a0 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 10 Oct 2026 18:47:25 -0700 Subject: [PATCH 17/30] Drop PrintInvariants repeatability changes; keep only Daikon and MultiDiff fixes --- java/daikon/Daikon.java | 8 +- java/daikon/PrintInvariants.java | 91 +++------------ java/daikon/test/AllTestsSuite.java | 1 - .../test/PrintInvariantsRepeatTest.java | 104 ------------------ 4 files changed, 19 insertions(+), 185 deletions(-) delete mode 100644 java/daikon/test/PrintInvariantsRepeatTest.java diff --git a/java/daikon/Daikon.java b/java/daikon/Daikon.java index b5cc93e8f6..2a8aa2e77c 100644 --- a/java/daikon/Daikon.java +++ b/java/daikon/Daikon.java @@ -985,8 +985,6 @@ public static void cleanup() { spinfoFiles.clear(); proto_invs.clear(); - - PrintInvariants.cleanup(); } // Structure for return value of read_options. @@ -1509,13 +1507,11 @@ public static String getOptarg(Getopt g) { new ArrayList<@ClassGetName String>(); /** - * Creates the list of prototype invariants for all Daikon invariants, replacing any previous - * contents of {@link #proto_invs}. New invariants must be added to this list. + * Creates the list of prototype invariants for all Daikon invariants. New invariants must be + * added to this list. */ public static void setup_proto_invs() { - proto_invs.clear(); - // Unary scalar invariants { // OneOf (OneOf.java.jpp) diff --git a/java/daikon/PrintInvariants.java b/java/daikon/PrintInvariants.java index b7387aa5ca..f7bc9121c3 100644 --- a/java/daikon/PrintInvariants.java +++ b/java/daikon/PrintInvariants.java @@ -55,6 +55,7 @@ import java.util.regex.Pattern; import org.checkerframework.checker.mustcall.qual.Owning; import org.checkerframework.checker.nullness.qual.KeyFor; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.RequiresNonNull; import org.plumelib.util.RegexUtil; @@ -260,19 +261,19 @@ public static String addPrestateExpression(String expr) { // even in that case, which means to output a discard-reason for every // invariant. /** Output discard reasons for this class. If null, output discard reasons for all classes. */ - private static @Nullable String discClass = null; + private static @MonotonicNonNull String discClass = null; /** * Comma-separated variable names. Output discard reasons if those are the variables. If null, * output discard reasons for all variable tuples. */ - private static @Nullable String discVars = null; + private static @MonotonicNonNull String discVars = null; /** * Output discard reasons for this program point. If null, output discard reasons for all program * points. */ - private static @Nullable String discPpt = null; + private static @MonotonicNonNull String discPpt = null; /** The usage message for this program. */ private static String usage = @@ -325,69 +326,6 @@ public static void mainHelper(String[] args) OptionalDataException, IOException, ClassNotFoundException { - // Cleanup from any previous runs. This also calls PrintInvariants.cleanup(). - Daikon.cleanup(); - - Throwable failure = null; - try { - printInvariantsFromArgs(args); - } catch (Throwable t) { - failure = t; - throw t; - } finally { - closeOutputStream(failure); - } - } - - /** - * Resets the static variables that are set by command-line options, so that {@link #mainHelper} - * can be called more than once. Does not close the output stream; {@link #mainHelper} does that. - */ - public static void cleanup() { - ppt_regexp = null; - print_discarded_invariants = false; - wrap_xml = false; - print_csharp_metadata = false; - discClass = null; - discVars = null; - discPpt = null; - } - - /** - * Closes the output stream, if --output was specified. - * - * @param failure the exception that is propagating out of {@link #mainHelper}, or null if none. - * An exception thrown while closing is added to it as a suppressed exception, rather than - * replacing it. - * @throws IOException if closing fails and {@code failure} is null - */ - private static void closeOutputStream(@Nullable Throwable failure) throws IOException { - if (out_stream == null) { - return; - } - OutputStream os = out_stream; - out_stream = null; - try { - os.close(); - } catch (IOException e) { - if (failure == null) { - throw e; - } - failure.addSuppressed(e); - } - } - - /** - * Does the work of {@link #mainHelper}, except closing the output stream. - * - * @param args command-line arguments, like those of {@link #main} - */ - private static void printInvariantsFromArgs(String[] args) - throws FileNotFoundException, - StreamCorruptedException, - OptionalDataException, - IOException, - ClassNotFoundException { LongOpt[] longopts = new LongOpt[] { @@ -466,12 +404,6 @@ private static void printInvariantsFromArgs(String[] args) } break; } else if (PrintInvariants.output_SWITCH.equals(option_name)) { - if (out_stream != null) { - throw new Daikon.UserError( - "multiple --" - + PrintInvariants.output_SWITCH - + " options supplied on command line"); - } String output_file = Daikon.getOptarg(g); try { out_stream = new FileOutputStream(output_file); @@ -560,6 +492,14 @@ private static void printInvariantsFromArgs(String[] args) } print_invariants(ppts); + + // Close the output stream if --output was specified. + if (out_stream != null) { + out_stream.flush(); + assert out_stream != null + : "@AssumeAssertion(nullness): flush() does not affect any global variables"; + out_stream.close(); + } } /** @@ -806,8 +746,11 @@ else if (arg.indexOf('<') != -1) { @RequiresNonNull("FileIO.new_decl_format") public static void print_invariants(PptMap all_ppts) { - OutputStream os = (out_stream != null) ? out_stream : System.out; - PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(os, UTF_8)), true); + if (out_stream == null) { + out_stream = System.out; + } + PrintWriter pw = + new PrintWriter(new BufferedWriter(new OutputStreamWriter(out_stream, UTF_8)), true); if (wrap_xml) { pw.println(""); } diff --git a/java/daikon/test/AllTestsSuite.java b/java/daikon/test/AllTestsSuite.java index 5cd4e2f1fc..7d74e7e7ac 100644 --- a/java/daikon/test/AllTestsSuite.java +++ b/java/daikon/test/AllTestsSuite.java @@ -34,7 +34,6 @@ daikon.test.TestQuant.class, daikon.test.TestAnnotate.class, daikon.test.DtraceDiffTester.class, - daikon.test.PrintInvariantsRepeatTest.class, // , // // I'm having trouble with this; need to fix, reinstate, and not call // // specially from Makefile. -MDE 7/8/2005 diff --git a/java/daikon/test/PrintInvariantsRepeatTest.java b/java/daikon/test/PrintInvariantsRepeatTest.java deleted file mode 100644 index d58a878a76..0000000000 --- a/java/daikon/test/PrintInvariantsRepeatTest.java +++ /dev/null @@ -1,104 +0,0 @@ -package daikon.test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; - -import daikon.Daikon; -import daikon.PrintInvariants; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import org.junit.BeforeClass; -import org.junit.ClassRule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -/** - * Tests that {@link Daikon#mainHelper} and {@link PrintInvariants#mainHelper} can be called more - * than once in a JVM, without one call's command-line options affecting a later call. - */ -public class PrintInvariantsRepeatTest { - - /** A directory for the input and output files. */ - @ClassRule public static TemporaryFolder tmp = new TemporaryFolder(); - - /** The serialized invariants that Daikon produces from the test input. */ - private static String invFile; - - /** Run Daikon (twice) to create the .inv.gz file. */ - @BeforeClass - public static void setUpClass() throws IOException { - File dtrace = new File(tmp.getRoot(), "Hanoi.dtrace.gz"); - try (InputStream in = - ClassLoader.getSystemResourceAsStream("daikon/test/dtracediff/Hanoi.dtrace.gz")) { - if (in == null) { - throw new Error("Cannot find Hanoi.dtrace.gz"); - } - Files.copy(in, dtrace.toPath()); - } - invFile = new File(tmp.getRoot(), "Hanoi.inv.gz").toString(); - String[] daikonArgs = { - "--" + Daikon.no_text_output_SWITCH, - "--" + Daikon.no_show_progress_SWITCH, - "-o", - invFile, - dtrace.toString() - }; - Daikon.mainHelper(daikonArgs); - Daikon.mainHelper(daikonArgs); - } - - /** - * Runs PrintInvariants with the given options on {@link #invFile}, and returns its output. - * - * @param options command-line options, not including {@code --output} or the input file - * @return the output of PrintInvariants - */ - private static String printInvariants(String... options) throws Exception { - File output = tmp.newFile(); - String[] args = new String[options.length + 3]; - System.arraycopy(options, 0, args, 0, options.length); - args[options.length] = "--output"; - args[options.length + 1] = output.toString(); - args[options.length + 2] = invFile; - PrintInvariants.mainHelper(args); - return new String(Files.readAllBytes(output.toPath()), StandardCharsets.UTF_8); - } - - @Test - public void testRepeatedCalls() throws Exception { - String all = printInvariants(); - assertTrue(all, all.contains("moveDisk")); - assertTrue(all, all.contains("moveTower")); - - // A --ppt-select-pattern must neither cause an error nor filter output in a later call. - String moveDisk = printInvariants("--" + Daikon.ppt_regexp_SWITCH, "moveDisk"); - assertTrue(moveDisk, moveDisk.contains("moveDisk")); - assertFalse(moveDisk, moveDisk.contains("moveTower")); - assertEquals(moveDisk, printInvariants("--" + Daikon.ppt_regexp_SWITCH, "moveDisk")); - - // --wrap_xml from one call must not affect later calls. - String xml = printInvariants("--" + Daikon.wrap_xml_SWITCH); - assertNotEquals(all, xml); - - // Repeating a call, after other calls, produces the same output. - assertEquals(all, printInvariants()); - } - - @Test - public void testMultipleOutputOptions() throws Exception { - String out1 = tmp.newFile().toString(); - String out2 = tmp.newFile().toString(); - assertThrows( - Daikon.UserError.class, - () -> - PrintInvariants.mainHelper(new String[] {"--output", out1, "--output", out2, invFile})); - // A failed call does not affect a later one. - assertTrue(printInvariants().contains("moveDisk")); - } -} From 50e3e2aa99ed1713b7f585257aa1fdfd49967a69 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 10 Oct 2026 18:56:22 -0700 Subject: [PATCH 18/30] Simplify DaikonGetopt: one constructor, which always handles -h and --help --- java/daikon/DaikonGetopt.java | 109 ++++------------------------ java/daikon/test/BadOptionTest.java | 64 ++++++++-------- 2 files changed, 48 insertions(+), 125 deletions(-) diff --git a/java/daikon/DaikonGetopt.java b/java/daikon/DaikonGetopt.java index 47d387c0f9..26bf668530 100644 --- a/java/daikon/DaikonGetopt.java +++ b/java/daikon/DaikonGetopt.java @@ -4,15 +4,13 @@ import gnu.getopt.Getopt; import gnu.getopt.LongOpt; -import org.checkerframework.checker.nullness.qual.Nullable; import org.plumelib.util.ArraysPlume; /** * A command-line option processor that throws a {@link Daikon.UserError} that describes a bad - * command-line option, instead of printing a message and returning {@code '?'} or {@code ':'}. - * - *

If constructed with a usage message, it also handles {@code -h} and {@code --help}: it prints - * the usage message and throws {@link Daikon.NormalTermination}. + * command-line option, instead of printing a message and returning {@code '?'} or {@code ':'}. It + * also handles {@code -h} and {@code --help}: it prints the usage message and throws {@link + * Daikon.NormalTermination}. */ public class DaikonGetopt extends Getopt { @@ -21,43 +19,8 @@ public class DaikonGetopt extends Getopt { */ private static final String PROGNAME = "DaikonGetopt"; - /** - * The usage message printed for {@code -h} and {@code --help}, or null if the caller handles - * them. - */ - private final @Nullable String usage; - - /** Text appended to the description of a bad command-line option, or null to append nothing. */ - private final @Nullable String usageHint; - - /** - * Creates a command-line option processor that recognizes short and long options. Its description - * of a bad option does not suggest running with {@code -h}, because it does not handle {@code - * -h}. - * - * @param argv the command-line arguments - * @param optstring the short options, in the format of {@link Getopt} - * @param longopts the long options - */ - public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts) { - super(PROGNAME, argv, nonEmpty(optstring), longopts); - this.usage = null; - this.usageHint = null; - opterr = false; - } - - /** - * Returns short options that are equivalent to the given ones but are not empty. Getopt replaces - * an empty optstring by " ", which makes "- " a valid option. - * - * @param optstring short options, in the format of {@link Getopt} - * @return {@code optstring}, or ":" if {@code optstring} is empty - */ - private static String nonEmpty(String optstring) { - // A leading ':' only makes Getopt return ':' rather than '?' for a missing argument, which - // getopt() treats identically. - return optstring.isEmpty() ? ":" : optstring; - } + /** The usage message printed for {@code -h} and {@code --help}. */ + private final String usage; /** * Creates a command-line option processor that recognizes short and long options, plus {@code -h} @@ -69,78 +32,38 @@ private static String nonEmpty(String optstring) { * @param usage the usage message to print for {@code -h} and {@code --help} */ public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts, String usage) { - super(PROGNAME, argv, withH(optstring), withHelp(longopts)); + super( + PROGNAME, + argv, + optstring + "h", + ArraysPlume.append( + longopts, new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 'h'))); this.usage = usage; - this.usageHint = "run with -h for usage"; opterr = false; } /** - * Returns the given short options plus {@code -h}. - * - * @param optstring short options, in the format of {@link Getopt}; must not contain {@code h} - * @return {@code optstring} plus {@code -h} - */ - private static String withH(String optstring) { - if (optstring.indexOf('h') != -1) { - throw new IllegalArgumentException("optstring already contains h: " + optstring); - } - return optstring + "h"; - } - - /** - * Returns the given long options plus {@code --help}. - * - * @param longopts long options; must not contain {@code help} - * @return {@code longopts} plus {@code --help} - */ - private static LongOpt[] withHelp(LongOpt[] longopts) { - for (LongOpt longopt : longopts) { - if (longopt.getName().equals(Daikon.help_SWITCH)) { - throw new IllegalArgumentException("longopts already contains " + Daikon.help_SWITCH); - } - } - return ArraysPlume.append( - longopts, new LongOpt(Daikon.help_SWITCH, LongOpt.NO_ARGUMENT, null, 'h')); - } - - /** - * Like {@link Getopt#getopt()}, but never returns {@code '?'} or {@code ':'}. Getopt returns - * {@code ':'} for a missing argument if the optstring starts with {@code ':'}. If this was - * constructed with a usage message, also never returns {@code 'h'}. + * Like {@link Getopt#getopt()}, but never returns {@code '?'}, {@code ':'}, or {@code 'h'}. + * Getopt returns {@code ':'} for a missing argument if the optstring starts with {@code ':'}. * * @return the next option, as described in {@link Getopt#getopt()} * @throws Daikon.UserError if the next option is unrecognized, ambiguous, or malformed * @throws Daikon.NormalTermination after printing the usage message, if the next option is {@code - * -h} or {@code --help} and this was constructed with a usage message + * -h} or {@code --help} */ @Override public int getopt() { int c = super.getopt(); if (c == '?' || c == ':') { - throw badOptionError(); + throw new Daikon.UserError(badOptionMessage() + "; run with -h for usage"); } - if (c == 'h' && usage != null) { + if (c == 'h') { System.out.println(usage); throw new Daikon.NormalTermination(); } return c; } - /** - * Returns an exception that describes the bad command-line option that {@link Getopt#getopt()} - * just rejected. - * - * @return an exception that describes the bad command-line option - */ - private Daikon.UserError badOptionError() { - String message = badOptionMessage(); - if (usageHint != null) { - message += "; " + usageHint; - } - return new Daikon.UserError(message); - } - /** * Returns a description of the bad command-line option that {@link Getopt#getopt()} just * rejected. diff --git a/java/daikon/test/BadOptionTest.java b/java/daikon/test/BadOptionTest.java index 78f76e8159..0f50ea31e2 100644 --- a/java/daikon/test/BadOptionTest.java +++ b/java/daikon/test/BadOptionTest.java @@ -4,6 +4,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import daikon.Daikon; import daikon.DaikonGetopt; @@ -24,27 +25,43 @@ */ public class BadOptionTest { - /** The long options used by {@link #parse}. */ + /** The long options used by {@link #parse}. DaikonGetopt adds {@code --help}. */ private static final LongOpt[] longopts = new LongOpt[] { - new LongOpt("help", LongOpt.NO_ARGUMENT, null, 0), new LongOpt("config", LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt("config_option", LongOpt.REQUIRED_ARGUMENT, null, 0), new LongOpt("debug", LongOpt.REQUIRED_ARGUMENT, null, 'd'), new LongOpt("verbose", LongOpt.NO_ARGUMENT, null, 'v'), }; + /** The suffix of every description of a bad command-line option. */ + private static final String USAGE_HINT = "; run with -h for usage"; + /** * Parses the given command-line arguments, which must contain a bad option, and returns the - * resulting error message. + * resulting error message, without its suffix that suggests running with -h. * + * @param optstring the short options, in the format of Getopt * @param args command-line arguments - * @return the message of the UserError for the bad option + * @return the message of the UserError for the bad option, without {@link #USAGE_HINT} */ - private static String parse(String... args) { - DaikonGetopt g = new DaikonGetopt(args, "ho:", longopts); + private static String parseWith(String optstring, String... args) { + DaikonGetopt g = new DaikonGetopt(args, optstring, longopts, "usage"); Daikon.UserError e = assertThrows(Daikon.UserError.class, () -> consumeAll(g)); - return String.valueOf(e.getMessage()); + String message = String.valueOf(e.getMessage()); + assertTrue(message, message.endsWith(USAGE_HINT)); + return message.substring(0, message.length() - USAGE_HINT.length()); + } + + /** + * Parses the given command-line arguments, which must contain a bad option, and returns the + * resulting error message, without its suffix that suggests running with -h. + * + * @param args command-line arguments + * @return the message of the UserError for the bad option, without {@link #USAGE_HINT} + */ + private static String parse(String... args) { + return parseWith("vo:", args); } /** @@ -62,15 +79,15 @@ public void testUnrecognizedLongOption() { assertEquals("Unrecognized command-line option --bogus", parse("--bogus")); assertEquals("Unrecognized command-line option --bogus", parse("--bogus=3")); assertEquals("Unrecognized command-line option --=3", parse("--=3")); - assertEquals("Unrecognized command-line option --bogus", parse("-h", "--bogus", "file")); + assertEquals("Unrecognized command-line option --bogus", parse("-v", "--bogus", "file")); } /** Tests an unrecognized short option, alone and combined with a valid one. */ @Test public void testUnrecognizedShortOption() { assertEquals("Unrecognized command-line option -x", parse("-x")); - assertEquals("Unrecognized command-line option -x", parse("-xh")); - assertEquals("Unrecognized command-line option -x", parse("-hx")); + assertEquals("Unrecognized command-line option -x", parse("-xv")); + assertEquals("Unrecognized command-line option -x", parse("-vx")); assertEquals("Unrecognized command-line option -:", parse("-:")); } @@ -78,9 +95,7 @@ public void testUnrecognizedShortOption() { @Test public void testNoShortOptions() { for (String arg : new String[] {"- ", "-x", "-:"}) { - DaikonGetopt g = new DaikonGetopt(new String[] {arg}, "", longopts); - Daikon.UserError e = assertThrows(Daikon.UserError.class, g::getopt); - assertEquals("Unrecognized command-line option " + arg, String.valueOf(e.getMessage())); + assertEquals("Unrecognized command-line option " + arg, parseWith("", arg)); } } @@ -91,35 +106,20 @@ public void testAmbiguousLongOption() { assertEquals("Ambiguous command-line option --conf", parse("--conf")); } - /** Tests that the description of a bad option suggests -h if DaikonGetopt handles -h. */ - @Test - public void testUsageHint() { - DaikonGetopt g = new DaikonGetopt(new String[] {"--bogus"}, "o:", new LongOpt[0], "usage"); - Daikon.UserError e = assertThrows(Daikon.UserError.class, g::getopt); - assertEquals( - "Unrecognized command-line option --bogus; run with -h for usage", - String.valueOf(e.getMessage())); - } - /** Tests options that are missing their required argument. */ @Test public void testMissingArgument() { assertEquals("Command-line option --config requires an argument", parse("--config")); assertEquals("Command-line option --debug requires an argument", parse("--deb")); assertEquals("Command-line option -o requires an argument", parse("-o")); - assertEquals("Command-line option -o requires an argument", parse("-ho")); + assertEquals("Command-line option -o requires an argument", parse("-vo")); } /** Tests options that are missing their required argument, when the optstring starts with ':'. */ @Test public void testMissingArgumentLeadingColon() { - DaikonGetopt g = new DaikonGetopt(new String[] {"-o"}, ":ho:", longopts); - Daikon.UserError e = assertThrows(Daikon.UserError.class, g::getopt); - assertEquals("Command-line option -o requires an argument", String.valueOf(e.getMessage())); - g = new DaikonGetopt(new String[] {"--config"}, ":ho:", longopts); - e = assertThrows(Daikon.UserError.class, g::getopt); - assertEquals( - "Command-line option --config requires an argument", String.valueOf(e.getMessage())); + assertEquals("Command-line option -o requires an argument", parseWith(":o:", "-o")); + assertEquals("Command-line option --config requires an argument", parseWith(":o:", "--config")); } /** Tests long options that are given an argument they do not take. */ @@ -137,7 +137,7 @@ public void testUnexpectedArgument() { */ private static void assertBadOption(String description, ThrowingRunnable mainCall) { Daikon.UserError e = assertThrows(Daikon.UserError.class, mainCall); - assertEquals(description + "; run with -h for usage", String.valueOf(e.getMessage())); + assertEquals(description + USAGE_HINT, String.valueOf(e.getMessage())); } /** Tests an unrecognized long option in a tool. */ From 84d0a6014082554377951ff23913f32b627e3abc Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 10 Oct 2026 18:59:24 -0700 Subject: [PATCH 19/30] TraceSelect calls Daikon.mainHelper, not Daikon.main; remove reinitializeDaikon --- java/daikon/tools/TraceSelect.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/java/daikon/tools/TraceSelect.java b/java/daikon/tools/TraceSelect.java index 970fffeed7..b40d181de0 100644 --- a/java/daikon/tools/TraceSelect.java +++ b/java/daikon/tools/TraceSelect.java @@ -292,9 +292,7 @@ private static void invokeDaikon(String dtraceName) throws IOException { // create an array to store the Strings in daikonArgsList String[] daikonArgs = daikonArgsList.toArray(new String[0]); - // initializes daikon again or else an exception is thrown - reinitializeDaikon(); - daikon.Daikon.main(daikonArgs); + 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")); @@ -311,10 +309,6 @@ private static void invokeDaikon(String dtraceName) throws IOException { } } - private static void reinitializeDaikon() { - daikon.Daikon.inv_file = null; - } - private static String calcOut(String strFileName) { StringBuilder product = new StringBuilder(); int index = strFileName.indexOf('.'); From 7df7f238d03bb540bc3b3730e619b4570453a062 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 10 Oct 2026 18:59:32 -0700 Subject: [PATCH 20/30] Move test-driver fixes and doc spelling fixes to separate pull requests --- doc/developer.texinfo | 2 +- java/daikon/Daikon.java | 2 +- java/daikon/test/SampleTester.java | 29 +----------- .../split/SplitterFactoryTestUpdater.java | 44 +++++-------------- scripts/runcluster.pl | 2 +- scripts/trace-purge-vars.pl | 2 +- 6 files changed, 16 insertions(+), 65 deletions(-) diff --git a/doc/developer.texinfo b/doc/developer.texinfo index 52bbd5591a..0951936384 100644 --- a/doc/developer.texinfo +++ b/doc/developer.texinfo @@ -259,7 +259,7 @@ call the new invariant's @code{get_proto} method and recompile Daikon. @item If you do not wish to edit the Daikon source code, then compile the new invariant and put its @file{.class} file on your classpath. Then, invoke -Daikon with the @option{--user-defined-invariant} command-line argument +Daikon with the @option{--user_defined_invariant} command-line argument (@pxref{Options to control invariant detection,,,daikon,Daikon User Manual}). @end itemize diff --git a/java/daikon/Daikon.java b/java/daikon/Daikon.java index 5cde424fb4..aaa2b3f3d6 100644 --- a/java/daikon/Daikon.java +++ b/java/daikon/Daikon.java @@ -1486,7 +1486,7 @@ public static String getOptarg(Getopt g) { } /** - * Invariants passed on the command line with the {@code --user-defined-invariant} option. A list + * Invariants passed on the command line with the {@code --user_defined_invariant} option. A list * of class names in the format required by {@link Class#forName(String)}. */ private static List<@ClassGetName String> userDefinedInvariants = diff --git a/java/daikon/test/SampleTester.java b/java/daikon/test/SampleTester.java index adde5344b0..7373ba7cdf 100644 --- a/java/daikon/test/SampleTester.java +++ b/java/daikon/test/SampleTester.java @@ -67,7 +67,7 @@ public class SampleTester { /** The usage message for this program. */ private static String usage = StringsPlume.joinLines( - "Usage: java daikon.test.SampleTester [OPTION]...", + "Usage: java daikon.PrintInvariants [OPTION]... FILE", " -h, --" + Daikon.help_SWITCH, " Display this usage message", " --" + Daikon.config_option_SWITCH, @@ -78,29 +78,8 @@ public class SampleTester { " Specify a class, varinfos, and ppt to debug track.", " Format is class@ppt"); - /** - * Runs the tests without JUnit. - * - * @param args command-line arguments, like those of {@link #mainHelper} - * @throws IOException if there is a problem reading a test file - */ + @SuppressWarnings("JUnitMethodInvoked") // main() runs the tests without JUnit public static void main(String[] args) throws IOException { - try { - mainHelper(args); - } catch (Daikon.DaikonTerminationException e) { - Daikon.handleDaikonTerminationException(e); - } - } - - /** - * This does the work of {@link #main(String[])}, but it never calls System.exit, so it is - * appropriate to be called programmatically. - * - * @param args command-line arguments, like those of {@link #main} - * @throws IOException if there is a problem reading a test file - */ - @SuppressWarnings("JUnitMethodInvoked") // mainHelper() runs the tests without JUnit - public static void mainHelper(String[] args) throws IOException { LongOpt[] longopts = new LongOpt[] { @@ -144,10 +123,6 @@ public static void mainHelper(String[] args) throws IOException { throw new Daikon.BugInDaikon("getopt() returned " + c); } } - if (g.getOptind() < args.length) { - throw new Daikon.UserError( - "Unexpected argument " + args[g.getOptind()] + "; run with -h for usage"); - } daikon.LogHelper.setupLogs(Global.debugAll ? FINE : INFO); diff --git a/java/daikon/test/split/SplitterFactoryTestUpdater.java b/java/daikon/test/split/SplitterFactoryTestUpdater.java index b5dfc175e3..5025e8c145 100644 --- a/java/daikon/test/split/SplitterFactoryTestUpdater.java +++ b/java/daikon/test/split/SplitterFactoryTestUpdater.java @@ -195,11 +195,7 @@ private static String getTestClassText() { ps.println("import java.io.*;"); ps.println("import java.util.*;"); ps.println("import junit.framework.*;"); - ps.println("import org.junit.BeforeClass;"); ps.println("import org.junit.Test;"); - ps.println("import org.junit.runner.JUnitCore;"); - ps.println("import org.junit.runner.Result;"); - ps.println("import org.junit.runner.notification.Failure;"); ps.println("import org.plumelib.util.FilesPlume;"); ps.println("import org.plumelib.util.StringsPlume;"); ps.println("import org.plumelib.util.UtilPlume;"); @@ -233,6 +229,11 @@ private static String getTestClassText() { ps.println(" // java files it produces, changing the order that the setUpTests"); ps.println(" // commands are run will cause the tests to fail."); ps.println(); + ps.println(" /** Do not instantiate. */"); + ps.println(" private SplitterFactoryTest() {"); + ps.println(" throw new Error(\"Do not instantiate\");"); + ps.println(" }"); + ps.println(); ps.println(" private static String targetDir = \"" + targetDir + "\";"); ps.println(); ps.println(" private static @Nullable String tempDir = null;"); @@ -241,21 +242,12 @@ private static String getTestClassText() { ps.println(); ps.println(" private static String usage ="); ps.println(" StringsPlume.joinLines("); - ps.println(" \"Usage: java daikon.test.split.SplitterFactoryTest [OPTION]...\","); + ps.println(" \"Usage: java daikon.tools.CreateSpinfo FILE.java ...\","); ps.println( - " \" -s Save (do not delete) the splitter java files in the temp" - + " directory\","); - ps.println(" \" -h, --help Display this usage message\");"); + " \" -s Save (do not delete) the splitter java files in the temp directory\","); + ps.println(" \" -h Display this usage message\");"); ps.println(); ps.println(" public static void main(String[] args) {"); - ps.println(" try {"); - ps.println(" mainHelper(args);"); - ps.println(" } catch (Daikon.DaikonTerminationException e) {"); - ps.println(" Daikon.handleDaikonTerminationException(e);"); - ps.println(" }"); - ps.println(" }"); - ps.println(); - ps.println(" public static void mainHelper(String[] args) {"); ps.println(" DaikonGetopt g = new DaikonGetopt(args, \"s\", new LongOpt[0], usage);"); ps.println(" int c;"); ps.println(" while ((c = g.getopt()) != -1) {"); @@ -267,21 +259,6 @@ private static String getTestClassText() { ps.println(" throw new Daikon.BugInDaikon(\"getopt() returned \" + c);"); ps.println(" }"); ps.println(" }"); - ps.println(" if (g.getOptind() < args.length) {"); - ps.println(" throw new Daikon.UserError("); - ps.println( - " \"Unexpected argument \" + args[g.getOptind()] + \"; run with -h for" - + " usage\");"); - ps.println(" }"); - ps.println(" Result result = JUnitCore.runClasses(SplitterFactoryTest.class);"); - ps.println(" for (Failure failure : result.getFailures()) {"); - ps.println(" System.err.println(failure.getTrace());"); - ps.println(" }"); - ps.println(" if (!result.wasSuccessful()) {"); - ps.println( - " throw new Daikon.UserError(StringsPlume.nPlural(result.getFailureCount(), \"test\")" - + " + \" failed\");"); - ps.println(" }"); ps.println(" }"); ps.println(); @@ -335,8 +312,7 @@ private static String getTestClassText() { * SplitterFactoryTest to set up the needed files to run the tests on. */ public static void appendSetUpTest(PrintStream ps) { - ps.println(" @BeforeClass"); - ps.println(" public static void setUpTests() {"); + ps.println(" private static void setUpTests() {"); ps.println(" List spinfoFiles;"); ps.println(" List declsFiles;"); for (int i = 0; i < spinfoFileLists.size(); i++) { @@ -366,7 +342,7 @@ private static void appendTests(PrintStream ps) { ps.println(); for (String className : classNames) { ps.println(" @Test"); - ps.println(" public void test" + className + "() {"); + ps.println(" public static void test" + className + "() {"); ps.println(" assertEqualFiles(\"" + className + ".java\");"); ps.println(" }"); ps.println(); diff --git a/scripts/runcluster.pl b/scripts/runcluster.pl index 12559cd84c..bf31f6de12 100755 --- a/scripts/runcluster.pl +++ b/scripts/runcluster.pl @@ -209,7 +209,7 @@ () } my $invfile = "runcluster_temp_$algorithm-$ncluster.inv"; -$command = "java -cp $SCRIPTDIR/../daikon.jar -Xmx7g daikon.Daikon -o $invfile --config_option daikon.PptTopLevel.pairwise_implications=true --var-omit-pattern=\"class\" --no_text_output --no_show_progress $spinfo_file $decls_new " . join(' ', @new_dtraces) . " 2>&1 > runcluster_temp_Daikon_output.txt"; +$command = "java -cp $SCRIPTDIR/../daikon.jar -Xmx7g daikon.Daikon -o $invfile --config_option daikon.PptTopLevel.pairwise_implications=true --var_omit_pattern=\"class\" --no_text_output --no_show_progress $spinfo_file $decls_new " . join(' ', @new_dtraces) . " 2>&1 > runcluster_temp_Daikon_output.txt"; system_or_die($command, $verbose); $invfile =~ /(.*)\.inv/; diff --git a/scripts/trace-purge-vars.pl b/scripts/trace-purge-vars.pl index c75fdc9a7e..f3b95d73d9 100755 --- a/scripts/trace-purge-vars.pl +++ b/scripts/trace-purge-vars.pl @@ -8,7 +8,7 @@ # regular expression. (Its name is taken from grep.) # This script reduces the size of a trace file, but typically, using -# Daikon's --var-omit-pattern flag is a better option. +# Daikon's --var_omit_pattern flag is a better option. # I'm intentionally not operating by paragraphs, because I want the regexp # to only apply to the variable lines. From 4f54694b9d02587021bb74eaecf194324bf0d5df Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 10 Oct 2026 19:10:01 -0700 Subject: [PATCH 21/30] Handle DaikonTerminationException in SampleTester and SplitterFactoryTest main methods --- java/daikon/test/SampleTester.java | 23 ++++++++++++++++++- .../split/SplitterFactoryTestUpdater.java | 20 ++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/java/daikon/test/SampleTester.java b/java/daikon/test/SampleTester.java index 7373ba7cdf..ee6c5eda2e 100644 --- a/java/daikon/test/SampleTester.java +++ b/java/daikon/test/SampleTester.java @@ -78,8 +78,29 @@ public class SampleTester { " Specify a class, varinfos, and ppt to debug track.", " Format is class@ppt"); - @SuppressWarnings("JUnitMethodInvoked") // main() runs the tests without JUnit + /** + * Runs the sample tests. + * + * @param args command-line arguments + * @throws IOException if there is trouble reading a file + */ public static void main(String[] args) throws IOException { + try { + mainHelper(args); + } catch (Daikon.DaikonTerminationException e) { + Daikon.handleDaikonTerminationException(e); + } + } + + /** + * Does the work of {@link #main}, but throws an exception rather than calling {@code + * System.exit}. + * + * @param args command-line arguments + * @throws IOException if there is trouble reading a file + */ + @SuppressWarnings("JUnitMethodInvoked") // mainHelper() runs the tests without JUnit + public static void mainHelper(String[] args) throws IOException { LongOpt[] longopts = new LongOpt[] { diff --git a/java/daikon/test/split/SplitterFactoryTestUpdater.java b/java/daikon/test/split/SplitterFactoryTestUpdater.java index 5025e8c145..5391293804 100644 --- a/java/daikon/test/split/SplitterFactoryTestUpdater.java +++ b/java/daikon/test/split/SplitterFactoryTestUpdater.java @@ -247,7 +247,27 @@ private static String getTestClassText() { " \" -s Save (do not delete) the splitter java files in the temp directory\","); ps.println(" \" -h Display this usage message\");"); ps.println(); + ps.println(" /**"); + ps.println(" * Processes command-line options."); + ps.println(" *"); + ps.println(" * @param args command-line arguments"); + ps.println(" */"); ps.println(" public static void main(String[] args) {"); + ps.println(" try {"); + ps.println(" mainHelper(args);"); + ps.println(" } catch (Daikon.DaikonTerminationException e) {"); + ps.println(" Daikon.handleDaikonTerminationException(e);"); + ps.println(" }"); + ps.println(" }"); + ps.println(); + ps.println(" /**"); + ps.println( + " * Does the work of {@link #main}, but throws an exception rather than calling {@code"); + ps.println(" * System.exit}."); + ps.println(" *"); + ps.println(" * @param args command-line arguments"); + ps.println(" */"); + ps.println(" public static void mainHelper(String[] args) {"); ps.println(" DaikonGetopt g = new DaikonGetopt(args, \"s\", new LongOpt[0], usage);"); ps.println(" int c;"); ps.println(" while ((c = g.getopt()) != -1) {"); From 680a8147d426af53ef9519f715269747bec17349 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 10 Oct 2026 19:17:07 -0700 Subject: [PATCH 22/30] TraceSelect: catch only checked exceptions, so DaikonTerminationException propagates without a rethrow clause --- java/daikon/tools/TraceSelect.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/java/daikon/tools/TraceSelect.java b/java/daikon/tools/TraceSelect.java index b40d181de0..762e4716ce 100644 --- a/java/daikon/tools/TraceSelect.java +++ b/java/daikon/tools/TraceSelect.java @@ -248,9 +248,7 @@ else if (args[i].endsWith(".dtrace")) { } } - } catch (daikon.Daikon.DaikonTerminationException e) { - throw e; - } catch (Exception e) { + } catch (IOException | ReflectiveOperationException e) { throw new Error(e); } } From 117da2eab375853c545c55e55dd61d2286e431e3 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sun, 11 Oct 2026 10:45:55 -0700 Subject: [PATCH 23/30] Brevity --- doc/CHANGELOG.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 6aa334f3e8..5787f9dd1f 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -23,10 +23,7 @@ Further documentation can be found in: ## Version 5.9.1 (???, 2026) -* Daikon and its tools now reject an unrecognized, ambiguous, or malformed - command-line option: they describe the problem and exit with a non-zero - status. Previously, Daikon printed its usage message and exited with - status 0, and most other tools ignored the bad option and continued. +* Daikon and its tools now reject a malformed command-line option. * `daikon.diff.Diff` no longer accepts the `-u` and `-z` options, which had no effect. From 21a337740eef539088205c4074cd5112db176a37 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sun, 11 Oct 2026 18:00:47 -0700 Subject: [PATCH 24/30] Add the DaikonGetopt helper that this pull request uses: argsAfterLeadingOptions --- java/daikon/DaikonGetopt.java | 41 +++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/java/daikon/DaikonGetopt.java b/java/daikon/DaikonGetopt.java index 26bf668530..9226928ab6 100644 --- a/java/daikon/DaikonGetopt.java +++ b/java/daikon/DaikonGetopt.java @@ -4,6 +4,7 @@ import gnu.getopt.Getopt; import gnu.getopt.LongOpt; +import java.util.Arrays; import org.plumelib.util.ArraysPlume; /** @@ -42,6 +43,46 @@ public DaikonGetopt(String[] argv, String optstring, LongOpt[] longopts, String opterr = false; } + /** + * Processes the leading command-line arguments of a program that takes no options other than + * {@code -h} and {@code --help}. Processing stops at the first argument that is not an option, so + * later arguments may be options for some other program. + * + * @param argv the command-line arguments + * @param usage the usage message to print for {@code -h} and {@code --help} + * @return the arguments from the first one that is not an option onward + * @throws Daikon.NormalTermination after printing the usage message, if a leading argument + * requests it + * @throws Daikon.UserError if a leading argument is any other option + */ + public static String[] argsAfterLeadingOptions(String[] argv, String usage) { + // A leading "+" tells Getopt to stop at the first argument that is not an option. + return argsAfterOptions(argv, "+", usage); + } + + /** + * Processes command-line arguments, permitting no options other than {@code -h} and {@code + * --help}. + * + * @param argv the command-line arguments + * @param optstring "" to process all the arguments, or "+" to stop at the first non-option + * @param usage the usage message to print for {@code -h} and {@code --help} + * @return the arguments that Getopt did not process as options + * @throws Daikon.NormalTermination after printing the usage message, if an argument requests it + * @throws Daikon.UserError if an argument is any other option + */ + private static String[] argsAfterOptions(String[] argv, String optstring, String usage) { + // Getopt permutes its argument array, so give it a copy to avoid modifying the caller's array. + String[] argvCopy = argv.clone(); + DaikonGetopt g = new DaikonGetopt(argvCopy, optstring, new LongOpt[0], usage); + // Every option other than -h and --help is bad, so getopt() returns only -1 or throws. + int c = g.getopt(); + if (c != -1) { + throw new Daikon.BugInDaikon("getopt() returned " + c); + } + return Arrays.copyOfRange(argvCopy, g.getOptind(), argvCopy.length); + } + /** * Like {@link Getopt#getopt()}, but never returns {@code '?'}, {@code ':'}, or {@code 'h'}. * Getopt returns {@code ':'} for a missing argument if the optstring starts with {@code ':'}. From 081553d208609767d576b31a14e43e42f9b1b229 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sun, 11 Oct 2026 20:38:50 -0700 Subject: [PATCH 25/30] TraceSelect: reset Daikon state before checking arguments; report trace read errors as UserError --- java/daikon/tools/TraceSelect.java | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/java/daikon/tools/TraceSelect.java b/java/daikon/tools/TraceSelect.java index 0033cc2a25..ced20b3fe5 100644 --- a/java/daikon/tools/TraceSelect.java +++ b/java/daikon/tools/TraceSelect.java @@ -4,9 +4,9 @@ import static java.nio.charset.StandardCharsets.UTF_8; import daikon.DaikonGetopt; -import java.io.File; 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; @@ -266,6 +266,9 @@ public static List selectSample( } return dec.patchValues(al, includeUnreturned); + } catch (UncheckedIOException e) { + // DtracePartitioner's Iterator methods cannot throw IOException. + throw e.getCause(); } } @@ -309,11 +312,9 @@ private static void deleteQuietly(String fileName) { * Checks Daikon command-line arguments by passing them to {@link daikon.Daikon#read_options}. * Handles -h and --help. * - *

This does not call {@link daikon.Daikon#cleanup}, so that it does not discard global state - * that the caller may depend on. {@code read_options} sets static fields for the options in - * {@code args}; {@link daikon.Daikon#mainHelper} resets them before it runs. An exception is - * {@code -o}, which {@code read_options} rejects if {@link daikon.Daikon#inv_file} is already - * set. This method clears that field during the check and then restores it. + *

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 @@ -321,13 +322,8 @@ private static void deleteQuietly(String fileName) { * @throws daikon.Daikon.UserError if an argument is bad */ private static void checkDaikonArgs(String[] args) { - File savedInvFile = daikon.Daikon.inv_file; - daikon.Daikon.inv_file = null; - try { - daikon.Daikon.read_options(args, usage); - } finally { - daikon.Daikon.inv_file = savedInvFile; - } + daikon.Daikon.cleanup(); + daikon.Daikon.read_options(args, usage); } /** From e60d34d3a52ec9da397d866b081dfcc9e4a0e7a7 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sun, 11 Oct 2026 20:42:59 -0700 Subject: [PATCH 26/30] Comments --- doc/CHANGELOG.md | 3 --- java/daikon/test/BadOptionTest.java | 6 +++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 41282cd440..5787f9dd1f 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -26,9 +26,6 @@ Further documentation can be found in: * Daikon and its tools now reject a malformed command-line option. * `daikon.diff.Diff` no longer accepts the `-u` and `-z` options, which had no effect. -* `daikon.tools.TraceSelect` checks all its arguments before sampling. Its - `-SEED` option now takes effect. Its options must precede the arguments - that it passes to Daikon. ## Version 5.9.0 (September 1, 2026) diff --git a/java/daikon/test/BadOptionTest.java b/java/daikon/test/BadOptionTest.java index 34e1d22b31..94faab9d9d 100644 --- a/java/daikon/test/BadOptionTest.java +++ b/java/daikon/test/BadOptionTest.java @@ -21,9 +21,9 @@ /** * Tests that Daikon tools reject bad command-line options with a UserError. * - *

These tests do not call {@code Daikon.mainHelper} or {@code Daikon.cleanup}, because they - * reset Daikon's global state, which other tests in the same JVM depend on. TraceSelect checks its - * Daikon arguments without calling either of them. + *

These tests do not call {@code Daikon.mainHelper} or {@code Daikon.cleanup}, because those + * methods reset Daikon's global state, which other tests in the same JVM depend on. TraceSelect + * checks its Daikon arguments without calling either of them. */ public class BadOptionTest { From 930b94842c42ba0fb96b29fa80763bbc778b4ae3 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sun, 11 Oct 2026 21:27:37 -0700 Subject: [PATCH 27/30] DtracePartitioner: propagate I/O errors instead of printing a stack trace --- java/daikon/tools/DtracePartitioner.java | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/java/daikon/tools/DtracePartitioner.java b/java/daikon/tools/DtracePartitioner.java index ae82a139be..5fe23e8ff5 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); } } From 79b88be639d9f77f8cb631f4228392f03a52310a Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 12 Oct 2026 04:22:49 -0700 Subject: [PATCH 28/30] BadOptionTest: correct the comment about resetting Daikon state --- java/daikon/test/BadOptionTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/java/daikon/test/BadOptionTest.java b/java/daikon/test/BadOptionTest.java index 94faab9d9d..cfe5d40312 100644 --- a/java/daikon/test/BadOptionTest.java +++ b/java/daikon/test/BadOptionTest.java @@ -21,9 +21,9 @@ /** * Tests that Daikon tools reject bad command-line options with a UserError. * - *

These tests do not call {@code Daikon.mainHelper} or {@code Daikon.cleanup}, because those - * methods reset Daikon's global state, which other tests in the same JVM depend on. TraceSelect - * checks its Daikon arguments without calling either of them. + *

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 { From e4638cbff857b7ea59ef3aad80777b6e34a74224 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sat, 17 Oct 2026 09:36:16 -0700 Subject: [PATCH 29/30] TraceSelect: don't treat a Daikon option's value as the trace file; reject -o An option or an option's value, such as "--config foo.dtrace" or "--config=foo.dtrace", is passed to Daikon rather than taken as the trace file to sample. Daikon's -o option is rejected with a clear message: TraceSelect passes -o to Daikon itself, and Daikon rejects a second -o. --- java/daikon/test/BadOptionTest.java | 23 ++++++++++++++++ java/daikon/tools/TraceSelect.java | 42 +++++++++++++++++++++++++++-- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/java/daikon/test/BadOptionTest.java b/java/daikon/test/BadOptionTest.java index eece35ca53..9e8641ff4f 100644 --- a/java/daikon/test/BadOptionTest.java +++ b/java/daikon/test/BadOptionTest.java @@ -183,6 +183,29 @@ public void testTraceSelectBadArguments() { "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. */ diff --git a/java/daikon/tools/TraceSelect.java b/java/daikon/tools/TraceSelect.java index ced20b3fe5..c18ece96d4 100644 --- a/java/daikon/tools/TraceSelect.java +++ b/java/daikon/tools/TraceSelect.java @@ -16,6 +16,7 @@ import java.util.List; import java.util.Locale; import java.util.Random; +import java.util.Set; import java.util.StringTokenizer; import java.util.regex.Pattern; import org.checkerframework.dataflow.qual.Pure; @@ -42,6 +43,30 @@ private TraceSelect() { /** Matches a negative integer, which is not an option. */ private static final Pattern NEGATIVE_INTEGER = Pattern.compile("-[0-9]+"); + /** + * 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( @@ -134,11 +159,24 @@ public static void mainHelper(String[] args) { // 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. + // 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].contains(".dtrace")) { + 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 (DAIKON_OPTIONS_WITH_ARGUMENT.contains(args[i])) { + daikonArgs.add(args[i]); + if (i + 1 < args.length) { + daikonArgs.add(args[++i]); + } + } 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 " From 52ac66cb5aef72f31c5e4bbf8afb0c697b4c26d4 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sun, 18 Oct 2026 15:30:18 -0700 Subject: [PATCH 30/30] TraceSelect: report a TraceSelect option that follows the Daikon arguments Previously, in "20 10 foo.dtrace -NOCLEAN", -NOCLEAN was passed to Daikon, which reported "Unrecognized command-line option -N". Addresses a CodeRabbit review comment. --- java/daikon/test/BadOptionTest.java | 8 ++++++++ java/daikon/tools/TraceSelect.java | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/java/daikon/test/BadOptionTest.java b/java/daikon/test/BadOptionTest.java index 9e8641ff4f..3f64bb3c5b 100644 --- a/java/daikon/test/BadOptionTest.java +++ b/java/daikon/test/BadOptionTest.java @@ -174,6 +174,14 @@ public void testTraceSelectBadArguments() { "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( diff --git a/java/daikon/tools/TraceSelect.java b/java/daikon/tools/TraceSelect.java index c18ece96d4..93e9be6496 100644 --- a/java/daikon/tools/TraceSelect.java +++ b/java/daikon/tools/TraceSelect.java @@ -169,6 +169,13 @@ public static void mainHelper(String[] args) { 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) {