1 /*
   2  * Copyright (c) 2013, 2016, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 package jdk.test.lib;
  25 
  26 import java.io.File;
  27 import java.io.IOException;
  28 import java.net.InetAddress;
  29 import java.net.MalformedURLException;
  30 import java.net.ServerSocket;
  31 import java.net.URL;
  32 import java.net.URLClassLoader;
  33 import java.net.UnknownHostException;
  34 import java.nio.file.Files;
  35 import java.nio.file.Path;
  36 import java.nio.file.Paths;
  37 import java.util.ArrayList;
  38 import java.util.Arrays;
  39 import java.util.Collection;
  40 import java.util.Collections;
  41 import java.util.Iterator;
  42 import java.util.Map;
  43 import java.util.HashMap;
  44 import java.util.List;
  45 import java.util.Objects;
  46 import java.util.Random;
  47 import java.util.function.BooleanSupplier;
  48 import java.util.concurrent.TimeUnit;
  49 import java.util.function.Consumer;
  50 import java.util.function.Function;
  51 import java.util.regex.Matcher;
  52 import java.util.regex.Pattern;
  53 
  54 import static jdk.test.lib.Asserts.assertTrue;
  55 import jdk.test.lib.process.ProcessTools;
  56 import jdk.test.lib.process.OutputAnalyzer;
  57 
  58 /**
  59  * Common library for various test helper functions.
  60  */
  61 public final class Utils {
  62 
  63     /**
  64      * Returns the value of 'test.class.path' system property.
  65      */
  66     public static final String TEST_CLASS_PATH = System.getProperty("test.class.path", ".");
  67 
  68     /**
  69      * Returns the sequence used by operating system to separate lines.
  70      */
  71     public static final String NEW_LINE = System.getProperty("line.separator");
  72 
  73     /**
  74      * Returns the value of 'test.vm.opts' system property.
  75      */
  76     public static final String VM_OPTIONS = System.getProperty("test.vm.opts", "").trim();
  77 
  78     /**
  79      * Returns the value of 'test.java.opts' system property.
  80      */
  81     public static final String JAVA_OPTIONS = System.getProperty("test.java.opts", "").trim();
  82 
  83     /**
  84      * Returns the value of 'test.src' system property.
  85      */
  86     public static final String TEST_SRC = System.getProperty("test.src", "").trim();
  87 
  88     /*
  89      * Returns the value of 'test.jdk' system property
  90      */
  91     public static final String TEST_JDK = System.getProperty("test.jdk");
  92 
  93     /**
  94      * Returns the value of 'test.classes' system property
  95      */
  96     public static final String TEST_CLASSES = System.getProperty("test.classes", ".");
  97     /**
  98      * Defines property name for seed value.
  99      */
 100     public static final String SEED_PROPERTY_NAME = "jdk.test.lib.random.seed";
 101 
 102     /* (non-javadoc)
 103      * Random generator with (or without) predefined seed. Depends on
 104      * "jdk.test.lib.random.seed" property value.
 105      */
 106     private static volatile Random RANDOM_GENERATOR;
 107 
 108     /**
 109      * Contains the seed value used for {@link java.util.Random} creation.
 110      */
 111     public static final long SEED = Long.getLong(SEED_PROPERTY_NAME, new Random().nextLong());
 112     /**
 113     * Returns the value of 'test.timeout.factor' system property
 114     * converted to {@code double}.
 115     */
 116     public static final double TIMEOUT_FACTOR;
 117     static {
 118         String toFactor = System.getProperty("test.timeout.factor", "1.0");
 119         TIMEOUT_FACTOR = Double.parseDouble(toFactor);
 120     }
 121 
 122     /**
 123     * Returns the value of JTREG default test timeout in milliseconds
 124     * converted to {@code long}.
 125     */
 126     public static final long DEFAULT_TEST_TIMEOUT = TimeUnit.SECONDS.toMillis(120);
 127 
 128     private Utils() {
 129         // Private constructor to prevent class instantiation
 130     }
 131 
 132     /**
 133      * Returns the list of VM options.
 134      *
 135      * @return List of VM options
 136      */
 137     public static List<String> getVmOptions() {
 138         return Arrays.asList(safeSplitString(VM_OPTIONS));
 139     }
 140 
 141     /**
 142      * Returns the list of VM options with -J prefix.
 143      *
 144      * @return The list of VM options with -J prefix
 145      */
 146     public static List<String> getForwardVmOptions() {
 147         String[] opts = safeSplitString(VM_OPTIONS);
 148         for (int i = 0; i < opts.length; i++) {
 149             opts[i] = "-J" + opts[i];
 150         }
 151         return Arrays.asList(opts);
 152     }
 153 
 154     /**
 155      * Returns the default JTReg arguments for a jvm running a test.
 156      * This is the combination of JTReg arguments test.vm.opts and test.java.opts.
 157      * @return An array of options, or an empty array if no options.
 158      */
 159     public static String[] getTestJavaOpts() {
 160         List<String> opts = new ArrayList<String>();
 161         Collections.addAll(opts, safeSplitString(VM_OPTIONS));
 162         Collections.addAll(opts, safeSplitString(JAVA_OPTIONS));
 163         return opts.toArray(new String[0]);
 164     }
 165 
 166     /**
 167      * Combines given arguments with default JTReg arguments for a jvm running a test.
 168      * This is the combination of JTReg arguments test.vm.opts and test.java.opts
 169      * @return The combination of JTReg test java options and user args.
 170      */
 171     public static String[] addTestJavaOpts(String... userArgs) {
 172         List<String> opts = new ArrayList<String>();
 173         Collections.addAll(opts, getTestJavaOpts());
 174         Collections.addAll(opts, userArgs);
 175         return opts.toArray(new String[0]);
 176     }
 177 
 178     /**
 179      * Removes any options specifying which GC to use, for example "-XX:+UseG1GC".
 180      * Removes any options matching: -XX:(+/-)Use*GC
 181      * Used when a test need to set its own GC version. Then any
 182      * GC specified by the framework must first be removed.
 183      * @return A copy of given opts with all GC options removed.
 184      */
 185     private static final Pattern useGcPattern = Pattern.compile(
 186             "(?:\\-XX\\:[\\+\\-]Use.+GC)"
 187             + "|(?:\\-Xconcgc)");
 188     public static List<String> removeGcOpts(List<String> opts) {
 189         List<String> optsWithoutGC = new ArrayList<String>();
 190         for (String opt : opts) {
 191             if (useGcPattern.matcher(opt).matches()) {
 192                 System.out.println("removeGcOpts: removed " + opt);
 193             } else {
 194                 optsWithoutGC.add(opt);
 195             }
 196         }
 197         return optsWithoutGC;
 198     }
 199 
 200     /**
 201      * Returns the default JTReg arguments for a jvm running a test without
 202      * options that matches regular expressions in {@code filters}.
 203      * This is the combination of JTReg arguments test.vm.opts and test.java.opts.
 204      * @param filters Regular expressions used to filter out options.
 205      * @return An array of options, or an empty array if no options.
 206      */
 207     public static String[] getFilteredTestJavaOpts(String... filters) {
 208         String options[] = getTestJavaOpts();
 209 
 210         if (filters.length == 0) {
 211             return options;
 212         }
 213 
 214         List<String> filteredOptions = new ArrayList<String>(options.length);
 215         Pattern patterns[] = new Pattern[filters.length];
 216         for (int i = 0; i < filters.length; i++) {
 217             patterns[i] = Pattern.compile(filters[i]);
 218         }
 219 
 220         for (String option : options) {
 221             boolean matched = false;
 222             for (int i = 0; i < patterns.length && !matched; i++) {
 223                 Matcher matcher = patterns[i].matcher(option);
 224                 matched = matcher.find();
 225             }
 226             if (!matched) {
 227                 filteredOptions.add(option);
 228             }
 229         }
 230 
 231         return filteredOptions.toArray(new String[filteredOptions.size()]);
 232     }
 233 
 234     /**
 235      * Splits a string by white space.
 236      * Works like String.split(), but returns an empty array
 237      * if the string is null or empty.
 238      */
 239     private static String[] safeSplitString(String s) {
 240         if (s == null || s.trim().isEmpty()) {
 241             return new String[] {};
 242         }
 243         return s.trim().split("\\s+");
 244     }
 245 
 246     /**
 247      * @return The full command line for the ProcessBuilder.
 248      */
 249     public static String getCommandLine(ProcessBuilder pb) {
 250         StringBuilder cmd = new StringBuilder();
 251         for (String s : pb.command()) {
 252             cmd.append(s).append(" ");
 253         }
 254         return cmd.toString();
 255     }
 256 
 257     /**
 258      * Returns the free port on the local host.
 259      * The function will spin until a valid port number is found.
 260      *
 261      * @return The port number
 262      * @throws InterruptedException if any thread has interrupted the current thread
 263      * @throws IOException if an I/O error occurs when opening the socket
 264      */
 265     public static int getFreePort() throws InterruptedException, IOException {
 266         int port = -1;
 267 
 268         while (port <= 0) {
 269             Thread.sleep(100);
 270 
 271             ServerSocket serverSocket = null;
 272             try {
 273                 serverSocket = new ServerSocket(0);
 274                 port = serverSocket.getLocalPort();
 275             } finally {
 276                 serverSocket.close();
 277             }
 278         }
 279 
 280         return port;
 281     }
 282 
 283     /**
 284      * Returns the name of the local host.
 285      *
 286      * @return The host name
 287      * @throws UnknownHostException if IP address of a host could not be determined
 288      */
 289     public static String getHostname() throws UnknownHostException {
 290         InetAddress inetAddress = InetAddress.getLocalHost();
 291         String hostName = inetAddress.getHostName();
 292 
 293         assertTrue((hostName != null && !hostName.isEmpty()),
 294                 "Cannot get hostname");
 295 
 296         return hostName;
 297     }
 298 
 299     /**
 300      * Uses "jcmd -l" to search for a jvm pid. This function will wait
 301      * forever (until jtreg timeout) for the pid to be found.
 302      * @param key Regular expression to search for
 303      * @return The found pid.
 304      */
 305     public static int waitForJvmPid(String key) throws Throwable {
 306         final long iterationSleepMillis = 250;
 307         System.out.println("waitForJvmPid: Waiting for key '" + key + "'");
 308         System.out.flush();
 309         while (true) {
 310             int pid = tryFindJvmPid(key);
 311             if (pid >= 0) {
 312                 return pid;
 313             }
 314             Thread.sleep(iterationSleepMillis);
 315         }
 316     }
 317 
 318     /**
 319      * Searches for a jvm pid in the output from "jcmd -l".
 320      *
 321      * Example output from jcmd is:
 322      * 12498 sun.tools.jcmd.JCmd -l
 323      * 12254 /tmp/jdk8/tl/jdk/JTwork/classes/com/sun/tools/attach/Application.jar
 324      *
 325      * @param key A regular expression to search for.
 326      * @return The found pid, or -1 if not found.
 327      * @throws Exception If multiple matching jvms are found.
 328      */
 329     public static int tryFindJvmPid(String key) throws Throwable {
 330         OutputAnalyzer output = null;
 331         try {
 332             JDKToolLauncher jcmdLauncher = JDKToolLauncher.create("jcmd");
 333             jcmdLauncher.addToolArg("-l");
 334             output = ProcessTools.executeProcess(jcmdLauncher.getCommand());
 335             output.shouldHaveExitValue(0);
 336 
 337             // Search for a line starting with numbers (pid), follwed by the key.
 338             Pattern pattern = Pattern.compile("([0-9]+)\\s.*(" + key + ").*\\r?\\n");
 339             Matcher matcher = pattern.matcher(output.getStdout());
 340 
 341             int pid = -1;
 342             if (matcher.find()) {
 343                 pid = Integer.parseInt(matcher.group(1));
 344                 System.out.println("findJvmPid.pid: " + pid);
 345                 if (matcher.find()) {
 346                     throw new Exception("Found multiple JVM pids for key: " + key);
 347                 }
 348             }
 349             return pid;
 350         } catch (Throwable t) {
 351             System.out.println(String.format("Utils.findJvmPid(%s) failed: %s", key, t));
 352             throw t;
 353         }
 354     }
 355 
 356     /**
 357      * Adjusts the provided timeout value for the TIMEOUT_FACTOR
 358      * @param tOut the timeout value to be adjusted
 359      * @return The timeout value adjusted for the value of "test.timeout.factor"
 360      *         system property
 361      */
 362     public static long adjustTimeout(long tOut) {
 363         return Math.round(tOut * Utils.TIMEOUT_FACTOR);
 364     }
 365 
 366     /**
 367      * Return the contents of the named file as a single String,
 368      * or null if not found.
 369      * @param filename name of the file to read
 370      * @return String contents of file, or null if file not found.
 371      * @throws  IOException
 372      *          if an I/O error occurs reading from the file or a malformed or
 373      *          unmappable byte sequence is read
 374      */
 375     public static String fileAsString(String filename) throws IOException {
 376         Path filePath = Paths.get(filename);
 377         if (!Files.exists(filePath)) return null;
 378         return new String(Files.readAllBytes(filePath));
 379     }
 380 
 381     private static final char[] hexArray = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
 382 
 383     /**
 384      * Returns hex view of byte array
 385      *
 386      * @param bytes byte array to process
 387      * @return Space separated hexadecimal string representation of bytes
 388      */
 389 
 390     public static String toHexString(byte[] bytes) {
 391         char[] hexView = new char[bytes.length * 3];
 392         int i = 0;
 393         for (byte b : bytes) {
 394             hexView[i++] = hexArray[(b >> 4) & 0x0F];
 395             hexView[i++] = hexArray[b & 0x0F];
 396             hexView[i++] = ' ';
 397         }
 398         return new String(hexView);
 399     }
 400 
 401     /**
 402      * Returns {@link java.util.Random} generator initialized with particular seed.
 403      * The seed could be provided via system property {@link Utils#SEED_PROPERTY_NAME}
 404      * In case no seed is provided, the method uses a random number.
 405      * The used seed printed to stdout.
 406      * @return {@link java.util.Random} generator with particular seed.
 407      */
 408     public static Random getRandomInstance() {
 409         if (RANDOM_GENERATOR == null) {
 410             synchronized (Utils.class) {
 411                 if (RANDOM_GENERATOR == null) {
 412                     RANDOM_GENERATOR = new Random(SEED);
 413                     System.out.printf("For random generator using seed: %d%n", SEED);
 414                     System.out.printf("To re-run test with same seed value please add \"-D%s=%d\" to command line.%n", SEED_PROPERTY_NAME, SEED);
 415                 }
 416             }
 417         }
 418         return RANDOM_GENERATOR;
 419     }
 420 
 421     /**
 422      * Returns random element of non empty collection
 423      *
 424      * @param <T> a type of collection element
 425      * @param collection collection of elements
 426      * @return random element of collection
 427      * @throws IllegalArgumentException if collection is empty
 428      */
 429     public static <T> T getRandomElement(Collection<T> collection)
 430             throws IllegalArgumentException {
 431         if (collection.isEmpty()) {
 432             throw new IllegalArgumentException("Empty collection");
 433         }
 434         Random random = getRandomInstance();
 435         int elementIndex = 1 + random.nextInt(collection.size() - 1);
 436         Iterator<T> iterator = collection.iterator();
 437         while (--elementIndex != 0) {
 438             iterator.next();
 439         }
 440         return iterator.next();
 441     }
 442 
 443     /**
 444      * Returns random element of non empty array
 445      *
 446      * @param <T> a type of array element
 447      * @param array array of elements
 448      * @return random element of array
 449      * @throws IllegalArgumentException if array is empty
 450      */
 451     public static <T> T getRandomElement(T[] array)
 452             throws IllegalArgumentException {
 453         if (array == null || array.length == 0) {
 454             throw new IllegalArgumentException("Empty or null array");
 455         }
 456         Random random = getRandomInstance();
 457         return array[random.nextInt(array.length)];
 458     }
 459 
 460     /**
 461      * Wait for condition to be true
 462      *
 463      * @param condition, a condition to wait for
 464      */
 465     public static final void waitForCondition(BooleanSupplier condition) {
 466         waitForCondition(condition, -1L, 100L);
 467     }
 468 
 469     /**
 470      * Wait until timeout for condition to be true
 471      *
 472      * @param condition, a condition to wait for
 473      * @param timeout a time in milliseconds to wait for condition to be true
 474      * specifying -1 will wait forever
 475      * @return condition value, to determine if wait was successful
 476      */
 477     public static final boolean waitForCondition(BooleanSupplier condition,
 478             long timeout) {
 479         return waitForCondition(condition, timeout, 100L);
 480     }
 481 
 482     /**
 483      * Wait until timeout for condition to be true for specified time
 484      *
 485      * @param condition, a condition to wait for
 486      * @param timeout a time in milliseconds to wait for condition to be true,
 487      * specifying -1 will wait forever
 488      * @param sleepTime a time to sleep value in milliseconds
 489      * @return condition value, to determine if wait was successful
 490      */
 491     public static final boolean waitForCondition(BooleanSupplier condition,
 492             long timeout, long sleepTime) {
 493         long startTime = System.currentTimeMillis();
 494         while (!(condition.getAsBoolean() || (timeout != -1L
 495                 && ((System.currentTimeMillis() - startTime) > timeout)))) {
 496             try {
 497                 Thread.sleep(sleepTime);
 498             } catch (InterruptedException e) {
 499                 Thread.currentThread().interrupt();
 500                 throw new Error(e);
 501             }
 502         }
 503         return condition.getAsBoolean();
 504     }
 505 
 506     /**
 507      * Interface same as java.lang.Runnable but with
 508      * method {@code run()} able to throw any Throwable.
 509      */
 510     public static interface ThrowingRunnable {
 511         void run() throws Throwable;
 512     }
 513 
 514     /**
 515      * Filters out an exception that may be thrown by the given
 516      * test according to the given filter.
 517      *
 518      * @param test - method that is invoked and checked for exception.
 519      * @param filter - function that checks if the thrown exception matches
 520      *                 criteria given in the filter's implementation.
 521      * @return - exception that matches the filter if it has been thrown or
 522      *           {@code null} otherwise.
 523      * @throws Throwable - if test has thrown an exception that does not
 524      *                     match the filter.
 525      */
 526     public static Throwable filterException(ThrowingRunnable test,
 527             Function<Throwable, Boolean> filter) throws Throwable {
 528         try {
 529             test.run();
 530         } catch (Throwable t) {
 531             if (filter.apply(t)) {
 532                 return t;
 533             } else {
 534                 throw t;
 535             }
 536         }
 537         return null;
 538     }
 539 
 540     /**
 541      * Ensures a requested class is loaded
 542      * @param aClass class to load
 543      */
 544     public static void ensureClassIsLoaded(Class<?> aClass) {
 545         if (aClass == null) {
 546             throw new Error("Requested null class");
 547         }
 548         try {
 549             Class.forName(aClass.getName(), /* initialize = */ true,
 550                     ClassLoader.getSystemClassLoader());
 551         } catch (ClassNotFoundException e) {
 552             throw new Error("Class not found", e);
 553         }
 554     }
 555     /**
 556      * @param parent a class loader to be the parent for the returned one
 557      * @return an UrlClassLoader with urls made of the 'test.class.path' jtreg
 558      *         property and with the given parent
 559      */
 560     public static URLClassLoader getTestClassPathURLClassLoader(ClassLoader parent) {
 561         URL[] urls = Arrays.stream(TEST_CLASS_PATH.split(File.pathSeparator))
 562                 .map(Paths::get)
 563                 .map(Path::toUri)
 564                 .map(x -> {
 565                     try {
 566                         return x.toURL();
 567                     } catch (MalformedURLException ex) {
 568                         throw new Error("Test issue. JTREG property"
 569                                 + " 'test.class.path'"
 570                                 + " is not defined correctly", ex);
 571                     }
 572                 }).toArray(URL[]::new);
 573         return new URLClassLoader(urls, parent);
 574     }
 575 
 576     /**
 577      * Runs runnable and checks that it throws expected exception. If exceptionException is null it means
 578      * that we expect no exception to be thrown.
 579      * @param runnable what we run
 580      * @param expectedException expected exception
 581      */
 582     public static void runAndCheckException(Runnable runnable, Class<? extends Throwable> expectedException) {
 583         runAndCheckException(runnable, t -> {
 584             if (t == null) {
 585                 if (expectedException != null) {
 586                     throw new AssertionError("Didn't get expected exception " + expectedException.getSimpleName());
 587                 }
 588             } else {
 589                 String message = "Got unexpected exception " + t.getClass().getSimpleName();
 590                 if (expectedException == null) {
 591                     throw new AssertionError(message, t);
 592                 } else if (!expectedException.isAssignableFrom(t.getClass())) {
 593                     message += " instead of " + expectedException.getSimpleName();
 594                     throw new AssertionError(message, t);
 595                 }
 596             }
 597         });
 598     }
 599 
 600     /**
 601      * Runs runnable and makes some checks to ensure that it throws expected exception.
 602      * @param runnable what we run
 603      * @param checkException a consumer which checks that we got expected exception and raises a new exception otherwise
 604      */
 605     public static void runAndCheckException(Runnable runnable, Consumer<Throwable> checkException) {
 606         try {
 607             runnable.run();
 608             checkException.accept(null);
 609         } catch (Throwable t) {
 610             checkException.accept(t);
 611         }
 612     }
 613 
 614     /**
 615      * Converts to VM type signature
 616      *
 617      * @param type Java type to convert
 618      * @return string representation of VM type
 619      */
 620     public static String toJVMTypeSignature(Class<?> type) {
 621         if (type.isPrimitive()) {
 622             if (type == boolean.class) {
 623                 return "Z";
 624             } else if (type == byte.class) {
 625                 return "B";
 626             } else if (type == char.class) {
 627                 return "C";
 628             } else if (type == double.class) {
 629                 return "D";
 630             } else if (type == float.class) {
 631                 return "F";
 632             } else if (type == int.class) {
 633                 return "I";
 634             } else if (type == long.class) {
 635                 return "J";
 636             } else if (type == short.class) {
 637                 return "S";
 638             } else if (type == void.class) {
 639                 return "V";
 640             } else {
 641                 throw new Error("Unsupported type: " + type);
 642             }
 643         }
 644         String result = type.getName().replaceAll("\\.", "/");
 645         if (!type.isArray()) {
 646             return "L" + result + ";";
 647         }
 648         return result;
 649     }
 650 
 651     public static Object[] getNullValues(Class<?>... types) {
 652         Object[] result = new Object[types.length];
 653         int i = 0;
 654         for (Class<?> type : types) {
 655             result[i++] = NULL_VALUES.get(type);
 656         }
 657         return result;
 658     }
 659     private static Map<Class<?>, Object> NULL_VALUES = new HashMap<>();
 660     static {
 661         NULL_VALUES.put(boolean.class, false);
 662         NULL_VALUES.put(byte.class, (byte) 0);
 663         NULL_VALUES.put(short.class, (short) 0);
 664         NULL_VALUES.put(char.class, '\0');
 665         NULL_VALUES.put(int.class, 0);
 666         NULL_VALUES.put(long.class, 0L);
 667         NULL_VALUES.put(float.class, 0.0f);
 668         NULL_VALUES.put(double.class, 0.0d);
 669     }
 670 
 671     /**
 672      * Returns mandatory property value
 673      * @param propName is a name of property to request
 674      * @return a String with requested property value
 675      */
 676     public static String getMandatoryProperty(String propName) {
 677         Objects.requireNonNull(propName, "Requested null property");
 678         String prop = System.getProperty(propName);
 679         Objects.requireNonNull(prop,
 680                 String.format("A mandatory property '%s' isn't set", propName));
 681         return prop;
 682     }





















































 683 }
 684 
--- EOF ---