001package daikon.test;
002
003import static java.io.StreamTokenizer.TT_WORD;
004import static java.nio.charset.StandardCharsets.UTF_8;
005import static java.util.logging.Level.FINE;
006import static java.util.logging.Level.INFO;
007import static org.junit.Assert.assertEquals;
008import static org.junit.Assert.fail;
009
010import daikon.Daikon;
011import daikon.Debug;
012import daikon.FileIO;
013import daikon.Global;
014import daikon.PptMap;
015import daikon.PptSlice;
016import daikon.PptTopLevel;
017import daikon.PrintInvariants;
018import daikon.ValueTuple;
019import daikon.VarInfo;
020import daikon.inv.Invariant;
021import gnu.getopt.Getopt;
022import gnu.getopt.LongOpt;
023import java.io.File;
024import java.io.IOException;
025import java.io.InputStream;
026import java.io.InputStreamReader;
027import java.io.LineNumberReader;
028import java.io.StreamTokenizer;
029import java.io.StringReader;
030import java.io.UncheckedIOException;
031import java.net.URL;
032import java.util.ArrayList;
033import java.util.Arrays;
034import java.util.HashSet;
035import java.util.List;
036import java.util.Set;
037import java.util.StringJoiner;
038import java.util.logging.Logger;
039import java.util.regex.Matcher;
040import java.util.regex.Pattern;
041import org.checkerframework.checker.nullness.qual.Nullable;
042import org.checkerframework.checker.signature.qual.ClassGetName;
043import org.junit.Test;
044import org.plumelib.util.IPair;
045import org.plumelib.util.StringsPlume;
046
047/**
048 * This tests Daikon's state as samples are processed. A standard decl file specifies the ppts. A
049 * sample input file specifies the samples and assertions that should be true at various points
050 * while processing.
051 *
052 * <p>The input file format is documented in the developer manual.
053 */
054@SuppressWarnings({"nullness", "builder"}) // test code
055public class SampleTester {
056
057  public static final Logger debug = Logger.getLogger("daikon.test.SampleTester");
058  public static final Logger debug_progress = Logger.getLogger("daikon.test.SampleTester.progress");
059
060  static boolean first_decl = true;
061  String fname;
062  LineNumberReader fp;
063  PptMap all_ppts;
064  PptTopLevel ppt;
065  VarInfo[] vars;
066
067  /** The usage message for this program. */
068  private static String usage =
069      StringsPlume.joinLines(
070          "Usage: java daikon.PrintInvariants [OPTION]... FILE",
071          "  -h, --" + Daikon.help_SWITCH,
072          "      Display this usage message",
073          "  --" + Daikon.config_option_SWITCH,
074          "      Specify a configuration option ",
075          "  --" + Daikon.debug_SWITCH,
076          "      Specify a logger to enable",
077          "  --" + Daikon.track_SWITCH,
078          "      Specify a class, varinfos, and ppt to debug track.",
079          "      Format is class<var1,var2,var3>@ppt");
080
081  public static void main(String[] args) throws IOException {
082
083    LongOpt[] longopts =
084        new LongOpt[] {
085          new LongOpt(Daikon.config_option_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0),
086          new LongOpt(Daikon.debugAll_SWITCH, LongOpt.NO_ARGUMENT, null, 0),
087          new LongOpt(Daikon.debug_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0),
088          new LongOpt(Daikon.track_SWITCH, LongOpt.REQUIRED_ARGUMENT, null, 0),
089        };
090
091    Getopt g = new Getopt("daikon.test.SampleTester", args, "h:", longopts);
092    int c;
093    while ((c = g.getopt()) != -1) {
094      switch (c) {
095
096        // long option
097        case 0:
098          String option_name = longopts[g.getLongind()].getName();
099          if (Daikon.help_SWITCH.equals(option_name)) {
100            System.out.println(usage);
101            throw new Daikon.NormalTermination();
102
103          } else if (Daikon.config_option_SWITCH.equals(option_name)) {
104            String item = Daikon.getOptarg(g);
105            daikon.config.Configuration.getInstance().apply(item);
106            break;
107
108          } else if (Daikon.debugAll_SWITCH.equals(option_name)) {
109            Global.debugAll = true;
110
111          } else if (Daikon.debug_SWITCH.equals(option_name)) {
112            daikon.LogHelper.setLevel(Daikon.getOptarg(g), FINE);
113          } else if (Daikon.track_SWITCH.equals(option_name)) {
114            daikon.LogHelper.setLevel("daikon.Debug", FINE);
115            String error = Debug.add_track(Daikon.getOptarg(g));
116            if (error != null) {
117              throw new Daikon.UserError(
118                  "Error parsing track argument '" + Daikon.getOptarg(g) + "' - " + error);
119            }
120          } else {
121            throw new RuntimeException("Unknown long option received: " + option_name);
122          }
123          break;
124
125        case 'h':
126          System.out.println(usage);
127          throw new Daikon.NormalTermination();
128
129        case '?':
130          break; // getopt() already printed an error
131
132        default:
133          System.out.println("getopt() returned " + c);
134          break;
135      }
136    }
137
138    daikon.LogHelper.setupLogs(Global.debugAll ? FINE : INFO);
139
140    new SampleTester().test_samples();
141    System.out.println("Test Passes");
142  }
143
144  private static @Nullable String find_file(String fname) {
145
146    URL input_file_location = ClassLoader.getSystemResource(fname);
147
148    if (input_file_location == null) {
149      return null;
150    } else {
151      return input_file_location.toExternalForm();
152    }
153  }
154
155  /** Test {@link #splitData}. */
156  @Test
157  public void test_splitData() {
158    assertEquals(2, splitData("\"foo@bar.com\" [\"foo@bar.com\"]").size());
159    assertEquals(2, splitData("\"baz@a.b.edu\" [\"baz@a.b.edu\" \"baz@a.b.edu\"]").size());
160    assertEquals(2, splitData("\"a@b.c.biz\" []").size());
161    assertEquals(2, splitData("\"b@b.c.biz\" [\"a@b.c.biz\"]").size());
162    assertEquals(2, splitData("\"c@b.c.biz\" [\"c@b.c.biz\" \"c@b.c.biz\" \"c@b.c.biz\"]").size());
163    assertEquals(2, splitData("- []").size());
164  }
165
166  /**
167   * This function is the actual function performed when this class is run through JUnit.
168   *
169   * @throws IOException if there in a problem with I/O
170   */
171  @Test
172  public void test_samples() throws IOException {
173    test_samples("SampleTester.commands");
174    test_samples("SampleTester.commands_linear_ternary");
175  }
176
177  /**
178   * This function is the actual function performed when this class is run through JUnit.
179   *
180   * @param commandFile the file containing the commands
181   * @throws IOException if there in a problem with I/O
182   */
183  public void test_samples(String commandFile) throws IOException {
184
185    FileIO.new_decl_format = null;
186
187    try (InputStream commands = getClass().getResourceAsStream(commandFile)) {
188      if (commands == null) {
189        fail(
190            "Missing input file: "
191                + commandFile
192                + " (Should be in daikon.test and it must be within the classpath)");
193      }
194
195      SampleTester ts = new SampleTester();
196      ts.proc_sample_file(commands, commandFile);
197    }
198  }
199
200  public void proc_sample_file(InputStream commands, String filename) throws IOException {
201
202    if (PrintInvariants.dkconfig_print_inv_class) {
203      System.out.println("Warning: turning off PrintInvariants.dkconfig_print_inv_class");
204      PrintInvariants.dkconfig_print_inv_class = false;
205    }
206
207    // Enable expected invariants
208    daikon.inv.ternary.threeScalar.FunctionBinary.dkconfig_enabled = true;
209    daikon.inv.ternary.threeScalar.FunctionBinaryFloat.dkconfig_enabled = true;
210
211    this.fname = filename;
212    fp = new LineNumberReader(new InputStreamReader(commands, UTF_8));
213
214    for (String line = fp.readLine(); line != null; line = fp.readLine()) {
215
216      // Remove comments and skip blank lines
217      line = line.replaceAll("#.*", "");
218      line = line.trim();
219      if (line.length() == 0) {
220        continue;
221      }
222
223      // Get the line type and the remainder of the line
224      String[] sa = line.split(": *", 2);
225      if (sa.length != 2) parse_error("No line type specified");
226      String ltype = sa[0].trim();
227      String cmd = sa[1].trim();
228      if (cmd.length() == 0) {
229        parse_error("no command specified");
230      }
231
232      // Process the specified type of command
233      if (ltype.equals("decl")) proc_decl(cmd);
234      else if (ltype.equals("ppt")) proc_ppt(cmd);
235      else if (ltype.equals("vars")) proc_vars(cmd);
236      else if (ltype.equals("data")) proc_data(cmd, fp, filename);
237      else if (ltype.equals("assert")) proc_assert(cmd);
238      else {
239        parse_error("unknown line type: " + ltype);
240      }
241    }
242  }
243
244  /** Reads in the specified decl file and sets all_ppts accordingly. */
245  private void proc_decl(String decl_file) throws IOException {
246
247    debug_progress.fine("Processing " + decl_file);
248
249    // Read in the specified file
250    Set<File> decl_files = new HashSet<>(1);
251    String absolute_decl_file = find_file(decl_file);
252    if (absolute_decl_file == null) {
253      fail("Decl file " + decl_file + " not found.");
254    }
255
256    if (absolute_decl_file.startsWith("file:")) {
257      absolute_decl_file = absolute_decl_file.substring(5);
258    }
259
260    decl_files.add(new File(absolute_decl_file));
261    all_ppts = FileIO.read_declaration_files(decl_files);
262
263    // Setup everything to run
264    if (first_decl) {
265      Daikon.setup_proto_invs();
266      Daikon.setup_NISuppression();
267      first_decl = false;
268    }
269
270    ppt = null;
271  }
272
273  /** Looks up the specified ppt name and set ppt accordingly. */
274  private void proc_ppt(String ppt_name) {
275
276    if (all_ppts == null) parse_error("decl file must be specified before ppt");
277    ppt = all_ppts.get(ppt_name);
278    if (ppt == null) {
279      parse_error("ppt name " + ppt_name + " not found in decl file");
280    }
281    vars = null;
282  }
283
284  /**
285   * Processes a variable list. Sets up the vars[] array to point to the matching variables in the
286   * ppt. The ppt must have been previously specified.
287   *
288   * @param var_names the variable names, separated by spaces
289   */
290  private void proc_vars(String var_names) {
291
292    if (ppt == null) {
293      parse_error("ppt must be specified first");
294    }
295
296    // Variable names are separated by blanks
297    String[] var_arr = var_names.split("  *");
298
299    // The var array contains the variables in the ppt that correspond
300    // to each name specified
301    vars = new VarInfo[var_arr.length];
302
303    // Loop through each variable name and find it in the variable list
304    for (int i = 0; i < var_arr.length; i++) {
305      String vname = var_arr[i];
306      for (int j = 0; j < ppt.var_infos.length; j++) {
307        if (vname.equals(ppt.var_infos[j].name())) vars[i] = ppt.var_infos[j];
308      }
309      if (vars[i] == null) parse_error("Variable " + vname + " not found in ppt " + ppt.name());
310    }
311  }
312
313  /**
314   * Processes a line of sample data. There should be one item of data for each previously specified
315   * variable. Each data item is separated by spaces. Spaces cannot be included within a single item
316   * (i.e., strings and arrays can't include spaces). Missing items are indicated with a dash (-).
317   * Any variables not specifically mentioned in the variable string are set to missing as well.
318   *
319   * <p>Neither orig nor derived variables are added.
320   */
321  private void proc_data(String data, LineNumberReader reader, String filename) {
322
323    if (vars == null) parse_error("vars must be specified before data");
324    List<String> da = splitData(data);
325    if (da.size() != vars.length)
326      parse_error(String.format("%d vars but %d data elements: %s", vars.length, da.size(), data));
327    debug_progress.fine("data: " + Debug.toString(da));
328
329    VarInfo[] vis = ppt.var_infos;
330    int vals_array_size = vis.length;
331    Object[] vals = new Object[vals_array_size];
332    int[] mods = new int[vals_array_size];
333
334    // initially all variables are missing
335    for (int i = 0; i < vals_array_size; i++) {
336      vals[i] = null;
337      mods[i] = ValueTuple.parseModified("2");
338    }
339
340    // Parse and enter the specified variables, - indicates a missing value
341    for (int i = 0; i < vars.length; i++) {
342      String val = da.get(i);
343      if (val.equals("-")) {
344        continue;
345      }
346      VarInfo vi = vars[i];
347      vals[vi.value_index] = vi.rep_type.parse_value(val, reader, filename);
348      mods[vi.value_index] = ValueTuple.parseModified("1");
349    }
350
351    ValueTuple vt = ValueTuple.makeUninterned(vals, mods);
352
353    // We might want to add the following at some point.  Certainly the
354    // derived variables.  The orig variables force us to deal with matching
355    // enter and exit which I really don't want to do.  Perhaps we can always
356    // give them the same value at enter and exit.  Both of these calls
357    // are in FileIO
358
359    // compute_orig_variables (ppt, vt.vals, vt.mods, nonce);
360    // compute_derived_variables (ppt, vt.vals, vt.mods);
361
362    // Causes interning
363    vt = new ValueTuple(vt.vals, vt.mods);
364
365    ppt.add_bottom_up(vt, 1);
366  }
367
368  /**
369   * Splits a "data:" line of a SampleTester input file.
370   *
371   * @param s the "data:" line of a SampleTester input file
372   * @return the components of a "data:" line of a SampleTester input file
373   */
374  private List<String> splitData(String s) {
375    s = s.trim();
376    List<String> result = new ArrayList<>();
377    while (!s.isEmpty()) {
378      IPair<String, String> p = readValueFromBeginning(s);
379      result.add(p.first);
380      s = p.second;
381    }
382    return result;
383  }
384
385  /**
386   * Reads a value from the beginning of a string.
387   *
388   * @param s a string containing a sequence of Daikon dtrace values, separated by spaces
389   * @return the first value and the remaining string
390   */
391  private IPair<String, String> readValueFromBeginning(String s) {
392    s = s.trim();
393    if (s.isEmpty()) {
394      throw new Error("Cannot read a value from an empty string");
395    }
396    switch (s.charAt(0)) {
397      case '"':
398        for (int i = 1; i < s.length(); i++) {
399          switch (s.charAt(i)) {
400            case '"':
401              return IPair.of(s.substring(0, i + 1), s.substring(i + 1).trim());
402            case '\\':
403              i++;
404              break;
405            default:
406              break;
407          }
408        }
409        throw new Error("Unterminated string: " + s);
410      case '[':
411        StringJoiner sj = new StringJoiner(" ", "[", "]");
412        s = s.substring(1);
413        while (!s.startsWith("]")) {
414          IPair<String, String> p = readValueFromBeginning(s);
415          sj.add(p.first);
416          s = p.second;
417        }
418        return IPair.of(sj.toString(), s.substring(1).trim());
419      default:
420        Matcher m = spaceOrCloseBracket.matcher(s);
421        int pos = m.find() ? m.start() : s.length();
422        return IPair.of(s.substring(0, pos), s.substring(pos).trim());
423    }
424  }
425
426  /** A regular expression that matches a space or a close square bracket. */
427  Pattern spaceOrCloseBracket = Pattern.compile("[] ]");
428
429  /**
430   * Requires that the StreamTokenizer has just read a word. Returns that word.
431   *
432   * @param stok a StreamTokenizer that has just read a word
433   * @return the word that the StreamTokenizer just read
434   */
435  private String readString(StreamTokenizer stok) {
436    int ttype;
437    try {
438      ttype = stok.nextToken();
439    } catch (IOException e) {
440      throw new UncheckedIOException(e);
441    }
442    if (ttype == TT_WORD || ttype == '"') {
443      return stok.sval;
444    } else if (ttype > 0) {
445      return String.valueOf((char) ttype);
446    } else {
447      throw new Error("Expected string.  Got ttype = " + ttype);
448    }
449  }
450
451  /** Processes a single assertion. If the assertion is false, throws an error. */
452  private void proc_assert(String assertion) throws IOException {
453
454    // Look for negation
455    boolean negate = false;
456    String assert_string = assertion;
457    if (assertion.indexOf('!') == 0) {
458      negate = true;
459      assert_string = assert_string.substring(1);
460    }
461
462    boolean result = false;
463
464    try {
465
466      // Create a tokenizer over the assertion string
467      StreamTokenizer stok = new StreamTokenizer(new StringReader(assert_string));
468      stok.wordChars('_', '_');
469      stok.commentChar('#');
470      stok.quoteChar('"');
471
472      int ttype;
473
474      // Get the assertion name
475      ttype = stok.nextToken();
476      assertEquals(TT_WORD, ttype);
477      String name = stok.sval;
478
479      // Get the arguments (enclosed in parens, separated by commas)
480      String delimiter = readString(stok);
481      if (!"(".equals(delimiter)) {
482        throw new Error("Expected \"(\", got: " + delimiter);
483      }
484
485      List<String> args = new ArrayList<>(10);
486      do {
487        String arg = readString(stok);
488        delimiter = readString(stok);
489        while (delimiter.equals("[") || delimiter.equals("]")) {
490          arg += delimiter;
491          delimiter = readString(stok);
492        }
493        args.add(arg);
494      } while (delimiter.equals(","));
495      if (!delimiter.equals(")")) {
496        parse_error(String.format("%s found where ')' expected", delimiter));
497      }
498
499      // process the specific assertion
500      if (name.equals("inv")) {
501        result = proc_inv_assert(args);
502        if (!result && !negate) {
503          daikon.LogHelper.setLevel(debug, FINE);
504          proc_inv_assert(args);
505        }
506      } else if (name.equals("show_invs")) {
507        result = proc_show_invs_assert(args);
508      } else if (name.equals("constant")) {
509        result = proc_constant_assert(args);
510      } else {
511        parse_error("unknown assertion: " + name);
512      }
513
514      if (negate) {
515        result = !result;
516      }
517    } catch (Throwable t) {
518      throw new Error(
519          String.format(
520              "Problem in file %s, line %d, assertion: %s", fname, fp.getLineNumber(), assertion),
521          t);
522    }
523
524    if (!result) {
525      fail(
526          String.format(
527              "Assertion %s fails in file %s at line %d", assertion, fname, fp.getLineNumber()));
528    }
529  }
530
531  /**
532   * Processes an invariant existence assertion and returns true if it is found. The first argument
533   * should be the invariant class. The remaining arguments are the variables. This needs to be
534   * expanded to specify more information for invariants with state.
535   */
536  private boolean proc_inv_assert(List<String> args) {
537
538    if ((args.size() < 2) || (args.size() > 4)) {
539      parse_error("bad argument count (" + args.size() + ") for invariant assertion");
540    }
541
542    Class<?> cls = null;
543    String format = null;
544
545    // First argument is a classname or a quoted string
546    String arg0 = args.get(0);
547    try {
548      debug.fine("Looking for " + arg0);
549      @SuppressWarnings("signature") // user input (?); throws exception if fails
550      @ClassGetName String arg0_cgn = arg0;
551      cls = Class.forName(arg0_cgn);
552    } catch (Exception e) {
553      format = arg0;
554      debug.fine(String.format("Looking for format: '%s' in ppt %s", format, ppt));
555    }
556
557    // Build a vis to match the specified variables
558    VarInfo[] vis = new VarInfo[args.size() - 1];
559    for (int i = 0; i < vis.length; i++) {
560      vis[i] = ppt.find_var_by_name(args.get(i + 1));
561      if (vis[i] == null) {
562        parse_error(
563            String.format("Variable '%s' not found at ppt %s", args.get(i + 1), ppt.name()));
564      }
565    }
566    PptSlice slice = ppt.findSlice(vis);
567    if (slice == null) {
568      return false;
569    }
570
571    // Look for a matching invariant in the slices invariant list
572    for (Invariant inv : slice.invs) {
573      if (inv.getClass() == cls) {
574        return true;
575      }
576      if ((format != null) && format.equals(inv.format())) {
577        return true;
578      }
579      debug.fine(String.format("trace %s: '%s'", inv.getClass(), inv.format()));
580    }
581    return false;
582  }
583
584  /**
585   * Prints all of the invariants in the given slice identified by the arguments (each of which
586   * should be a valid variable name for this ppt).
587   *
588   * @param varNames a list of variable names
589   * @return true
590   */
591  private boolean proc_show_invs_assert(List<String> varNames) {
592
593    if ((varNames.size() < 1) || (varNames.size() > 3)) {
594      parse_error("bad argument count (" + varNames.size() + ") for show_invs");
595    }
596
597    // Build a vis to match the specified variables
598    VarInfo[] vis = new VarInfo[varNames.size()];
599    for (int i = 0; i < vis.length; i++) {
600      vis[i] = ppt.find_var_by_name(varNames.get(i));
601      if (vis[i] == null) {
602        parse_error(
603            String.format("Variable '%s' not found at ppt %s", varNames.get(i), ppt.name()));
604      }
605    }
606    PptSlice slice = ppt.findSlice(vis);
607    if (slice == null) {
608      System.out.println("No invariants found for vars: " + Arrays.toString(vis));
609      return true;
610    }
611
612    // Diagnostics.
613    if (false) {
614      System.out.printf(
615          "SampleTester %s show_invs: %d invariants%n", Arrays.toString(vis), slice.invs.size());
616      for (Invariant inv : slice.invs) {
617        System.out.printf("  %s: %s%n", inv.getClass(), inv.format());
618      }
619    }
620
621    return true;
622  }
623
624  /**
625   * The constant assertion returns true if all of its arguments are constants.
626   *
627   * @param varNames variables; must be non-empty
628   * @return true if all of the given variables are constants
629   */
630  private boolean proc_constant_assert(List<String> varNames) {
631
632    if (varNames.size() < 1) {
633      parse_error("Must be at least one argument for constant assertion");
634    }
635
636    for (String arg : varNames) {
637      VarInfo v = ppt.find_var_by_name(arg);
638      if (v == null) {
639        parse_error(String.format("Variable '%s' not found at ppt %s", arg, ppt.name()));
640      }
641      if (!ppt.constants.is_constant(v)) {
642        return false;
643      }
644    }
645    return true;
646  }
647
648  private void parse_error(String msg) {
649
650    fail(String.format("Error parsing %s at line %d: %s", fname, fp.getLineNumber(), msg));
651  }
652}