1 /* 2 * $Id$ 3 * 4 * Copyright (c) 2002, 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 occured 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.awt.EventQueue; 30 import java.util.Collections; 31 import java.util.Enumeration; 32 import java.util.Hashtable; 33 import java.util.HashSet; 34 import java.util.Iterator; 35 import java.util.LinkedList; 36 import java.util.Set; 37 38 import javax.swing.tree.TreeModel; 39 import javax.swing.tree.TreeNode; 40 import javax.swing.tree.TreePath; 41 import javax.swing.event.TreeModelEvent; 42 import javax.swing.event.TreeModelListener; 43 44 import com.sun.javatest.JavaTestError; 45 import com.sun.javatest.Parameters; 46 import com.sun.javatest.TRT_TreeNode; 47 import com.sun.javatest.TestFilter; 48 import com.sun.javatest.TestResult; 49 import com.sun.javatest.TestResultTable; 50 import com.sun.javatest.TestSuite; 51 import com.sun.javatest.WorkDirectory; 52 import com.sun.javatest.tool.UIFactory; 53 import com.sun.javatest.util.Debug; 54 import com.sun.javatest.util.DynamicArray; 55 import com.sun.javatest.util.StringArray; 56 57 import java.util.ArrayList; 58 import java.util.Arrays; 59 import java.util.Comparator; 60 import java.util.logging.Logger; 61 62 /** 63 * This is the data model bridge between JT Harness core and the GUI. 64 */// NOTE: if you are going to request the worker lock and the htLock, you 65 // MUST do it in that order. Be mindful of holding the htLock and 66 // calling a synchronized method, that is illegal and you must 67 // release the htLock before making the method call. 68 class TestTreeModel implements TreeModel, TestResultTable.TreeObserver { 69 70 TestTreeModel() { 71 } 72 73 TestTreeModel(Parameters p, FilterSelectionHandler filterHandler, UIFactory uif) { 74 this.filterHandler = filterHandler; 75 this.uif = uif; 76 77 cache = new Hashtable<>(); 78 cacheQueue = new LinkedList<>(); 79 suspendedQueue = new LinkedList<>(); 80 81 cacheWorker = new CacheWorker(); 82 cacheWorker.setPriority(Thread.MIN_PRIORITY + 1); 83 cacheWorker.start(); 84 85 setParameters(p); 86 87 watcher = new FilterWatcher(); 88 filterHandler.addObserver(watcher); 89 lastFilter = filterHandler.getActiveFilter(); 90 } 91 92 synchronized void dispose() { 93 disposed = true; 94 95 if (trt != null) { 96 trt.removeObserver(this); 97 trt.dispose(); 98 trt = null; 99 } 100 101 filterHandler.removeObserver(watcher); 102 103 if (cacheWorker != null) { 104 cacheWorker.interrupt(); 105 cacheWorker = null; 106 } 107 108 params = null; 109 } 110 111 TestFilter getTestFilter() { 112 return filterHandler.getActiveFilter(); 113 } 114 115 Parameters getParameters() { 116 return params; 117 } 118 119 // TreeModel methods 120 public void addTreeModelListener(TreeModelListener l) { 121 treeModelListeners = DynamicArray.append(treeModelListeners, l); 122 } 123 124 public Object getChild(Object parent, int index) { 125 if (parent instanceof TT_BasicNode) { 126 return ((TT_BasicNode) parent).getChildAt(index); 127 } else { 128 return null; 129 } 130 } 131 132 public int getChildCount(Object parent) { 133 if (parent instanceof TreeNode) { 134 return ((TreeNode) parent).getChildCount(); 135 } else { 136 return -1; 137 } 138 } 139 140 public int getIndexOfChild(Object parent, Object child) { 141 if (parent instanceof TT_BasicNode) { 142 return ((TT_BasicNode) parent).getIndex((TT_TreeNode) child); 143 } else { 144 return -1; 145 } 146 } 147 148 public Object getRoot() { 149 if (disposed) { 150 if (debug > 0) { 151 Debug.println("TTM - getRoot() ignored, model has been disposed."); 152 } 153 return null; 154 } 155 156 // trt is never supposed to be null 157 if (root == null) { 158 root = new TT_BasicNode(null, (TRT_TreeNode) (trt.getRoot()), trt.getTestFinder().getComparator()); 159 } 160 return root; 161 } 162 163 public boolean isLeaf(Object node) { 164 if (node == null) { 165 return true; 166 } 167 168 if (node instanceof TT_BasicNode) { 169 return false; 170 } else if (node instanceof TT_TestNode) { 171 return true; 172 } else { 173 throw new IllegalArgumentException(uif.getI18NString("tree.badType")); 174 } 175 } 176 177 public void removeTreeModelListener(TreeModelListener l) { 178 treeModelListeners = DynamicArray.remove(treeModelListeners, l); 179 } 180 181 public void valueForPathChanged(TreePath path, Object newValue) { 182 // XXX 183 System.err.println(getClass().getName() + ": VFPC"); 184 } 185 186 // TRT TreeObserver methods 187 public void nodeInserted(final TestResultTable.TreeNode[] path, 188 final Object what, final int index) { 189 190 if (!EventQueue.isDispatchThread()) { 191 Runnable t = new Runnable() { 192 193 public void run() { 194 nodeInserted0(path, what, index); 195 } 196 }; // Runnable 197 198 EventQueue.invokeLater(t); 199 } 200 else { 201 nodeInserted0(path, what, index); 202 } 203 } 204 205 206 private synchronized void nodeInserted0(final TestResultTable.TreeNode[] path, 207 final Object what, final int index) { 208 if (disposed) 209 return; 210 211 Object[] nodes = {what}; 212 213 TreeModelEvent tme; 214 TT_BasicNode[] transPath = translatePath(path, true); 215 216 if (transPath == null) { 217 return; // error condition 218 } 219 220 // accomplish the primary task - insertion 221 if (what instanceof TestResultTable.TreeNode) { 222 /* 223 if (!relevantNodes.contains(path[path.length - 1])) { 224 return; 225 }*/ 226 TestResultTable.TreeNode tn = (TestResultTable.TreeNode)what; 227 int[] newPositions = transPath[transPath.length-1].addNodes(new TestResultTable.TreeNode[] {tn}); 228 229 if (debug > 0) { 230 Debug.println("TTM - Node " + what + " inserting, path len=" + path.length); 231 } 232 if (path == null || path.length == 0) // root 233 { 234 tme = new TreeModelEvent(this, new TreePath(getRoot())); 235 } else { 236 tme = new TreeModelEvent(this, transPath, newPositions, nodes); 237 } 238 //if (cacheWorker != null && !cacheWorker.isPaused()) { 239 notifyModelListeners(tme, Notifier.INS); 240 //} 241 } else { // test result 242 TestResult tr = (TestResult) what; 243 244 // BK remove first part of if - should not happen 245 if (transPath != null /* && relevantNodes.contains(transPath[transPath.length - 1])*/) { 246 if (debug > 0) { 247 Debug.println("TTM - Node " + what + " inserted, path len=" + path.length); 248 Debug.println(" -> inserting " + tr.getTestName()); 249 Debug.println(" -> mutable " + tr.isMutable()); 250 Debug.println(" -> status " + tr.getStatus().getType()); 251 Debug.println(" -> Thread: " + Thread.currentThread()); 252 } 253 254 // if (cacheWorker != null && !cacheWorker.isPaused()) { 255 // BK optimize reuse of transPath var above 256 // notifyInserted(makeEvent(path, what, index)); 257 processInsert(transPath, tr); 258 // } 259 } 260 else { 261 } 262 263 // send updates to parent nodes for book keeping 264 for (int i = transPath.length - 1; i >= 0; i--) { 265 // walk path thru cache 266 TT_NodeCache ni = null; 267 268 synchronized (htLock) { 269 ni = cache.get(transPath[i].getTableNode()); 270 //ni = getNodeInfo(transPath[i].getTableNode(), false); 271 } // sync 272 273 // notify cache nodes 274 // most of the time ni would probably be null 275 if (ni != null) { 276 boolean result = false; 277 synchronized (ni.getNode()) { 278 result = ni.add(path, (TestResult) what, index); 279 } // sync 280 281 if (result || relevantNodes.contains(transPath[i])) { 282 // if change detected, and relevant, then broadcast 283 // update of summary numbers is passive because it retrieves 284 // updates itself. 285 // we need to tell the tree when to update though 286 if (transPath[i].isRoot()) { 287 tme = new TreeModelEvent(this, new TreePath(transPath[i])); 288 } else { 289 tme = makeEvent(TestResultTable.getObjectPath(path[i - 1]), path[i], path[i - 1].getIndex(path[i])); 290 } 291 if (cacheWorker != null && !cacheWorker.isPaused()) { 292 notifyModelListeners(tme, Notifier.CHANGE); 293 } 294 } 295 } 296 } // for 297 } 298 } 299 300 public void nodeChanged(final TestResultTable.TreeNode[] path, 301 final Object what, final int index, final Object old) { 302 if (!EventQueue.isDispatchThread()) { 303 Runnable t = new Runnable() { 304 305 public void run() { 306 nodeChanged0(path, what, index, old); 307 } 308 }; // Runnable 309 310 EventQueue.invokeLater(t); 311 } 312 else { 313 nodeChanged0(path, what, index, old); 314 } 315 } 316 317 private synchronized void nodeChanged0(TestResultTable.TreeNode[] path, Object what, 318 int index, Object old) { 319 if (disposed) 320 return; 321 322 if (what instanceof TestResultTable.TreeNode) { 323 // check relevance 324 if (relevantNodes.contains(path[path.length - 1])) { 325 notifyModelListeners(makeEvent(path, what, index), Notifier.CHANGE); 326 notifyModelListeners(new TreeModelEvent(this, path, null, null), Notifier.STRUCT); 327 } 328 } else { // test result 329 if (relevantTests.contains(old) && old != what) { 330 relevantTests.remove(old); 331 relevantTests.add((TestResult) what); 332 } 333 334 TT_BasicNode[] transPath = translatePath(path, false); 335 if (transPath != null) { 336 TreeModelEvent where = transPath[transPath.length-1].replaceTest((TestResult)what, false); 337 if (where == null && debug > 0) { 338 Debug.println("No test insertion pt for " + ((TestResult)what).getTestName()); 339 } 340 } 341 else { 342 // ignored. should log it. 343 } 344 345 for (int i = path.length - 1; i >= 0; i--) { 346 // walk path thru cache 347 TT_NodeCache ni = null; 348 349 synchronized (htLock) { 350 ni = cache.get(path[i]); 351 } // sync 352 353 // notify cache nodes 354 if (ni != null) { 355 boolean result = ni.replace(path, (TestResult) what, index, (TestResult) old); 356 357 /* dead code 358 if (result && relevantNodes.contains(transPath[i])) { 359 // if change detected, and relevant, then broadcast 360 //notifyChanged(makeEvent(path, what, index)); 361 }*/ 362 } 363 } // for 364 } 365 } 366 367 public void nodeRemoved(final TestResultTable.TreeNode[] path, 368 final Object what, final int index) { 369 370 if (!EventQueue.isDispatchThread()) { 371 Runnable t = new Runnable() { 372 373 public void run() { 374 nodeRemoved0(path, what, index); 375 } 376 }; // Runnable 377 378 EventQueue.invokeLater(t); 379 } 380 else { 381 nodeRemoved0(path, what, index); 382 } 383 } 384 385 private synchronized void nodeRemoved0(TestResultTable.TreeNode[] path, Object what, int index) { 386 if (disposed) 387 return; 388 if (what instanceof TestResultTable.TreeNode) { 389 //if (true || relevantNodes.contains(path[path.length - 1])) { 390 if (debug > 0) { 391 Debug.println("TTM - Node " + what + " removed, path len=" + path.length); 392 } 393 394 TT_BasicNode[] transPath = translatePath(path, false); 395 TestResultTable.TreeNode node = (TestResultTable.TreeNode)what; 396 TT_TreeNode tn = transPath[transPath.length-1].findByName(node.getName()); 397 int where = transPath[transPath.length-1].removeNode(tn); 398 399 if (where >= 0) { 400 TreeModelEvent tme = new TreeModelEvent(this, transPath, new int[] {where}, 401 new Object[] {tn}); 402 notifyModelListeners(tme, Notifier.DEL); 403 } 404 } else { // test result 405 TestResult tr = (TestResult) what; 406 TT_BasicNode[] transPath = translatePath(path,false); 407 408 relevantTests.remove(tr); 409 410 //if (debug > 0 && relevantNodes.contains(transPath[transPath.length - 1])) { 411 if (debug > 0) { 412 //if (debug > 0) { 413 Debug.println("TTM - Node " + what + " removed, path len=" + path.length); 414 Debug.println(" -> Removing " + tr.getTestName()); 415 Debug.println(" -> Thread: " + Thread.currentThread()); 416 } 417 418 processRemove(transPath, tr, index); 419 420 for (int i = path.length - 1; i >= 0; i--) { 421 // walk path thru cache 422 TT_NodeCache ni = null; 423 424 synchronized (htLock) { 425 ni = cache.get(path[i]); 426 } // sync 427 428 // notify cache nodes 429 if (ni != null) { 430 // need to lock node before locking the cache node 431 boolean result = false; 432 synchronized (ni.getNode()) { 433 result = ni.remove(path, (TestResult) what, index); 434 } // sync 435 } 436 } // for 437 } 438 } 439 440 // ----- private ----- 441 private void processRemove(final TT_BasicNode[] path, final TestResult tr, 442 final int index) { 443 if (!EventQueue.isDispatchThread()) { 444 Runnable t = new Runnable() { 445 446 public void run() { 447 processRemove0(path, tr, index); 448 } 449 }; // Runnable 450 451 EventQueue.invokeLater(t); 452 } 453 else { 454 processRemove0(path, tr, index); 455 } 456 } 457 458 private void processRemove0(TT_BasicNode[] path, TestResult tr, int index) { 459 //TT_TestNode tn = path[path.length - 1].findByName(tr); 460 TreeModelEvent tme = path[path.length - 1].removeTest(tr); 461 //if (tn == null || pos < 0) { 462 if (tme == null) { 463 return; 464 } 465 466 for (int i = 0; i < treeModelListeners.length; i++) { 467 treeModelListeners[i].treeNodesRemoved(tme); 468 } 469 } 470 471 private void processInsert(final TT_BasicNode[] path, final TestResult tr) { 472 if (!EventQueue.isDispatchThread()) { 473 Runnable t = new Runnable() { 474 475 public void run() { 476 processInsert0(path, tr); 477 } 478 }; // Runnable 479 480 EventQueue.invokeLater(t); 481 } else { 482 processInsert0(path, tr); 483 } 484 } 485 486 private void processInsert0(TT_BasicNode[] path, TestResult tr) { 487 TT_TestNode newNode = new TT_TestNode(path[path.length - 1], tr); 488 //int pos = path[path.length - 1].addTest(newNode, sortComparator); 489 TreeModelEvent tme = path[path.length - 1].addTest(newNode, sortComparator); 490 if (tme == null) return; 491 492 for (int i = 0; i < treeModelListeners.length; i++) { 493 treeModelListeners[i].treeNodesInserted(tme); 494 } 495 496 } 497 498 TreePath resolveUrl(String path) { 499 if (path == null || path.length() == 0 || root == null) { 500 return null; 501 } 502 ArrayList<TT_TreeNode> al = new ArrayList<>(); 503 al.add(root); 504 TT_BasicNode spot = root; 505 506 StringBuffer sb = new StringBuffer(path); 507 508 while (sb.length() > 0) { 509 int slash = sb.indexOf("/"); 510 String current = null; 511 if (slash < 0) { 512 current = sb.toString(); 513 sb.setLength(0); 514 } else { 515 current = sb.substring(0, slash); 516 sb = sb.delete(0, slash+1); 517 } 518 519 TT_TreeNode node = spot.findByName(current); 520 521 if (node == null) { 522 return null; 523 } else if (node instanceof TT_BasicNode) { 524 al.add(node); 525 spot = (TT_BasicNode) node; 526 } else if (node instanceof TT_TestNode) { 527 al.add(node); 528 sb.setLength(0); 529 } 530 } 531 532 return new TreePath(al.toArray()); 533 } 534 535 void addRelevantNode(TT_TreeNode node) { 536 relevantNodes.add(node); 537 } 538 539 void removeRelevantNode(TT_TreeNode node) { 540 relevantNodes.remove(node); 541 } 542 543 void addRelevantTest(TestResult tr) { 544 relevantTests.add(tr); 545 } 546 547 void removeRelevantTest(TestResult tr) { 548 if (relevantTests != null && tr != null) { 549 relevantTests.remove(tr); 550 } 551 } 552 553 /** 554 * Hint that the given node is of more importance than others. 555 */ 556 void setActiveNode(TT_BasicNode node) { 557 TT_NodeCache ni = null; 558 559 synchronized (htLock) { 560 ni = cache.get(node.getTableNode()); 561 } 562 563 if (ni != null) { 564 cacheWorker.requestActiveNode(ni); 565 } else { 566 // uh...why don't we have a node? 567 // create and schedule? 568 } 569 } 570 571 /** 572 * Create an event from the raw TRT data, so it can be passed to the GUI tree. 573 */ 574 private TreeModelEvent makeEvent(TestResultTable.TreeNode[] path, Object target, int index) { 575 if (getRoot() == null) { 576 return null; 577 } 578 int[] inds = {index}; 579 // per the TreeModelEvent javadoc, the path is supposed to lead to the parent of the 580 // changed node 581 582 TT_BasicNode[] transPath = translatePath(path, false); 583 584 TT_TreeNode[] transTarget = new TT_TreeNode[1]; 585 if (target instanceof TestResult && 586 transPath[transPath.length - 1] instanceof TT_BasicNode) { 587 TT_TestNode mtn = transPath[transPath.length - 1].findByName((TestResult) target); 588 if (mtn != null) { 589 transTarget[0] = mtn; 590 } else { 591 return null; // no matching test 592 } 593 } else { 594 TT_TreeNode mtn = transPath[transPath.length - 1].findByName( 595 ((TestResultTable.TreeNode) target).getName()); 596 if (mtn != null) { 597 transTarget[0] = mtn; 598 } else { 599 return null; // no matching test 600 } 601 } 602 603 if (debug > 1) { 604 Debug.println("TTM Broadcasing " + target + " change message..."); 605 //Debug.println(" -> Status = " + ((TestResult)target).getStatus().toString()); 606 Debug.println(" -> Path len=" + transPath.length); 607 Debug.println(" -> Index = " + index); 608 Debug.println(" -> target = " + Arrays.toString(transTarget)); 609 } 610 611 return new TreeModelEvent(this, transPath, inds, transTarget); 612 } 613 614 TT_BasicNode[] translatePath(TestResultTable.TreeNode[] path, boolean create) { 615 if (path == null) 616 return null; 617 618 TT_BasicNode location = (TT_BasicNode)getRoot(); 619 TT_BasicNode[] transPath = new TT_BasicNode[path.length]; 620 transPath[0] = location; 621 622 for (int i = 1; i < path.length; i++) { 623 TT_TreeNode mnode = location.findByName(path[i].getName()); 624 if (create && mnode == null) { 625 location.addNodes(new TestResultTable.TreeNode[] {path[i]}); 626 mnode = location.findByName(path[i].getName()); 627 } 628 else if (!(mnode instanceof TT_BasicNode)) { 629 return null; // ignore event 630 } 631 transPath[i] = (TT_BasicNode) mnode; 632 location = transPath[i]; 633 } 634 635 return transPath; 636 } 637 638 String[] pathsToStrings(TreePath[] paths) { 639 if (paths == null || paths.length == 0 || 640 !(getRoot() instanceof TT_BasicNode)) 641 return null; 642 643 String[] result = new String[paths.length]; 644 for (int i = 0; i < paths.length; i++) { 645 TT_TreeNode tn = (TT_TreeNode)(paths[i].getLastPathComponent()); 646 result[i] = tn.getLongPath(); 647 } 648 649 return result; 650 } 651 652 TreePath[] urlsToPaths(String[] urls) { 653 ArrayList<TreePath> result = new ArrayList<>(); 654 655 for (int i = 0; i < urls.length; i++) { 656 TreePath thisOne = urlToPath(urls[i]); 657 if (thisOne == null) 658 continue; // skipped for some reason 659 else 660 result.add(thisOne); 661 } 662 663 TreePath[] res = new TreePath[result.size()]; 664 result.toArray(res); 665 return res; 666 } 667 668 TreePath urlToPath(String url) { 669 if (url == null) 670 return null; 671 672 String[] urlPath = StringArray.splitList(url, "/"); 673 TT_TreeNode[] transPath = new TT_TreeNode[urlPath.length+1]; 674 TT_BasicNode location = (TT_BasicNode)getRoot(); 675 transPath[0] = location; 676 677 for (int i = 0; i < urlPath.length; i++) { 678 if (location == null) 679 return null; // special exit condition for the loop 680 681 TT_TreeNode mnode = location.findByName(urlPath[i]); 682 if (mnode == null) { 683 return null; 684 } 685 686 transPath[i+1] = mnode; 687 location = (transPath[i+1] instanceof TT_BasicNode ? 688 (TT_BasicNode)transPath[i+1] : null); 689 } 690 691 return new TreePath(transPath); 692 } 693 694 void setParameters(Parameters p) { 695 if (p != null) { 696 params = p; 697 init(); 698 } else { 699 TestResultTable dummy = new TestResultTable(); 700 setTestResultTable(dummy); 701 702 if (debug > 0) { 703 Debug.println("TTM - dummy TRT, root = " + dummy.getRoot()); 704 } 705 } 706 } 707 708 synchronized private void init() { 709 if (params == null) { 710 return; 711 } 712 WorkDirectory wd = params.getWorkDirectory(); 713 TestSuite ts = params.getTestSuite(); 714 715 if (wd != null) { // full config w/TS and WD 716 if (debug > 0) { 717 Debug.println("TTM - initializing with workdir"); 718 } 719 log = getLog(wd); 720 TestResultTable newTrt = wd.getTestResultTable(); 721 try { 722 newTrt.getLock().lock(); 723 setTestResultTable(newTrt); 724 } finally { 725 newTrt.getLock().unlock(); 726 } 727 728 sortComparator = newTrt.getTestFinder().getComparator(); 729 } else if (ts != null) { // TS, but no workdir 730 731 if (trt != null && trt.getTestFinder() == null) { 732 // configure a finder on the temporary TRT, this allows us to 733 // populate the table before we have a workdir 734 try { 735 trt.getLock().lock(); 736 trt.setTestFinder(ts.getTestFinder()); 737 setTestResultTable(trt); 738 } finally { 739 trt.getLock().unlock(); 740 sortComparator = trt.getTestFinder().getComparator(); 741 742 } 743 744 if (debug > 0) { 745 Debug.println("TTM - params set, no WD; setting finder on temp. TRT"); //notifyFullStructure(); 746 } 747 } else { 748 // already have a finder 749 if (debug > 0) { 750 Debug.println("TTM - temp. TRT already has finder"); 751 } 752 } 753 754 755 } // no WD or TS 756 else { 757 if (debug > 0) { 758 Debug.println("TTM - params set, no WD or TS"); // should we explicitly reset the model? 759 } 760 } 761 } 762 763 void pauseWork() { 764 cacheWorker.setPaused(true); 765 // should consider synchronizing with cacheWorker before checking 766 // condition 767 while (cacheWorker != null && cacheWorker.isAlive() && !cacheWorker.stopping && !cacheWorker.isReallyPaused) { 768 try { 769 synchronized (cacheWorker) { 770 cacheWorker.notifyAll(); 771 cacheWorker.wait(); 772 } 773 } catch (InterruptedException e) { 774 } 775 } 776 } 777 778 void unpauseWork() { 779 if (cacheWorker == null) { 780 return; 781 } 782 783 invalidateNodeInfo(); 784 cacheWorker.setPaused(false); 785 synchronized (cacheWorker) { 786 cacheWorker.notifyAll(); 787 } 788 } 789 790 boolean isWorkPaused() { 791 return cacheWorker.isPaused(); 792 } 793 794 /** 795 * This method will attempt to swap the table if compatible to avoid a full JTree 796 * swap. 797 */ 798 private void setTestResultTable(TestResultTable newTrt) { 799 // NOTE: may want to run this on when we figure out how important params are 800 // see note in setParameters() 801 //if (params == null) 802 // throw IllegalStateException(); 803 804 // assumptions: 805 // - a model must have a non-null TRT 806 if (isCompatible(trt, newTrt)) { 807 swapTables(newTrt); 808 } else { 809 if (trt != null) { 810 trt.removeObserver(this); 811 } 812 trt = newTrt; 813 trt.addObserver(this); 814 } 815 816 root = new TT_BasicNode(null, (TRT_TreeNode) (trt.getRoot()), 817 (trt.getTestFinder() == null ? null : trt.getTestFinder().getComparator())); 818 819 // prime relevant nodes with root and first level 820 relevantNodes = Collections.synchronizedSet(new HashSet<TT_TreeNode>()); 821 relevantTests = Collections.synchronizedSet(new HashSet<TestResult>()); 822 823 addRelevantNode((TT_TreeNode) getRoot()); 824 TT_BasicNode tn = ((TT_BasicNode) getRoot()); 825 for (int i = 0; i < ((TT_BasicNode) getRoot()).getChildCount(); i++) { 826 addRelevantNode((TT_TreeNode) (tn.getChildAt(i))); 827 } 828 notifyFullStructure(); 829 830 if (debug > 0) { 831 Debug.println("TTM - Model watching " + trt); 832 if (trt.getWorkDir() != null) { 833 Debug.println(" -> Workdir=" + trt.getWorkDir()); 834 Debug.println(" -> Workdir path=" + trt.getWorkDir().getPath()); 835 Debug.println(" -> root = " + trt.getRoot()); 836 } 837 } 838 } 839 840 static boolean isCompatible(TestResultTable t1, TestResultTable t2) { 841 // assumptions: 842 // - a TRT must have a finder and testsuite root to be comparable 843 // - the finder class objects must be equal for TRTs to be compatible 844 // - testsuite root paths must be equal 845 if (t1 == null || t2 == null || 846 t1.getTestSuiteRoot() == null || t2.getTestSuiteRoot() == null || 847 t1.getTestFinder() == null || t2.getTestFinder() == null) { 848 if (debug > 1) { 849 Debug.println("TTM - isCompatible() false because one or both TRTs are incomplete."); 850 if (t1 != null && t2 != null) { 851 Debug.println("t1 root = " + t1.getTestSuiteRoot()); 852 Debug.println("t2 root = " + t2.getTestSuiteRoot()); 853 Debug.println("t1 finder= " + t1.getTestFinder()); 854 Debug.println("t2 finder= " + t2.getTestFinder()); 855 } 856 } 857 858 return false; 859 } else if (!t1.getTestSuiteRoot().getPath().equals(t2.getTestSuiteRoot().getPath())) { 860 if (debug > 1) { 861 Debug.println("TTM - isCompatible() failed because testsuite paths differ."); 862 } 863 return false; 864 } else if (t1.getTestFinder() != t2.getTestFinder()) { 865 if (debug > 1) { 866 Debug.println("TTM - isCompatible() failed because TestFinders differ."); 867 } 868 return false; 869 } else { 870 return true; 871 } 872 } 873 874 TestResultTable getTestResultTable() { 875 return trt; 876 } 877 878 /** 879 * Retrieve the info about the given node. The returned info object may be 880 * either a running thread or a finished thread, depending on whether recent 881 * data is available. 882 * @param node The node to get information for. 883 * @param highPriority Should the task of retrieving this information be 884 * higher than normal. 885 */ 886 TT_NodeCache getNodeInfo(TestResultTable.TreeNode node, boolean highPriority) { 887 TT_NodeCache ni = null; 888 boolean wakeWorker = false; 889 890 // this line is outside the synchronized zone to prevent a 891 // deadlock when a filter change occurs and the cache is 892 // invalidated (on the event thread) 893 TestFilter activeFilter = filterHandler.getActiveFilter(); 894 895 synchronized (htLock) { 896 ni = cache.get(node); 897 898 if (ni == null) { 899 ni = new TT_NodeCache(node, activeFilter, log); 900 cache.put(node, ni); 901 902 if (!highPriority) // high pri. case below 903 { 904 cacheQueue.addFirst(ni); 905 } 906 wakeWorker = true; // because this is a new node 907 } else if (highPriority) { 908 // reorder for high priority 909 // could cancel current task, but that would complicate things 910 int index = cacheQueue.indexOf(ni); 911 912 if (index >= 0) { 913 cacheQueue.remove(index); 914 } 915 } 916 } // synchronized 917 918 // this is done outside the htLock block to ensure proper locking 919 // order. 920 if (highPriority) { 921 cacheWorker.requestActiveNode(ni); 922 wakeWorker = true; 923 } 924 925 // forward info for persistent storage and later use... 926 if (!statsForwarded && node.isRoot() && ni.isComplete()) { 927 int[] stats = ni.getStats(); 928 int total = 0; 929 930 for (int i = 0; i < stats.length; i++) { 931 total += stats[i]; 932 } 933 total += ni.getRejectCount(); 934 935 if (params != null) { 936 WorkDirectory wd = params.getWorkDirectory(); 937 938 if (wd != null) { 939 wd.setTestSuiteTestCount(total); 940 statsForwarded = true; 941 } 942 } 943 } 944 945 if (wakeWorker) { 946 synchronized (cacheWorker) { 947 cacheWorker.notify(); 948 } 949 } 950 return ni; 951 } 952 953 /** 954 * Invalidate any collected data about any of the nodes on the given path. 955 * This operation includes stopping any threads scanning for info and 956 * removing the entries from the cache. 957 * 958 * @param path Nodes to invalidate in the cache, must not be null. 959 * May be length zero. 960 */ 961 void invalidateNodeInfo(TestResultTable.TreeNode[] path) { 962 // do this as a batch 963 // we can reverse the sync and for if stalls are noticeable 964 synchronized (htLock) { 965 for (int i = 0; i < path.length; i++) { 966 TT_NodeCache info = cache.get(path[i]); 967 968 if (info != null) { 969 if (debug > 1) { 970 Debug.println("TTM - halting thread and removed from node cache"); 971 Debug.println(" -> " + path[i]); 972 Debug.println(" -> " + info); 973 } 974 975 info.halt(); 976 cache.remove(info.getNode()); 977 boolean wasInQueue = cacheQueue.remove(info); 978 979 info = new TT_NodeCache(info.getNode(), filterHandler.getActiveFilter(), log); 980 cache.put(info.getNode(), info); 981 cacheQueue.addFirst(info); 982 } 983 } // for 984 } // synchronized 985 986 // wake up the worker 987 synchronized (cacheWorker) { 988 cacheWorker.notify(); 989 } 990 } 991 992 /** 993 * Invalidate all collected node information. 994 * This is likely used when the internal contents of the parameters have 995 * changed. If the cache is not invalidated at certain points, rendering 996 * of the tree may be incorrect. 997 */ 998 void invalidateNodeInfo() { 999 synchronized (htLock) { 1000 Enumeration<TestResultTable.TreeNode> e = cache.keys(); 1001 while (e.hasMoreElements()) { 1002 (cache.get(e.nextElement())).invalidate(); 1003 } // while 1004 1005 cache = new Hashtable<>(); 1006 cacheQueue = new LinkedList<>(); 1007 suspendedQueue = new LinkedList<>(); 1008 } 1009 1010 // reprocess any needed nodes 1011 Iterator<TT_TreeNode> it = relevantNodes.iterator(); 1012 while (it.hasNext()) { 1013 TT_TreeNode tn = it.next(); 1014 if (tn instanceof TT_BasicNode) { 1015 getNodeInfo(((TT_BasicNode) tn).getTableNode(), false); 1016 } 1017 } // while 1018 } 1019 1020 /** 1021 * Invalidate any collected data about the given node. 1022 * This operation includes stopping any thread scanning for info and 1023 * removing the entry from the cache. 1024 * 1025 * @param node The node to invalidate in the cache. Must not be null. 1026 * @deprecated The cache will be smart enough to not need this. 1027 */ 1028 void invalidateNodeInfo(TestResultTable.TreeNode node) { 1029 invalidateNodeInfo(new TestResultTable.TreeNode[]{node}); 1030 } 1031 1032 /** 1033 * Trusting method which assumes that the given TRT is compatible with the 1034 * current one. Strange things will happen if it is not! 1035 */ 1036 void swapTables(TestResultTable newTrt) { 1037 if (newTrt == trt || newTrt == null) { 1038 return; 1039 } 1040 if (debug > 1) { 1041 Debug.println("Swapping TRTs under the covers."); 1042 Debug.println(" -> OLD=" + trt); 1043 Debug.println(" -> NEW=" + newTrt); 1044 } 1045 1046 trt.removeObserver(this); 1047 trt = newTrt; 1048 trt.addObserver(this); 1049 } 1050 1051 private void notifyModelListeners(TreeModelEvent e, int eType) { 1052 if (treeModelListeners != null) { 1053 Notifier n = new Notifier(eType, treeModelListeners, e, uif); 1054 if (!EventQueue.isDispatchThread()) { 1055 EventQueue.invokeLater(n); 1056 } 1057 else { 1058 n.run(); 1059 } 1060 } 1061 } 1062 1063 1064 void notifyFullStructure() { 1065 if (debug > 0) { 1066 Debug.println("TTM - sending full structure change event to model listeners."); 1067 } 1068 invalidateNodeInfo(); 1069 1070 Object[] path = {getRoot()}; 1071 TreeModelEvent e = new TreeModelEvent(this, path); 1072 notifyModelListeners(e, Notifier.STRUCT); 1073 } 1074 1075 protected void finalize() throws Throwable { 1076 super.finalize(); 1077 1078 if (trt != null) { 1079 trt.removeObserver(this); 1080 } 1081 } 1082 1083 private Logger getLog(WorkDirectory wd) { 1084 Logger log = null; 1085 final String logName = uif.getI18NString("tree.log.name"); 1086 try { 1087 log = wd.getTestSuite().createLog(wd, null, logName); 1088 } 1089 catch (TestSuite.DuplicateLogNameFault f) { 1090 try { 1091 log = wd.getTestSuite().getLog(wd, logName); 1092 } 1093 catch (TestSuite.NoSuchLogFault f2) { } 1094 } 1095 return log; 1096 } 1097 1098 private TT_BasicNode root; 1099 private UIFactory uif; 1100 private TestResultTable trt; 1101 private Parameters params; 1102 private FilterSelectionHandler filterHandler; 1103 private TestFilter lastFilter; 1104 private Comparator<String> sortComparator; 1105 private TreeModelListener[] treeModelListeners = new TreeModelListener[0]; 1106 private boolean statsForwarded; 1107 private boolean disposed; 1108 private CacheWorker cacheWorker; 1109 private FilterWatcher watcher; 1110 private Set<TT_TreeNode> relevantNodes; 1111 private Set<TestResult> relevantTests; // not used anymore 1112 1113 private Logger log; 1114 /** 1115 * Stores state info about individual nodes. 1116 * The key is a TestResultTable.TreeNode, and the value is a TT_NodeCache 1117 * object. Use the htLock when accessing the cache. 1118 */ 1119 protected Hashtable<TestResultTable.TreeNode, TT_NodeCache> cache; 1120 /** 1121 * Queue of items to be processed. 1122 * "First" is the most recently added, "last" is the next to be processed. 1123 */ 1124 protected LinkedList<TT_NodeCache> cacheQueue; 1125 /** 1126 * Queue of items which are in the middle of being processed. 1127 * "First" is the most recently added, "last" is the next to be processed. 1128 */ 1129 protected LinkedList<TT_NodeCache> suspendedQueue; 1130 protected final Object htLock = new Object(); 1131 private static final int CACHE_THREAD_PRI = Thread.MIN_PRIORITY; 1132 private static final int CACHE_NOTI_THR_PRI = Thread.MIN_PRIORITY; 1133 protected static int debug = Debug.getInt(TestTreeModel.class); 1134 1135 // ************** inner classes *************** 1136 private class CacheWorker extends Thread { 1137 1138 CacheWorker() { 1139 super("Test Tree Cache Worker"); 1140 } 1141 1142 public void run() { 1143 try { 1144 synchronized (CacheWorker.this) { 1145 wait(); 1146 } 1147 } catch (InterruptedException e) { 1148 // we're terminated I guess 1149 return; 1150 } 1151 1152 while (!stopping) { 1153 if (paused) { 1154 try { 1155 synchronized (CacheWorker.this) { 1156 isReallyPaused = true; 1157 notifyAll(); 1158 wait(); 1159 isReallyPaused = false; 1160 } 1161 continue; 1162 } catch (InterruptedException e) { 1163 // we're terminated I guess 1164 stopping = true; 1165 isReallyPaused = false; 1166 continue; 1167 } // poll the queue for work to do 1168 } 1169 currentUnit = getNextUnit(); 1170 1171 // nothing to do, wait 1172 if (currentUnit == null) { 1173 try { 1174 synchronized (CacheWorker.this) { 1175 notifyAll(); 1176 wait(); 1177 } 1178 continue; 1179 } // try 1180 catch (InterruptedException e) { 1181 // we're terminated I guess 1182 stopping = true; 1183 continue; 1184 } // catch 1185 } else { 1186 if (!currentUnit.canRun()) { 1187 //throw new IllegalStateException("cache malfunction - attempting to re-populate node " + currentUnit.isPaused() + " " + currentUnit.isValid() + " " + currentUnit.isComplete()); 1188 continue; 1189 } 1190 1191 // do the work 1192 if (debug > 0) { 1193 Debug.println("TTM cache processing " + currentUnit.getNode().getName()); 1194 } 1195 currentUnit.run(); 1196 1197 if (!currentUnit.isPaused() && currentUnit.isValid()) { 1198 finishJob(currentUnit); 1199 } 1200 } 1201 } // while 1202 1203 this.currentUnit = null; 1204 this.priorityUnit = null; 1205 1206 } 1207 1208 /** 1209 * Only call this to terminate all cache activity. 1210 * There is currently no way to restart the cache. 1211 */ 1212 /* should not override Thread.interrupt 1213 public void interrupt() { 1214 stopping = true; 1215 } 1216 */ 1217 /** 1218 * Find out which node is currently being processed. 1219 * @return The node which is currently being worked on, null if no work is 1220 * in progress. 1221 */ 1222 public TT_NodeCache getActiveNode() { 1223 return currentUnit; 1224 } 1225 1226 void setPaused(boolean state) { 1227 synchronized (this) { 1228 if (state != paused) { 1229 paused = state; 1230 if (paused && currentUnit != null) { 1231 currentUnit.pause(); 1232 suspendedQueue.addFirst(currentUnit); 1233 } 1234 1235 // wake up 1236 if (!paused) { 1237 this.notify(); 1238 } 1239 } 1240 } 1241 } 1242 1243 synchronized boolean isPaused() { 1244 return paused; 1245 } 1246 // ------- private ------- 1247 /** 1248 * Request that the worker process the given unit ASAP. 1249 * This method will first verify that the given node is a candidate for 1250 * work. This method is synchronized to avoid context switching into 1251 * other parts of this class. 1252 * @param what Which node to give attention to. 1253 */ 1254 synchronized void requestActiveNode(TT_NodeCache what) { 1255 if (what != null && currentUnit != what && what.canRun()) { 1256 // This will cause processing on the worker thread to terminate 1257 // and it will do selection via getNextUnit(). That will return 1258 // the priority unit below. 1259 priorityUnit = what; 1260 if (currentUnit != null) { 1261 currentUnit.pause(); 1262 suspendedQueue.addFirst(currentUnit); 1263 } 1264 } 1265 } 1266 1267 /** 1268 * From the available queues to be processed, select the most ripe. 1269 */ 1270 private TT_NodeCache getNextUnit() { 1271 boolean wasPriority = false; 1272 TT_NodeCache next = null; 1273 1274 synchronized (htLock) { 1275 // schedule next node in this priority: 1276 // 1) a priority unit 1277 // 2) a previously suspended unit 1278 // 3) the next unit in the queue 1279 if (priorityUnit != null) { 1280 wasPriority = true; 1281 next = priorityUnit; 1282 priorityUnit = null; 1283 } else { 1284 switch (SCHEDULING_ALGO) { 1285 case QUEUE: 1286 next = selectByQueuing(); 1287 break; 1288 case DEPTH: 1289 next = selectByDepth(); 1290 break; 1291 default: 1292 next = selectByQueuing(); 1293 } 1294 } 1295 1296 // additional selection criteria 1297 if (next != null && !next.canRun()) // discard unrunnable jobs 1298 { 1299 return getNextUnit(); 1300 } else // ok, schedule this one 1301 { 1302 return next; 1303 } 1304 } // sync 1305 } 1306 1307 // -------- various scheduling algorithms ---------- 1308 /** 1309 * Simply do it in the order the requests came in. 1310 * The root is excluded from selection until the last possible 1311 * time. 1312 */ 1313 private TT_NodeCache selectByQueuing() { 1314 TT_NodeCache selection = null; 1315 1316 if (suspendedQueue.size() > 0) { 1317 selection = suspendedQueue.removeLast(); 1318 } else if (cacheQueue.size() > 0) { 1319 selection = cacheQueue.removeLast(); 1320 } 1321 if (selection != null && 1322 selection.getNode().isRoot() && // trying to avoid root 1323 (cacheQueue.size() > 0 || suspendedQueue.size() > 0)) { 1324 cacheQueue.addFirst(selection); 1325 return selectByQueuing(); 1326 } 1327 1328 return selection; 1329 } 1330 1331 /** 1332 * Select the deepest node in the cacheQueue. 1333 */ 1334 private TT_NodeCache selectByDepth() { 1335 // note must already have htLock 1336 // note with this algo., the suspend and cache queues are 1337 // basically equivalent 1338 TT_NodeCache selected = null; 1339 int depth = -1; 1340 LinkedList<TT_NodeCache> theList = cacheQueue; 1341 boolean notDone = true; 1342 int count = 0; 1343 1344 if (cacheQueue.size() == 0) { 1345 if (suspendedQueue.size() == 0) { 1346 notDone = false; 1347 } else { 1348 theList = suspendedQueue; 1349 } 1350 } else { 1351 } 1352 1353 while (notDone) { 1354 TT_NodeCache possible = (theList.get(count)); 1355 int thisDepth = TestResultTable.getObjectPath(possible.getNode()).length; 1356 1357 if (thisDepth > depth) { 1358 theList.remove(count); 1359 1360 // requeue the last deepest node found 1361 if (selected != null) { 1362 cacheQueue.addFirst(selected); 1363 // adjust the counter since we just added one 1364 if (theList == cacheQueue) { 1365 count++; 1366 } 1367 } 1368 depth = thisDepth; 1369 selected = possible; 1370 } 1371 1372 count++; 1373 if (count >= theList.size()) { 1374 if (theList == suspendedQueue) { 1375 notDone = false; 1376 } else if (suspendedQueue.size() != 0) { 1377 theList = suspendedQueue; 1378 count = 0; 1379 } else { 1380 notDone = false; 1381 } 1382 } else { 1383 } 1384 } // while 1385 1386 return selected; 1387 } 1388 1389 private synchronized void finishJob(TT_NodeCache item) { 1390 // do not send update message if it's not important (onscreen) 1391 /* if (true || !relevantNodes.contains(item.getNode())) { 1392 return; 1393 }*/ 1394 TreeModelEvent e = null; 1395 TestResultTable.TreeNode node = item.getNode(); 1396 1397 // switch event format if the node is the root 1398 if (node.isRoot() && getRoot() != null) { 1399 e = new TreeModelEvent(this, new Object[]{getRoot()}, null, null); 1400 } else { 1401 // full path to the node, inclusive 1402 //TestResultTable.TreeNode[] fp = TestResultTable.getObjectPath(node); 1403 1404 // partial path - for JTree event 1405 //TestResultTable.TreeNode[] pp = new TestResultTable.TreeNode[fp.length - 1]; 1406 //System.arraycopy(fp, 0, pp, 0, fp.length - 1); 1407 TreePath path = resolveUrl(TestResultTable.getRootRelativePath(node)); 1408 if (path != null) { 1409 // index in parent of target node - required by JTree 1410 // ignore this operation if we are at the root (length==1) 1411 TreePath pp = path.getParentPath(); 1412 TT_BasicNode bn = (TT_BasicNode)(path.getLastPathComponent()); 1413 int index = ((TT_BasicNode)(pp.getLastPathComponent())).getIndex(bn); 1414 1415 //e = makeEvent(pp, bn, index); 1416 e = new TreeModelEvent(this, pp, new int[] {index}, new Object[] {bn}); 1417 } 1418 } 1419 1420 if (e != null) { 1421 // dispatch event 1422 notifyModelListeners(e, Notifier.CHANGE); 1423 1424 if (debug > 0) { 1425 Debug.println("NodeCache done for " + item.getNode().getName()); 1426 } 1427 } 1428 } 1429 private volatile boolean paused; 1430 private volatile boolean stopping; 1431 private volatile TT_NodeCache priorityUnit; 1432 private volatile TT_NodeCache currentUnit; 1433 private volatile boolean isReallyPaused; 1434 private static final int QUEUE = 0; 1435 private static final int DEPTH = 1; 1436 private static final int SCHEDULING_ALGO = DEPTH; 1437 } 1438 1439 // inner class 1440 /** 1441 * Object used to physically dispatch any model update events onto the event thread. 1442 */ 1443 private static class Notifier implements Runnable { 1444 1445 /** 1446 * Create a event notification object to be scheduled on the GUI event thread. 1447 * The type translates to a switch between the different possible observer 1448 * methods. 1449 * 1450 * @param eType Type of observer message to generate. 1451 * Must not be greater than zero, see the defined constants. 1452 * @param listeners The listeners to notify. This is shallow copied immediately. 1453 * Must not be null. May be of zero length. 1454 * @param e The event to give to the listeners. 1455 * Must not be null. 1456 */ 1457 Notifier(int eType, TreeModelListener[] listeners, TreeModelEvent e, 1458 UIFactory uif) { 1459 type = eType; 1460 this.e = e; 1461 this.uif = uif; 1462 1463 // make shallow copy 1464 TreeModelListener[] copy = new TreeModelListener[listeners.length]; 1465 System.arraycopy(listeners, 0, copy, 0, listeners.length); 1466 l = copy; 1467 } 1468 1469 public void run() { 1470 if (e == null) 1471 return; 1472 1473 switch (type) { 1474 case CHANGE: 1475 Object[] path = e.getPath(); 1476 if (path != null && path.length > 0 && 1477 path[path.length-1] instanceof TT_BasicNode) { 1478 TT_BasicNode bn = (TT_BasicNode)(path[path.length-1]); 1479 if (e.getChildIndices() != null && 1480 e.getChildIndices().length >= 1) { 1481 if (bn.getChildCount() <= e.getChildIndices()[0]) 1482 return; 1483 } 1484 } 1485 1486 for (int i = 0; i < l.length; i++) { 1487 l[i].treeNodesChanged(e); 1488 } 1489 break; 1490 case STRUCT: 1491 for (int i = 0; i < l.length; i++) { 1492 l[i].treeStructureChanged(e); 1493 } 1494 break; 1495 case INS: 1496 for (int i = 0; i < l.length; i++) { 1497 l[i].treeNodesInserted(e); 1498 } 1499 break; 1500 case DEL: 1501 for (int i = 0; i < l.length; i++) { 1502 l[i].treeNodesRemoved(e); 1503 } 1504 break; 1505 default: 1506 throw new JavaTestError(uif.getI18NString("tree.noEType")); 1507 } // switch 1508 } 1509 TreeModelListener[] l; 1510 int type; 1511 TreeModelEvent e; 1512 UIFactory uif; 1513 static final int CHANGE = 0; 1514 static final int STRUCT = 1; 1515 static final int INS = 2; 1516 static final int DEL = 3; 1517 } 1518 1519 // FilterSelectionHandler.Observer - may not be on event thread 1520 private class FilterWatcher implements FilterSelectionHandler.Observer { 1521 1522 public void filterUpdated(TestFilter f) { 1523 //notifyFullStructure(); 1524 invalidateNodeInfo(); 1525 } 1526 1527 public void filterSelected(TestFilter f) { 1528 //notifyFullStructure(); 1529 if (!lastFilter.equals(f)) { 1530 invalidateNodeInfo(); 1531 } 1532 lastFilter = f; 1533 } 1534 1535 public void filterAdded(TestFilter f) { 1536 // don't care 1537 } 1538 1539 public void filterRemoved(TestFilter f) { 1540 // don't care 1541 } 1542 } 1543 } 1544