1 /*
   2  * $Id$
   3  *
   4  * Copyright (c) 2001, 2011, Oracle and/or its affiliates. All rights reserved.
   5  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   6  *
   7  * This code is free software; you can redistribute it and/or modify it
   8  * under the terms of the GNU General Public License version 2 only, as
   9  * published by the Free Software Foundation.  Oracle designates this
  10  * particular file as subject to the "Classpath" exception as provided
  11  * by Oracle in the LICENSE file that accompanied this code.
  12  *
  13  * This code is distributed in the hope that it will be useful, but WITHOUT
  14  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  15  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  16  * version 2 for more details (a copy is included in the LICENSE file that
  17  * accompanied this code).
  18  *
  19  * You should have received a copy of the GNU General Public License version
  20  * 2 along with this work; if not, write to the Free Software Foundation,
  21  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  22  *
  23  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  24  * or visit www.oracle.com if you need additional information or have any
  25  * questions.
  26  */
  27 package com.sun.javatest.exec;
  28 
  29 import java.util.Hashtable;
  30 import java.util.Vector;
  31 
  32 import com.sun.javatest.Status;
  33 import com.sun.javatest.TestDescription;
  34 import com.sun.javatest.TestFilter;
  35 import com.sun.javatest.TestResult;
  36 import com.sun.javatest.TestResultTable;
  37 import com.sun.javatest.util.Debug;
  38 import com.sun.javatest.util.DynamicArray;
  39 import java.util.logging.Level;
  40 import java.util.logging.Logger;
  41 
  42 /**
  43  * Cached information about a particular tree node.
  44  * We want to collect pass/fail stats and other variable information so it can
  45  * be used to render a node quickly when requested by JTree.  Code here should
  46  * never run on the GUI thread.
  47  *
  48  * <p>
  49  * Objects of this type run to completion and are then immutable.
  50  */
  51 class TT_NodeCache implements Runnable {
  52 
  53     /**
  54      * Construct a cache object which will collect info about the given node
  55      * with respect to the supplied filter.
  56      *
  57      * @param n Node to examine, should not be null.
  58      * @param f Filter to use when producing information.  Can be null.
  59      */
  60     TT_NodeCache(TestResultTable.TreeNode n, TestFilter f, Logger l) {
  61         filter = f;
  62         node = n;
  63         log = l;
  64 
  65         // all the states plus filtered out
  66         testLists = new Vector[Status.NUM_STATES + 1];
  67         for (int i = 0; i < Status.NUM_STATES + 1; i++) {
  68             testLists[i] = new Vector<>();
  69         }
  70     }
  71 
  72     /**
  73      * Start or resume processing.
  74      */
  75     public void run() {
  76         if (debug) {
  77             Debug.println("TT_NodeCache starting");
  78             Debug.println("   -> " + this);
  79             Debug.println("   -> node " + node + "(" + node.getName() + ")");
  80             Debug.println("   -> filter=" + filter);
  81             Debug.println("   -> old state=" + state);
  82         }
  83 
  84         // needed for a new run, a paused object will already have one
  85         if (it == null) {
  86             it = init();
  87         }
  88 
  89         if (debug) {
  90             Debug.println("   -> iterator= " + it);
  91         }
  92 
  93         state = COMPUTING;
  94 
  95         process();
  96     }
  97 
  98     /**
  99      * Pause processing, and return immediately.
 100      * If the node has completed processing, calling this method has no
 101      * effect.
 102      * Be careful with MT activities when using <tt>pause() resume() isPaused()</tt>.
 103      * @see #resume()
 104      */
 105     void pause() {
 106         if (state < COMPLETED) {
 107             state = PAUSED;
 108             if (debug) {
 109                 Debug.println("TT_NodeCache for " + node.getName() + " pausing...");
 110             }
 111         }
 112     }
 113 
 114     /**
 115      * Continue processing after a pause.
 116      * @see #pause
 117      * @throws IllegalStateException if <tt>pause()</tt> was not previously called.
 118      */
 119     void resume() {
 120         if (state != PAUSED) {
 121             throw new IllegalStateException("Cache node not previously paused.");
 122         }
 123         if (debug) {
 124             Debug.println("TT_NodeCache for " + node.getName() + " resuming...");
 125         }
 126         state = COMPUTING;
 127         process();
 128     }
 129 
 130     /**
 131      * @return True if the replacement has an effect on the statistics.  False
 132      *         if it does not.
 133      */
 134     synchronized boolean add(TestResultTable.TreeNode[] path, TestResult what, int index) {
 135         boolean result = false;
 136         boolean wouldAccept = false;     // special case because of filtering
 137         boolean needsProcessing = false;
 138 
 139         // not even running yet
 140         if (it == null) {
 141             return false;
 142         }
 143 
 144         try {
 145             TestDescription td = what.getDescription();
 146             TestFilter rejector = null;
 147 
 148             synchronized (fObs) {
 149                 wouldAccept = filter.accepts(what, fObs);
 150                 if (!wouldAccept && fObs.lastTd == td) {
 151                     rejector = fObs.lastRejector;
 152                     fObs.clear();
 153                 }
 154             }   // sync
 155 
 156             needsProcessing = (!it.isPending(what));
 157 
 158             if (!needsProcessing) {
 159                 // It's still going to come out of the iterator.
 160                 // Make sure it's not the one which is committed to be next.
 161                 // Would be nice to work around this in a different way, either
 162                 // here or in the iterator.
 163                 Object peek = it.peek();
 164                 if (peek instanceof TestResult &&
 165                         ((TestResult) peek).getTestName().equals(what.getTestName())) {
 166                     it.next();      // consume
 167                     needsProcessing = true;
 168                 } else {
 169                 }
 170             } else {
 171             }
 172 
 173             if (needsProcessing) {
 174                 if (!wouldAccept) {
 175                     // add to filtered out list
 176                     localRejectCount++;
 177                     testLists[testLists.length - 1].add(what);
 178                     rejectReasons.put(what, rejector);
 179                 } else {
 180                     int type = what.getStatus().getType();
 181 
 182                     // unfortunately the add/remove messages don't seem to
 183                     // be 100% symmetric, or else there is some other race
 184                     // condition.  I tried to find the problem, but 0-5 tests out
 185                     // of 10000 will end up added twice when loading a workdir at
 186                     // startup. 12/9/2002
 187                     if (!testLists[type].contains(what)) {
 188                         stats[type]++;
 189                         testLists[type].add(what);
 190                     } else {
 191                     }
 192                 }
 193 
 194                 result = true;
 195             } else {    // will be counted later...
 196                 result = false;
 197             }
 198 
 199             // send out notifications if needed
 200             notify((wouldAccept ? what.getStatus().getType() + TT_NodeCacheObserver.OFFSET_FROM_STATUS
 201                     : TT_NodeCacheObserver.MSGS_FILTERED),
 202                     true, path, what, index);
 203 
 204             if (result) {
 205                 notifyStats();
 206             }
 207         } catch (TestResult.Fault f) {
 208 
 209             //if (debug) {
 210             //    f.printStackTrace(Debug.getWriter());
 211             //    Debug.println(msg);
 212             // }
 213 
 214             if (log != null && log.isLoggable(Level.SEVERE)) {
 215                 String msg = "TT_NodeCache - TR fault, purging old info. " + what.getTestName();
 216                 log.log(Level.SEVERE, msg, f);
 217             }
 218 
 219             // TR is somehow corrupt, remove it
 220             node.getEnclosingTable().resetTest(what.getTestName());
 221             result = false;
 222         } catch (TestFilter.Fault f) {
 223 
 224             f.printStackTrace(Debug.getWriter());
 225 
 226             if (log != null && log.isLoggable(Level.SEVERE)) {
 227                 log.log(Level.SEVERE, "TT_NodeCache - filter is broken", f);
 228             }
 229 
 230             // filter is broken, shove everything into filtered out.
 231             // this behavior is similar to what would happen in the same
 232             // case when the harness is iterating to select tests to run.
 233             // trying to only do this if counting of this test would have
 234             // been necessary.
 235             if (needsProcessing) {
 236                 localRejectCount++;
 237                 testLists[testLists.length - 1].add(what);
 238             // this reject will not have a reason entry in
 239             // rejectReasons
 240             }
 241 
 242             // ignore error and don't do anything
 243             result = false;
 244         }
 245 
 246         return result;
 247     }
 248 
 249     /**
 250      * @return True if the replacement has an effect on the statistics.  False
 251      *         if it does not.
 252      */
 253     synchronized boolean remove(TestResultTable.TreeNode[] path, TestResult what, int index) {
 254         boolean result = false;
 255         // special case because of filtering
 256 
 257         // not even running yet
 258         if (it == null) {
 259             return false;
 260         }
 261 
 262         int type = what.getStatus().getType();
 263 
 264         if (!it.isPending(what)) {          // check iterator position
 265             int[] rmList = locateTestInLists(what, type, -1);
 266 
 267             if (rmList[0] != -1) {
 268                 testLists[rmList[0]].remove(rmList[1]);
 269 
 270                 // decrement counter
 271                 if (rmList[0] < stats.length) {
 272                     stats[rmList[0]]--;
 273                 } else {
 274                     localRejectCount--;
 275                 }
 276 
 277                 // send out notifications if needed
 278                 notify(rmList[0] + TT_NodeCacheObserver.OFFSET_FROM_STATUS, false, path, what, index);
 279                 result = true;
 280             } else {
 281             }
 282         } else {        // pending in iterator
 283             Object peek = it.peek();
 284             if (peek instanceof TestResult &&
 285                     ((TestResult) peek).getTestName().equals(what.getTestName())) {
 286                 it.next();          // consume
 287 
 288                 // do it again to make it happen
 289                 result = remove(path, what, index);
 290             }
 291 
 292         }
 293 
 294         if (result) {
 295             notifyStats();
 296         }
 297 
 298         return result;
 299     }
 300 
 301     /**
 302      * @return True if the replacement has an effect on the statistics.  False
 303      *         if it does not.
 304      */
 305     synchronized boolean replace(TestResultTable.TreeNode[] path, TestResult what,
 306             int index, TestResult old) {
 307         boolean result = false;
 308         // not even running yet
 309         if (it == null) {
 310             return false;
 311         }
 312 
 313         if (!it.isPending(what)) {          // check iterator position
 314             int typeNew = what.getStatus().getType();
 315             int typeOld = old.getStatus().getType();
 316 
 317             // filtering of old does not work because of the status filter
 318             boolean wouldAcceptNew = false;
 319             TestFilter rejector = null;
 320 
 321             try {
 322                 TestDescription td = what.getDescription();
 323 
 324                 synchronized (fObs) {
 325                     wouldAcceptNew = filter.accepts(what, fObs);
 326                     if (!wouldAcceptNew && fObs.lastTd == td) {
 327                         rejector = fObs.lastRejector;
 328                         fObs.clear();
 329                     }
 330                 }       // sync
 331             } // try
 332             catch (TestResult.Fault f) {
 333 
 334                 f.printStackTrace(Debug.getWriter());
 335                 if (log != null && log.isLoggable(Level.SEVERE)) {
 336                     log.log(Level.SEVERE, "TT_NodeCache - problem with test result", f);
 337                 }
 338 
 339                 // ignore error and don't do anything
 340                 return false;
 341             } // catch
 342             catch (TestFilter.Fault f) {
 343 
 344                 f.printStackTrace(Debug.getWriter());
 345                 if (log != null && log.isLoggable(Level.SEVERE)) {
 346                     log.log(Level.SEVERE, "TT_NodeCache - filter is broken", f);
 347                 }
 348 
 349 
 350                 // ignore error and don't do anything
 351                 return false;
 352             }   // catch
 353 
 354             // inserting into one of the status lists or the filtered out list
 355             int targetList = (wouldAcceptNew ? typeNew : testLists.length - 1);
 356             int[] rmList = null;
 357 
 358             // optimization to search based on expected location of old test
 359             // and optimize out null changes
 360             if (what == old) {
 361                 if (typeOld != typeNew) {       // special case optimization
 362                     // no change to lists
 363                     rmList = new int[]{-1, -1};
 364                 } else {
 365                     rmList = locateTestInLists(old, Status.NOT_RUN, targetList);
 366 
 367                     if (rmList[0] == targetList) {
 368                         // same TR object, same status
 369                         // use -1 to specify no remove action
 370                         rmList[0] = -1;
 371                         rmList[1] = -1;
 372                     }
 373                 }
 374             } else {
 375                 rmList = locateTestInLists(old, typeOld, targetList);
 376             }
 377 
 378             if (rmList[0] != -1) {
 379                 testLists[rmList[0]].remove(rmList[1]);
 380                 testLists[targetList].add(what);
 381 
 382                 // decrement counter
 383                 if (rmList[0] < stats.length) {
 384                     stats[rmList[0]]--;
 385                 } else {
 386                     localRejectCount--;
 387                     rejectReasons.remove(what);
 388                 }
 389 
 390                 // increment counter
 391                 if (targetList < stats.length) {
 392                     stats[targetList]++;
 393                 } else {
 394                     localRejectCount++;
 395                     rejectReasons.put(what, rejector);
 396                 }
 397 
 398                 // remove the old
 399                 // add the new
 400                 notify(rmList[0] + TT_NodeCacheObserver.OFFSET_FROM_STATUS,
 401                         false, path, old, index);
 402                 notify(targetList + TT_NodeCacheObserver.OFFSET_FROM_STATUS,
 403                         true, path, what, index);
 404 
 405                 result = true;
 406             } else {
 407                 // must be a null change
 408             }
 409         } else // this TR has yet to be processed
 410         {
 411             result = false;
 412         }
 413 
 414         if (result) {
 415             notifyStats();
 416         }
 417 
 418         return result;
 419     }
 420 
 421     boolean isPaused() {
 422         return (state == PAUSED);
 423     }
 424 
 425     /**
 426      * Is this object eligible to run.
 427      * This translates to being either unprocessed or paused.
 428      */
 429     boolean canRun() {
 430         return ((state == PAUSED || state == NOT_COMPUTED) &&
 431                 valid == true);
 432     }
 433 
 434     void halt() {
 435         if (debug) {
 436             Debug.println("TT_NodeCache thread stopping");
 437             Debug.println("   -> " + this);
 438         }
 439 
 440         state = ABORTED;
 441         valid = false;
 442     }
 443 
 444     /**
 445      * Is info still being collected?
 446      * The state is not considered active if the processing is paused.
 447      *
 448      * @return True if information is incomplete, false otherwise.
 449      * @see #isPaused()
 450      */
 451     boolean isActive() {
 452         return (state == COMPUTING);
 453     }
 454 
 455     /**
 456      * Has all the information been collected.
 457      * @return True if all information is up to date, and will not change
 458      *         unless this node is invalidated.
 459      */
 460     boolean isComplete() {
 461         return (state == COMPLETED);
 462     }
 463 
 464     boolean isAborted() {
 465         return (state == ABORTED);
 466     }
 467 
 468     /**
 469      * Invalidate any information in this node cache.
 470      * @see #isValid()
 471      */
 472     void invalidate() {
 473         valid = false;
 474     }
 475 
 476     /**
 477      * Has the data in this node been invalidated.
 478      * A node may be valid while it is active, but becomes invalid when
 479      * notified that the constraints that it is executing with are no longer
 480      * correct.  It may also become invalid if the thread is interrupted.
 481      * @see #invalidate()
 482      */
 483     boolean isValid() {
 484         return valid;
 485     }
 486 
 487     TestResultTable.TreeNode getNode() {
 488         return node;
 489     }
 490 
 491     TestFilter getFilter() {
 492         return filter;
 493     }
 494 
 495     /**
 496      * Get the pass fail error notrun stats.
 497      * The data may be in flux if the data is still being collected, use
 498      * <tt>isActive()</tt> to anticipate this.
 499      * @return An array of size Status.NUM_STATES.  This is not a copy, do not
 500      *         alter.
 501      * @see #isActive()
 502      * @see com.sun.javatest.Status#NUM_STATES
 503      */
 504     int[] getStats() {
 505         return stats;
 506     }
 507 
 508     /**
 509      * Find out how many tests were rejected by filters.
 510      * @return Number of rejected tests found in and below this node.
 511      */
 512     int getRejectCount() {
 513         if (it != null) {
 514             return it.getRejectCount() + localRejectCount;
 515         } else {
 516             return localRejectCount;
 517         }
 518     }
 519 
 520     TestFilter getRejectReason(TestResult tr) {
 521         return rejectReasons.get(tr);
 522     }
 523 
 524     /**
 525      * Snapshot the current data and add an observer.
 526      * This is an atomic operation so that you can get completely up to date
 527      * and monitor all changes going forward.
 528      *
 529      * @param obs The observer to attach.  Must not be null.
 530      * @param needSnapshot Does the caller want a snapshot of the current test lists.
 531      *        True if yes, false if not.
 532      * @return A copy of the Vectors that contain the current list of tests.  Null if
 533      *         <tt>needSnapshot</tt> is false.
 534      */
 535     synchronized Vector<TestResult>[] addObserver(TT_NodeCacheObserver obs, boolean needSnapshot) {
 536         // snapshot the current data
 537         // must be done before adding the observer to ensure correct data
 538         // delivery to client
 539 
 540         Vector<TestResult>[] cp = null;
 541         if (needSnapshot) {
 542             cp = new Vector[testLists.length];
 543             for (int i = 0; i < testLists.length; i++) {
 544                 cp[i] = (Vector<TestResult>) (testLists[i].clone());
 545             }
 546         }
 547 
 548         if (obs != null) {
 549             observers = DynamicArray.append(observers, obs);
 550         }
 551 
 552         return cp;
 553     }
 554 
 555     // advisory - many thread access this.  you should lock this object before
 556     // calling.  See BP_TestListSubpanel.reset(TT_NodeCache), which locks this,
 557     // THEN itself (the GUI component) for proper locking sequence, since the
 558     // highest contention is for this cache object.
 559     synchronized void removeObserver(TT_NodeCacheObserver obs) {
 560         observers = DynamicArray.remove(observers, obs);
 561     }
 562 
 563     // ------------- PRIVATE -----------------
 564     private void process() {
 565         final TestResultTable trt = node.getEnclosingTable();
 566         if (trt == null) {
 567             return;
 568         }
 569         try {
 570             trt.getLock().lock();
 571             while (state != ABORTED && state != PAUSED && it.hasNext()) {
 572                 try {
 573                     synchronized (node) {       // to maintain locking order
 574                         synchronized (this) {   // sync to lockout during add/remove/replace
 575                             if (!it.hasNext()) // need to recheck after locking
 576                             {
 577                                 continue;
 578                             }
 579 
 580                             TestResult tr = it.next();
 581                             TestDescription td = tr.getDescription();
 582                             TestFilter rejector = null;
 583                             boolean wouldAccept = false;
 584 
 585                             synchronized (fObs) {
 586                                 try {
 587                                     wouldAccept = filter.accepts(tr, fObs);
 588                                 } catch (TestFilter.Fault f) {
 589                                     f.printStackTrace(Debug.getWriter());
 590                                     if (log != null && log.isLoggable(Level.SEVERE)) {
 591                                         log.log(Level.SEVERE, "TT_NodeCache - filter is broken", f);
 592                                     }
 593 
 594                                     // assume it is accepted, this is what would
 595                                     // happen for test execution as well
 596                                     wouldAccept = true;
 597                                 }       // catch
 598 
 599                                 if (!wouldAccept && fObs.lastTd == td) {
 600                                     rejector = fObs.lastRejector;
 601                                     fObs.clear();
 602                                 }
 603                             }   // sync
 604 
 605                             if (wouldAccept) {
 606                                 int type = tr.getStatus().getType();
 607 
 608                                 if (!testLists[type].contains(tr)) {
 609                                     stats[type]++;
 610                                     testLists[type].add(tr);
 611 
 612                                     // XXX We are not producing
 613                                     // the all the parameters.  it seems to be overkill
 614                                     // for what we are currently using this event for.
 615                                     // Perhaps the API need to be revisited.
 616                                     notify(type + TT_NodeCacheObserver.OFFSET_FROM_STATUS,
 617                                             true, null, tr, -1);
 618                                 } else {
 619                                     // duplicate, no action
 620                                 }
 621 
 622                             } else {
 623                                 // filtered out list
 624                                 testLists[testLists.length - 1].add(tr);
 625                                 rejectReasons.put(tr, rejector);
 626                                 localRejectCount++;
 627 
 628                                 // XXX We are not producing
 629                                 // the all the parameters.  it seems to be overkill
 630                                 // for what we are currently using this event for.
 631                                 // Perhaps the API need to be revisited.
 632                                 notify(TT_NodeCacheObserver.MSGS_FILTERED,
 633                                         true, null, tr, -1);
 634                             }
 635                         }       // sync this
 636                     }   // sync node
 637                 } // try
 638                 catch (TestResult.Fault f) {
 639                     f.printStackTrace(Debug.getWriter());
 640                     if (log != null && log.isLoggable(Level.SEVERE)) {
 641                         log.log(Level.SEVERE, "TT_NodeCache - problem with test result", f);
 642                     }
 643 
 644                 // try to recover it?
 645                 // ignore error and don't do anything
 646                 }   // catch
 647 
 648                 notifyStats();
 649             }   // while
 650 
 651             if (state != PAUSED) {
 652                 cleanup();
 653             }
 654 
 655         } finally {
 656             trt.getLock().unlock();
 657         }
 658     }
 659 
 660     /**
 661      * Find a particular test in the test lists.  You can specify which lists to
 662      * search first or last to help mitigate the O(n) search performance.  It is
 663      * assumed that synchronization for access to the lists has been taken care of.
 664      *
 665      * @param tr The test to locate.
 666      * @param firstListToCheck Hint of where to start looking.  -1 to specify none.
 667      * @param lastListToCheck Hint of where the last place to look should be.
 668      *          -1 to specify none.
 669      * @return Array describing [0] which list the item was found in, [1] at
 670      *         what index.  If [0] is greater than -1, then so will [1].  [0]
 671      *         of -1 indicates that then item was not found.
 672      */
 673     private int[] locateTestInLists(TestResult tr, int firstListToCheck,
 674             int lastListToCheck) {
 675         int[] result = new int[2];
 676         result[0] = -1;
 677         result[1] = -1;
 678 
 679         if (firstListToCheck >= 0) {
 680             int possible = testLists[firstListToCheck].indexOf(tr);
 681             if (possible != -1) {
 682                 // done with search
 683                 result[0] = firstListToCheck;
 684                 result[1] = possible;
 685             }
 686         } else {
 687         }
 688 
 689         // do a more exhaustive search if not found yet
 690         if (result[0] == -1) {
 691             for (int i = 0; i < testLists.length; i++) {
 692                 // skip the lists which have been checked or that should be
 693                 // checked last
 694                 // this is just a performance optimization
 695                 if (i == firstListToCheck || i == lastListToCheck) {
 696                     continue;
 697                 }
 698 
 699                 int possible = testLists[i].indexOf(tr);
 700                 if (possible != -1) {
 701                     // found in list i, position possible
 702                     // done with search
 703                     result[0] = i;
 704                     result[1] = possible;
 705                     break;
 706                 }
 707             }   // for
 708         }
 709 
 710         // if still not found, check the list which was specified to be checked
 711         // last, this is just a performance optimization
 712         if (result[0] == -1 && lastListToCheck >= 0) {
 713             int possible = testLists[lastListToCheck].indexOf(tr);
 714             if (possible != -1) {
 715                 result[0] = lastListToCheck;
 716                 result[1] = possible;
 717             }
 718         }
 719 
 720         return result;
 721     }
 722 
 723     /**
 724      * Determine the index of a particular test in a vector.
 725      */
 726     private int searchList(TestResult target, Vector list) {
 727         int possible = list.indexOf(target);
 728         return possible;
 729     }
 730 
 731     private void cleanup() {
 732         if (state == ABORTED) {
 733             // no valid info
 734             return;
 735         }
 736 
 737         state = COMPLETED;
 738     }
 739 
 740     /**
 741      * Prepare to process this node.
 742      * Avoid calling until it is time to process the node.
 743      * @return Iterator for this node, or null if not possible.
 744      */
 745     private TestResultTable.TreeIterator init() {
 746         if (node == null) {
 747             valid = false;
 748             state = ABORTED;
 749             return null;
 750         }
 751 
 752         return TestResultTable.getIterator(node);
 753     }
 754 
 755     private synchronized void notify(int type, boolean isAdd,
 756             TestResultTable.TreeNode[] path,
 757             TestResult what, int index) {
 758         if (observers.length == 0) {
 759             return;
 760         }
 761 
 762         for (int i = 0; i < observers.length; i++) {
 763             boolean[] mask = observers[i].getEventMasks();
 764             if (mask[0] || mask[type]) {
 765                 if (isAdd) {
 766                     observers[i].testAdded(type, path, what, index);
 767                 } else {
 768                     observers[i].testRemoved(type, path, what, index);
 769                 }
 770             }
 771         }   // for
 772     }
 773 
 774     private synchronized void notifyStats() {
 775         if (observers.length == 0) {
 776             return;
 777         }
 778 
 779         for (int i = 0; i < observers.length; i++) {
 780             boolean[] mask = observers[i].getEventMasks();
 781             if (mask[0] || mask[TT_NodeCacheObserver.MSGS_STATS]) {
 782                 observers[i].statsUpdated(stats);
 783             }
 784         }   // for
 785     }
 786 
 787 
 788     private final TestResultTable.TreeNode node;
 789     private final Logger log;
 790     private TestResultTable.TreeIterator it;
 791     private TestFilter filter;
 792     private int[] stats = new int[Status.NUM_STATES];
 793     private int localRejectCount;
 794     private Hashtable<TestResult, TestFilter> rejectReasons = new Hashtable<>();
 795     private final FilterObserver fObs = new FilterObserver();
 796     private TT_NodeCacheObserver[] observers = new TT_NodeCacheObserver[0];
 797     private Vector<TestResult>[] testLists;     // could use unsynchronized data structure
 798     private volatile int state;
 799     private volatile boolean valid = true;
 800     private static final int NOT_COMPUTED = 0;
 801     private static final int COMPUTING = 1;
 802     private static final int COMPLETED = 2;
 803     private static final int PAUSED = 3;
 804     private static final int ABORTED = 4;
 805     private static final boolean debug = Debug.getBoolean(TT_NodeCache.class);
 806 
 807     static abstract class TT_NodeCacheObserver {
 808 
 809         public TT_NodeCacheObserver() {
 810             interestList = new boolean[EVENT_LIST_SIZE];
 811         }
 812 
 813         /**
 814          * Find out what messages this observer is interested in.
 815          */
 816         public boolean[] getEventMasks() {
 817             return interestList;
 818         }
 819 
 820         public abstract void testAdded(int messageType,
 821                 TestResultTable.TreeNode[] path, TestResult what, int index);
 822 
 823         public abstract void testRemoved(int messageType,
 824                 TestResultTable.TreeNode[] path, TestResult what, int index);
 825 
 826         public abstract void statsUpdated(int[] stats);
 827         protected boolean[] interestList;
 828         public static final int EVENT_LIST_SIZE = 7;
 829         public static final int MSGS_ALL = 0;
 830         public static final int MSGS_STATS = 1;
 831         public static final int MSGS_PASSED = 2;
 832         public static final int MSGS_FAILED = 3;
 833         public static final int MSGS_ERRORS = 4;
 834         public static final int MSGS_NOT_RUNS = 5;
 835         public static final int MSGS_FILTERED = 6;
 836         public static final int OFFSET_FROM_STATUS = 2;
 837     }
 838 
 839     static class FilterObserver implements TestFilter.Observer {
 840 
 841         public void rejected(TestDescription d, TestFilter rejector) {
 842             lastTd = d;
 843             lastRejector = rejector;
 844         }
 845 
 846         public void clear() {
 847             lastTd = null;
 848             lastRejector = null;
 849         }
 850         TestDescription lastTd;
 851         TestFilter lastRejector;
 852     }
 853 
 854 }