1// © 2016 and later: Unicode, Inc. and others. 2// License & terms of use: http://www.unicode.org/copyright.html#License 3/* 4 ******************************************************************************* 5 * Copyright (C) 1996-2015, International Business Machines Corporation and * 6 * others. All Rights Reserved. * 7 ******************************************************************************* 8 */ 9package com.ibm.icu.dev.test; 10 11import java.lang.reflect.Constructor; 12import java.lang.reflect.Method; 13import java.lang.reflect.Modifier; 14import java.security.Policy; 15import java.util.ArrayList; 16import java.util.Arrays; 17import java.util.List; 18import java.util.Locale; 19import java.util.Map; 20import java.util.Properties; 21import java.util.Random; 22import java.util.TreeMap; 23 24import org.junit.After; 25import org.junit.Assert; 26import org.junit.Before; 27 28import com.ibm.icu.util.TimeZone; 29import com.ibm.icu.util.ULocale; 30 31/** 32 * TestFmwk is a base class for tests that can be run conveniently from the 33 * command line as well as under the Java test harness. 34 * <p> 35 * Sub-classes implement a set of methods named Test <something>. Each of these 36 * methods performs some test. Test methods should indicate errors by calling 37 * either err or errln. This will increment the errorCount field and may 38 * optionally print a message to the log. Debugging information may also be 39 * added to the log via the log and logln methods. These methods will add their 40 * arguments to the log only if the test is being run in verbose mode. 41 */ 42abstract public class TestFmwk extends AbstractTestLog { 43 /** 44 * The default time zone for all of our tests. Used in @Before 45 */ 46 private final static TimeZone defaultTimeZone = TimeZone.getTimeZone("America/Los_Angeles"); 47 48 /** 49 * The default locale used for all of our tests. Used in @Before 50 */ 51 private final static Locale defaultLocale = Locale.US; 52 53 private static final String EXHAUSTIVENESS = "ICU.exhaustive"; 54 private static final int DEFAULT_EXHAUSTIVENESS = 0; 55 private static final int MAX_EXHAUSTIVENESS = 10; 56 57 private static final String LOGGING_LEVEL = "ICU.logging"; 58 private static final int DEFAULT_LOGGING_LEVEL = 0; 59 private static final int MAX_LOGGING_LEVEL = 3; 60 61 public static final int LOGGING_NONE = 0; 62 public static final int LOGGING_WARN = 1; 63 public static final int LOGGING_INFO = 2; 64 public static final int LOGGING_DEBUG = 3; 65 66 private static final String SEED = "ICU.seed"; 67 private static final String SECURITY_POLICY = "ICU.securitypolicy"; 68 69 private static final TestParams testParams; 70 static { 71 testParams = TestParams.create(); 72 } 73 74 protected TestFmwk() { 75 } 76 77 @Before 78 public void testInitialize() { 79 Locale.setDefault(defaultLocale); 80 TimeZone.setDefault(defaultTimeZone); 81 82 if (getParams().testSecurityManager != null) { 83 System.setSecurityManager(getParams().testSecurityManager); 84 } 85 } 86 87 @After 88 public void testTeardown() { 89 if (getParams().testSecurityManager != null) { 90 System.setSecurityManager(getParams().originalSecurityManager); 91 } 92 } 93 94 private static TestParams getParams() { 95 //return paramsReference.get(); 96 return testParams; 97 } 98 99 protected static boolean isVerbose() { 100 return getParams().getLoggingLevel() >= LOGGING_INFO; 101 } 102 103 /** 104 * 0 = fewest tests, 5 is normal build, 10 is most tests 105 */ 106 protected static int getExhaustiveness() { 107 return getParams().inclusion; 108 } 109 110 protected static boolean isQuick() { 111 return getParams().getInclusion() == 0; 112 } 113 114 // use this instead of new random so we get a consistent seed 115 // for our tests 116 protected Random createRandom() { 117 return new Random(getParams().getSeed()); 118 } 119 120 /** 121 * Integer Random number generator, produces positive int values. 122 * Similar to C++ std::minstd_rand, with the same algorithm & constants. 123 * Provided for compatibility with ICU4C. 124 * Get & set of the seed allows for reproducible monkey tests. 125 */ 126 protected class ICU_Rand { 127 private int fLast; 128 129 public ICU_Rand(int seed) { 130 seed(seed); 131 } 132 133 public int next() { 134 fLast = (int)((fLast * 48271L) % 2147483647L); 135 return fLast; 136 } 137 138 public void seed(int seed) { 139 if (seed <= 0) { 140 seed = 1; 141 } 142 seed %= 2147483647; // = 0x7FFFFFFF 143 fLast = seed > 0 ? seed : 1; 144 } 145 146 public int getSeed() { 147 return fLast; 148 } 149 150 } 151 152 static final String ICU_TRAC_URL = "http://bugs.icu-project.org/trac/ticket/"; 153 static final String CLDR_TRAC_URL = "http://unicode.org/cldr/trac/ticket/"; 154 static final String CLDR_TICKET_PREFIX = "cldrbug:"; 155 156 /** 157 * Log the known issue. 158 * This method returns true unless -prop:logKnownIssue=no is specified 159 * in the argument list. 160 * 161 * @param ticket A ticket number string. For an ICU ticket, use numeric characters only, 162 * such as "10245". For a CLDR ticket, use prefix "cldrbug:" followed by ticket number, 163 * such as "cldrbug:5013". 164 * @param comment Additional comment, or null 165 * @return true unless -prop:logKnownIssue=no is specified in the test command line argument. 166 */ 167 protected static boolean logKnownIssue(String ticket, String comment) { 168 if (!getBooleanProperty("logKnownIssue", true)) { 169 return false; 170 } 171 172 StringBuffer descBuf = new StringBuffer(); 173 // TODO(junit) : what to do about this? 174 //getParams().stack.appendPath(descBuf); 175 if (comment != null && comment.length() > 0) { 176 descBuf.append(" (" + comment + ")"); 177 } 178 String description = descBuf.toString(); 179 180 String ticketLink = "Unknown Ticket"; 181 if (ticket != null && ticket.length() > 0) { 182 boolean isCldr = false; 183 ticket = ticket.toLowerCase(Locale.ENGLISH); 184 if (ticket.startsWith(CLDR_TICKET_PREFIX)) { 185 isCldr = true; 186 ticket = ticket.substring(CLDR_TICKET_PREFIX.length()); 187 } 188 ticketLink = (isCldr ? CLDR_TRAC_URL : ICU_TRAC_URL) + ticket; 189 } 190 191 if (getParams().knownIssues == null) { 192 getParams().knownIssues = new TreeMap<String, List<String>>(); 193 } 194 List<String> lines = getParams().knownIssues.get(ticketLink); 195 if (lines == null) { 196 lines = new ArrayList<String>(); 197 getParams().knownIssues.put(ticketLink, lines); 198 } 199 if (!lines.contains(description)) { 200 lines.add(description); 201 } 202 203 return true; 204 } 205 206 protected static String getProperty(String key) { 207 return getParams().getProperty(key); 208 } 209 210 protected static boolean getBooleanProperty(String key) { 211 return getParams().getBooleanProperty(key); 212 } 213 214 protected static boolean getBooleanProperty(String key, boolean defVal) { 215 return getParams().getBooleanProperty(key, defVal); 216 } 217 218 protected static int getIntProperty(String key, int defVal) { 219 return getParams().getIntProperty(key, defVal); 220 } 221 222 protected static int getIntProperty(String key, int defVal, int maxVal) { 223 return getParams().getIntProperty(key, defVal, maxVal); 224 } 225 226 protected static TimeZone safeGetTimeZone(String id) { 227 TimeZone tz = TimeZone.getTimeZone(id); 228 if (tz == null) { 229 // should never happen 230 errln("FAIL: TimeZone.getTimeZone(" + id + ") => null"); 231 } 232 if (!tz.getID().equals(id)) { 233 warnln("FAIL: TimeZone.getTimeZone(" + id + ") => " + tz.getID()); 234 } 235 return tz; 236 } 237 238 239 // Utility Methods 240 241 protected static String hex(char[] s){ 242 StringBuffer result = new StringBuffer(); 243 for (int i = 0; i < s.length; ++i) { 244 if (i != 0) result.append(','); 245 result.append(hex(s[i])); 246 } 247 return result.toString(); 248 } 249 250 protected static String hex(byte[] s){ 251 StringBuffer result = new StringBuffer(); 252 for (int i = 0; i < s.length; ++i) { 253 if (i != 0) result.append(','); 254 result.append(hex(s[i])); 255 } 256 return result.toString(); 257 } 258 259 protected static String hex(char ch) { 260 StringBuffer result = new StringBuffer(); 261 String foo = Integer.toString(ch, 16).toUpperCase(); 262 for (int i = foo.length(); i < 4; ++i) { 263 result.append('0'); 264 } 265 return result + foo; 266 } 267 268 protected static String hex(int ch) { 269 StringBuffer result = new StringBuffer(); 270 String foo = Integer.toString(ch, 16).toUpperCase(); 271 for (int i = foo.length(); i < 4; ++i) { 272 result.append('0'); 273 } 274 return result + foo; 275 } 276 277 protected static String hex(CharSequence s) { 278 StringBuilder result = new StringBuilder(); 279 for (int i = 0; i < s.length(); ++i) { 280 if (i != 0) 281 result.append(','); 282 result.append(hex(s.charAt(i))); 283 } 284 return result.toString(); 285 } 286 287 protected static String prettify(CharSequence s) { 288 StringBuilder result = new StringBuilder(); 289 int ch; 290 for (int i = 0; i < s.length(); i += Character.charCount(ch)) { 291 ch = Character.codePointAt(s, i); 292 if (ch > 0xfffff) { 293 result.append("\\U00"); 294 result.append(hex(ch)); 295 } else if (ch > 0xffff) { 296 result.append("\\U000"); 297 result.append(hex(ch)); 298 } else if (ch < 0x20 || 0x7e < ch) { 299 result.append("\\u"); 300 result.append(hex(ch)); 301 } else { 302 result.append((char) ch); 303 } 304 305 } 306 return result.toString(); 307 } 308 309 private static java.util.GregorianCalendar cal; 310 311 /** 312 * Return a Date given a year, month, and day of month. This is similar to 313 * new Date(y-1900, m, d). It uses the default time zone at the time this 314 * method is first called. 315 * 316 * @param year 317 * use 2000 for 2000, unlike new Date() 318 * @param month 319 * use Calendar.JANUARY etc. 320 * @param dom 321 * day of month, 1-based 322 * @return a Date object for the given y/m/d 323 */ 324 protected static synchronized java.util.Date getDate(int year, int month, 325 int dom) { 326 if (cal == null) { 327 cal = new java.util.GregorianCalendar(); 328 } 329 cal.clear(); 330 cal.set(year, month, dom); 331 return cal.getTime(); 332 } 333 334 private static class TestParams { 335 336 private int inclusion; 337 private long seed; 338 private int loggingLevel; 339 340 private String policyFileName; 341 private SecurityManager testSecurityManager; 342 private SecurityManager originalSecurityManager; 343 344 private Map<String, List<String>> knownIssues; 345 346 private Properties props; 347 348 349 private TestParams() { 350 } 351 352 static TestParams create() { 353 TestParams params = new TestParams(); 354 Properties props = System.getProperties(); 355 params.parseProperties(props); 356 return params; 357 } 358 359 private void parseProperties(Properties props) { 360 this.props = props; 361 362 inclusion = getIntProperty(EXHAUSTIVENESS, DEFAULT_EXHAUSTIVENESS, MAX_EXHAUSTIVENESS); 363 seed = getLongProperty(SEED, System.currentTimeMillis()); 364 loggingLevel = getIntProperty(LOGGING_LEVEL, DEFAULT_LOGGING_LEVEL, MAX_LOGGING_LEVEL); 365 366 policyFileName = getProperty(SECURITY_POLICY); 367 if (policyFileName != null) { 368 String originalPolicyFileName = System.getProperty("java.security.policy"); 369 originalSecurityManager = System.getSecurityManager(); 370 System.setProperty("java.security.policy", policyFileName); 371 Policy.getPolicy().refresh(); 372 testSecurityManager = new SecurityManager(); 373 System.setProperty("java.security.policy", originalPolicyFileName==null ? "" : originalPolicyFileName); 374 } 375 } 376 377 public String getProperty(String key) { 378 String val = null; 379 if (key != null && key.length() > 0) { 380 val = props.getProperty(key); 381 } 382 return val; 383 } 384 385 public boolean getBooleanProperty(String key) { 386 return getBooleanProperty(key, false); 387 } 388 389 public boolean getBooleanProperty(String key, boolean defVal) { 390 String s = getProperty(key); 391 if (s == null) { 392 return defVal; 393 } 394 if (s.equalsIgnoreCase("yes") || s.equalsIgnoreCase("true") || s.equals("1")) { 395 return true; 396 } 397 return false; 398 } 399 400 public int getIntProperty(String key, int defVal) { 401 return getIntProperty(key, defVal, -1); 402 } 403 404 public int getIntProperty(String key, int defVal, int maxVal) { 405 String s = getProperty(key); 406 if (s == null) { 407 return defVal; 408 } 409 return (maxVal == -1) ? Integer.valueOf(s) : Math.max(Integer.valueOf(s), maxVal); 410 } 411 412 public long getLongProperty(String key, long defVal) { 413 String s = getProperty(key); 414 if (s == null) { 415 return defVal; 416 } 417 return Long.valueOf(s); 418 } 419 420 public int getInclusion() { 421 return inclusion; 422 } 423 424 public long getSeed() { 425 return seed; 426 } 427 428 public int getLoggingLevel() { 429 return loggingLevel; 430 } 431 } 432 433 /** 434 * Check the given array to see that all the strings in the expected array 435 * are present. 436 * 437 * @param msg 438 * string message, for log output 439 * @param array 440 * array of strings to check 441 * @param expected 442 * array of strings we expect to see, or null 443 * @return the length of 'array', or -1 on error 444 */ 445 protected static int checkArray(String msg, String array[], String expected[]) { 446 int explen = (expected != null) ? expected.length : 0; 447 if (!(explen >= 0 && explen < 31)) { // [sic] 31 not 32 448 errln("Internal error"); 449 return -1; 450 } 451 int i = 0; 452 StringBuffer buf = new StringBuffer(); 453 int seenMask = 0; 454 for (; i < array.length; ++i) { 455 String s = array[i]; 456 if (i != 0) 457 buf.append(", "); 458 buf.append(s); 459 // check expected list 460 for (int j = 0, bit = 1; j < explen; ++j, bit <<= 1) { 461 if ((seenMask & bit) == 0) { 462 if (s.equals(expected[j])) { 463 seenMask |= bit; 464 logln("Ok: \"" + s + "\" seen"); 465 } 466 } 467 } 468 } 469 logln(msg + " = [" + buf + "] (" + i + ")"); 470 // did we see all expected strings? 471 if (((1 << explen) - 1) != seenMask) { 472 for (int j = 0, bit = 1; j < expected.length; ++j, bit <<= 1) { 473 if ((seenMask & bit) == 0) { 474 errln("\"" + expected[j] + "\" not seen"); 475 } 476 } 477 } 478 return array.length; 479 } 480 481 /** 482 * Check the given array to see that all the locales in the expected array 483 * are present. 484 * 485 * @param msg 486 * string message, for log output 487 * @param array 488 * array of locales to check 489 * @param expected 490 * array of locales names we expect to see, or null 491 * @return the length of 'array' 492 */ 493 protected static int checkArray(String msg, Locale array[], String expected[]) { 494 String strs[] = new String[array.length]; 495 for (int i = 0; i < array.length; ++i) { 496 strs[i] = array[i].toString(); 497 } 498 return checkArray(msg, strs, expected); 499 } 500 501 /** 502 * Check the given array to see that all the locales in the expected array 503 * are present. 504 * 505 * @param msg 506 * string message, for log output 507 * @param array 508 * array of locales to check 509 * @param expected 510 * array of locales names we expect to see, or null 511 * @return the length of 'array' 512 */ 513 protected static int checkArray(String msg, ULocale array[], String expected[]) { 514 String strs[] = new String[array.length]; 515 for (int i = 0; i < array.length; ++i) { 516 strs[i] = array[i].toString(); 517 } 518 return checkArray(msg, strs, expected); 519 } 520 521 // JUnit-like assertions. 522 523 protected static boolean assertTrue(String message, boolean condition) { 524 return handleAssert(condition, message, "true", null); 525 } 526 527 protected static boolean assertFalse(String message, boolean condition) { 528 return handleAssert(!condition, message, "false", null); 529 } 530 531 protected static boolean assertEquals(String message, boolean expected, 532 boolean actual) { 533 return handleAssert(expected == actual, message, String 534 .valueOf(expected), String.valueOf(actual)); 535 } 536 537 protected static boolean assertEquals(String message, long expected, long actual) { 538 return handleAssert(expected == actual, message, String 539 .valueOf(expected), String.valueOf(actual)); 540 } 541 542 // do NaN and range calculations to precision of float, don't rely on 543 // promotion to double 544 protected static boolean assertEquals(String message, float expected, 545 float actual, double error) { 546 boolean result = Float.isInfinite(expected) 547 ? expected == actual 548 : !(Math.abs(expected - actual) > error); // handles NaN 549 return handleAssert(result, message, String.valueOf(expected) 550 + (error == 0 ? "" : " (within " + error + ")"), String 551 .valueOf(actual)); 552 } 553 554 protected static boolean assertEquals(String message, double expected, 555 double actual, double error) { 556 boolean result = Double.isInfinite(expected) 557 ? expected == actual 558 : !(Math.abs(expected - actual) > error); // handles NaN 559 return handleAssert(result, message, String.valueOf(expected) 560 + (error == 0 ? "" : " (within " + error + ")"), String 561 .valueOf(actual)); 562 } 563 564 protected static <T> boolean assertEquals(String message, T[] expected, T[] actual) { 565 // Use toString on a List to get useful, readable messages 566 String expectedString = expected == null ? "null" : Arrays.asList(expected).toString(); 567 String actualString = actual == null ? "null" : Arrays.asList(actual).toString(); 568 return assertEquals(message, expectedString, actualString); 569 } 570 571 protected static boolean assertEquals(String message, Object expected, 572 Object actual) { 573 boolean result = expected == null ? actual == null : expected 574 .equals(actual); 575 return handleAssert(result, message, stringFor(expected), 576 stringFor(actual)); 577 } 578 579 protected static boolean assertNotEquals(String message, Object expected, 580 Object actual) { 581 boolean result = !(expected == null ? actual == null : expected 582 .equals(actual)); 583 return handleAssert(result, message, stringFor(expected), 584 stringFor(actual), "not equal to", true); 585 } 586 587 protected boolean assertSame(String message, Object expected, Object actual) { 588 return handleAssert(expected == actual, message, stringFor(expected), 589 stringFor(actual), "==", false); 590 } 591 592 protected static boolean assertNotSame(String message, Object expected, 593 Object actual) { 594 return handleAssert(expected != actual, message, stringFor(expected), 595 stringFor(actual), "!=", true); 596 } 597 598 protected static boolean assertNull(String message, Object actual) { 599 return handleAssert(actual == null, message, null, stringFor(actual)); 600 } 601 602 protected static boolean assertNotNull(String message, Object actual) { 603 return handleAssert(actual != null, message, null, stringFor(actual), 604 "!=", true); 605 } 606 607 protected static void fail() { 608 fail(""); 609 } 610 611 protected static void fail(String message) { 612 if (message == null) { 613 message = ""; 614 } 615 if (!message.equals("")) { 616 message = ": " + message; 617 } 618 errln(sourceLocation() + message); 619 } 620 621 private static boolean handleAssert(boolean result, String message, 622 String expected, String actual) { 623 return handleAssert(result, message, expected, actual, null, false); 624 } 625 626 public static boolean handleAssert(boolean result, String message, 627 Object expected, Object actual, String relation, boolean flip) { 628 if (!result || isVerbose()) { 629 if (message == null) { 630 message = ""; 631 } 632 if (!message.equals("")) { 633 message = ": " + message; 634 } 635 relation = relation == null ? ", got " : " " + relation + " "; 636 if (result) { 637 logln("OK " + message + ": " 638 + (flip ? expected + relation + actual : expected)); 639 } else { 640 // assert must assume errors are true errors and not just warnings 641 // so cannot warnln here 642 errln( message 643 + ": expected" 644 + (flip ? relation + expected : " " + expected 645 + (actual != null ? relation + actual : ""))); 646 } 647 } 648 return result; 649 } 650 651 private static final String stringFor(Object obj) { 652 if (obj == null) { 653 return "null"; 654 } 655 if (obj instanceof String) { 656 return "\"" + obj + '"'; 657 } 658 return obj.getClass().getName() + "<" + obj + ">"; 659 } 660 661 // Return the source code location of the caller located callDepth frames up the stack. 662 protected static String sourceLocation() { 663 // Walk up the stack to the first call site outside this file 664 for (StackTraceElement st : new Throwable().getStackTrace()) { 665 String source = st.getFileName(); 666 if (source != null && !source.equals("TestFmwk.java") && !source.equals("AbstractTestLog.java")) { 667 String methodName = st.getMethodName(); 668 if (methodName != null && 669 (methodName.startsWith("Test") || methodName.startsWith("test") || methodName.equals("main"))) { 670 return "(" + source + ":" + st.getLineNumber() + ") "; 671 } 672 } 673 } 674 throw new InternalError(); 675 } 676 677 protected static boolean checkDefaultPrivateConstructor(String fullyQualifiedClassName) throws Exception { 678 return checkDefaultPrivateConstructor(Class.forName(fullyQualifiedClassName)); 679 } 680 681 protected static boolean checkDefaultPrivateConstructor(Class<?> classToBeTested) throws Exception { 682 Constructor<?> constructor = classToBeTested.getDeclaredConstructor(); 683 684 // Check that the constructor is private. 685 boolean isPrivate = Modifier.isPrivate(constructor.getModifiers()); 686 687 // Call the constructor for coverage. 688 constructor.setAccessible(true); 689 constructor.newInstance(); 690 691 if (!isPrivate) { 692 errln("Default private constructor for class: " + classToBeTested.getName() + " is not private."); 693 } 694 return isPrivate; 695 } 696 697 /** 698 * Tests the toString method on a private or hard-to-reach class. Assumes constructor of the class does not 699 * take any arguments. 700 * @param fullyQualifiedClassName 701 * @return The output of the toString method. 702 * @throws Exception 703 */ 704 protected static String invokeToString(String fullyQualifiedClassName) throws Exception { 705 return invokeToString(fullyQualifiedClassName, new Class<?>[]{}, new Object[]{}); 706 } 707 708 /** 709 * Tests the toString method on a private or hard-to-reach class. Assumes constructor of the class does not 710 * take any arguments. 711 * @param classToBeTested 712 * @return The output of the toString method. 713 * @throws Exception 714 */ 715 protected static String invokeToString(Class<?> classToBeTested) throws Exception { 716 return invokeToString(classToBeTested, new Class<?>[]{}, new Object[]{}); 717 } 718 719 /** 720 * Tests the toString method on a private or hard-to-reach class. Allows you to specify the argument types for 721 * the constructor. 722 * @param fullyQualifiedClassName 723 * @return The output of the toString method. 724 * @throws Exception 725 */ 726 protected static String invokeToString(String fullyQualifiedClassName, 727 Class<?>[] constructorParamTypes, Object[] constructorParams) throws Exception { 728 return invokeToString(Class.forName(fullyQualifiedClassName), constructorParamTypes, constructorParams); 729 } 730 731 /** 732 * Tests the toString method on a private or hard-to-reach class. Allows you to specify the argument types for 733 * the constructor. 734 * @param classToBeTested 735 * @return The output of the toString method. 736 * @throws Exception 737 */ 738 protected static String invokeToString(Class<?> classToBeTested, 739 Class<?>[] constructorParamTypes, Object[] constructorParams) throws Exception { 740 Constructor<?> constructor = classToBeTested.getDeclaredConstructor(constructorParamTypes); 741 constructor.setAccessible(true); 742 Object obj = constructor.newInstance(constructorParams); 743 Method toStringMethod = classToBeTested.getDeclaredMethod("toString"); 744 toStringMethod.setAccessible(true); 745 return (String) toStringMethod.invoke(obj); 746 } 747 748 749 // End JUnit-like assertions 750 751 // TODO (sgill): added to keep errors away 752 /* (non-Javadoc) 753 * @see com.ibm.icu.dev.test.TestLog#msg(java.lang.String, int, boolean, boolean) 754 */ 755 //@Override 756 protected static void msg(String message, int level, boolean incCount, boolean newln) { 757 if (level == TestLog.WARN || level == TestLog.ERR) { 758 Assert.fail(message); 759 } 760 // TODO(stuartg): turned off - causing OOM running under ant 761// while (level > 0) { 762// System.out.print(" "); 763// level--; 764// } 765// System.out.print(message); 766// if (newln) { 767// System.out.println(); 768// } 769 } 770 771} 772