| 1 | // Jomic - a viewer for comic book archives. |
| 2 | // Copyright (C) 2004-2011 Thomas Aglassinger |
| 3 | // |
| 4 | // This program is free software: you can redistribute it and/or modify |
| 5 | // it under the terms of the GNU General Public License as published by |
| 6 | // the Free Software Foundation, either version 3 of the License, or |
| 7 | // (at your option) any later version. |
| 8 | // |
| 9 | // This program is distributed in the hope that it will be useful, |
| 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | // GNU General Public License for more details. |
| 13 | // |
| 14 | // You should have received a copy of the GNU General Public License |
| 15 | // along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 16 | package net.sf.jomic.tools; |
| 17 | |
| 18 | import java.awt.Dimension; |
| 19 | import java.awt.image.RenderedImage; |
| 20 | import java.io.File; |
| 21 | import java.io.FileFilter; |
| 22 | import java.io.FileInputStream; |
| 23 | import java.io.FileNotFoundException; |
| 24 | import java.io.FileOutputStream; |
| 25 | import java.io.IOException; |
| 26 | import java.io.InputStream; |
| 27 | import java.util.Arrays; |
| 28 | import java.util.Iterator; |
| 29 | import java.util.LinkedList; |
| 30 | import java.util.List; |
| 31 | import java.util.Properties; |
| 32 | import java.util.zip.ZipEntry; |
| 33 | import java.util.zip.ZipOutputStream; |
| 34 | |
| 35 | import javax.imageio.ImageIO; |
| 36 | import javax.imageio.ImageReader; |
| 37 | import javax.imageio.ImageWriter; |
| 38 | import javax.imageio.stream.ImageInputStream; |
| 39 | import javax.imageio.stream.ImageOutputStream; |
| 40 | import javax.media.jai.JAI; |
| 41 | import javax.media.jai.PlanarImage; |
| 42 | import javax.swing.JComponent; |
| 43 | import javax.swing.JFrame; |
| 44 | |
| 45 | import junit.framework.Assert; |
| 46 | import net.sf.jomic.comic.ComicCache; |
| 47 | import net.sf.jomic.common.JomicConfigurator; |
| 48 | import net.sf.jomic.common.PropertyConstants; |
| 49 | import net.sf.jomic.common.Settings; |
| 50 | import net.sf.jomic.common.StartupTools; |
| 51 | import org.apache.commons.logging.Log; |
| 52 | import org.apache.commons.logging.LogFactory; |
| 53 | |
| 54 | import org.apache.log4j.Level; |
| 55 | |
| 56 | /** |
| 57 | * Utility class to simplify testing. |
| 58 | * |
| 59 | * @author Thomas Aglassinger |
| 60 | */ |
| 61 | public final class TestTools |
| 62 | { |
| 63 | public static final String BROKEN_IMAGE_INCOMPLETE_CBZ = "broken_image_incomplete.cbz"; |
| 64 | public static final String BROKEN_NO_IMAGES_PDF = "broken_no_images.pdf"; |
| 65 | public static final String DISGUISED_RAR = "disguised_rar.cbz"; |
| 66 | public static final String DISGUISED_ZIP = "disguised_zip.cbr"; |
| 67 | public static final String[] EMPTY_IMAGE_NAMES = new String[]{"empty.gif", "empty.jpg", "empty.png"}; |
| 68 | public static final int FRAME_HEIGHT = 300; |
| 69 | public static final int FRAME_WIDTH = 500; |
| 70 | public static final int HUGE_PAGE_COUNT = 150; |
| 71 | public static final String HUGO_COMIC = "huge.cbz"; |
| 72 | public static final String IMAGES_WITH_WRONG_SUFFIX_COMIC = "images_with_wrong_suffix.cbz"; |
| 73 | public static final String SINGLE_PAGE_COMIC = "1page.cbz"; |
| 74 | public static final String TEST_COMIC_CBR = "test.cbr"; |
| 75 | public static final String TEST_COMIC_CBZ = "test.cbz"; |
| 76 | public static final String TEST_COMIC_FILE_NAME = TEST_COMIC_CBZ; |
| 77 | public static final String TEST_COMIC_INTERNAL_ERROR = "broken_comic_internal_zip_error.cbz"; |
| 78 | public static final String TEST_COMIC_LANDSCAPE_ONLY = "test_landscape_only.cbz"; |
| 79 | public static final String TEST_COMIC_PDF = "test.pdf"; |
| 80 | public static final String TEST_COMIC_WITH_LOGO_TITLE = "test_with_logo_title.cbz"; |
| 81 | public static final String TEST_EMPTY_IMAGES_CBZ = "test_empty_images.cbz"; |
| 82 | public static final String TEST_IMAGE_1_BIT = "1-bit.png"; |
| 83 | public static final String TEST_IMAGE_4_BIT = "4-bit.png"; |
| 84 | public static final String TEST_IMAGE_8_BIT = "01.87a.gif"; |
| 85 | public static final String TEST_IMAGE_8_BIT_GRAY = "8-bit-gray.jpg"; |
| 86 | public static final String TEST_IMAGE_BROKEN_BY_CUTTING_NAME = "02-broken-image-by-cutting.png"; |
| 87 | public static final String TEST_IMAGE_BROKEN_BY_FILLING_NAME = "03-broken-image-by-filling.png"; |
| 88 | public static final String TEST_IMAGE_BROKEN_EMPTY_NAME = "04-broken-image-empty.png"; |
| 89 | public static final String TEST_IMAGE_DISGUISED_JPG = "02-jpg.png"; |
| 90 | public static final String TEST_IMAGE_DISGUISED_PNG = "01-png.jpg"; |
| 91 | public static final String TEST_IMAGE_FILE_NAME = "01.png"; |
| 92 | public static final String TEST_IMAGE_GIF87A = "01.87a.gif"; |
| 93 | public static final String TEST_IMAGE_GIF89A = "01.89a.gif"; |
| 94 | public static final String TEST_IMAGE_IBM_TIFF = "01.ibm.tiff"; |
| 95 | public static final String TEST_IMAGE_JP2_NAME = "01.jp2"; |
| 96 | public static final String TEST_IMAGE_JPG_NAME = "27.jpg"; |
| 97 | public static final String TEST_IMAGE_LANDSCAPE_NAME = "04+05.png"; |
| 98 | public static final String TEST_IMAGE_MAC_TIFF = "01.mac.tiff"; |
| 99 | public static final String TEST_IMAGE_PNG_NAME = "01.png"; |
| 100 | public static final String TEST_IMAGE_PORTRAIT_NAME = "01.png"; |
| 101 | public static final String TEST_MORONIC_NUMBERING_CBZ = "test_moronic_numbering.cbz"; |
| 102 | public static final String TEST_TEXT_NAME = "test.txt"; |
| 103 | public static final String THREE_PAGE_COMIC = "3pages.cbz"; |
| 104 | public static final String TWO_PAGE_COMIC = "2pages.cbz"; |
| 105 | |
| 106 | private static final int BUFFER_SIZE = 4096; |
| 107 | |
| 108 | private static TestTools instance; |
| 109 | |
| 110 | private int delay; |
| 111 | private FileTools fileTools; |
| 112 | private File jomicHome; |
| 113 | private Log logger; |
| 114 | private StringTools stringTools; |
| 115 | private File testExpectedDir; |
| 116 | private File testGeneratedInputDir; |
| 117 | private File testInputDir; |
| 118 | private File testOutputDir; |
| 119 | private File testsBaseDir; |
| 120 | private File testsDataDir; |
| 121 | |
| 122 | private TestTools() { |
| 123 | logger = LogFactory.getLog(TestTools.class); |
| 124 | try { |
| 125 | setupLogging(); |
| 126 | } catch (IOException error) { |
| 127 | // No point in continuing any testing with a broken logging. |
| 128 | IllegalStateException bug = new IllegalStateException("cannot setup logging"); |
| 129 | |
| 130 | bug.initCause(error); |
| 131 | throw bug; |
| 132 | } |
| 133 | setupAbbotLogging(); |
| 134 | |
| 135 | Settings settings = Settings.instance(); |
| 136 | String propertyJomicHome = PropertyConstants.TEST_JOMIC_HOME; |
| 137 | |
| 138 | settings.setDefault(PropertyConstants.TEST_JOMIC_HOME, System.getProperty("user.dir")); |
| 139 | |
| 140 | String home = settings.getProperty(propertyJomicHome); |
| 141 | |
| 142 | if (home == null) { |
| 143 | String propertyJomicHomeFull = PropertyConstants.SYSTEM_PROPERTY_PREFIX + propertyJomicHome; |
| 144 | |
| 145 | throw new IllegalStateException( |
| 146 | "property " + propertyJomicHomeFull + " must be set" |
| 147 | + " (for example: -D" + propertyJomicHomeFull + "=/Users/me/Programs/jomic)"); |
| 148 | } |
| 149 | jomicHome = new File(home); |
| 150 | testsBaseDir = new File(jomicHome, "tests"); |
| 151 | if (!testsBaseDir.exists()) { |
| 152 | String propertyJomicHomeFull = PropertyConstants.SYSTEM_PROPERTY_PREFIX + propertyJomicHome; |
| 153 | |
| 154 | throw new IllegalStateException( |
| 155 | "folder specified by " + propertyJomicHomeFull + " must exist: " + testsBaseDir); |
| 156 | } |
| 157 | testGeneratedInputDir = new File(testsBaseDir, "generated"); |
| 158 | testsDataDir = testGeneratedInputDir; |
| 159 | testInputDir = new File(testsBaseDir, "input"); |
| 160 | testExpectedDir = new File(testsBaseDir, "expected"); |
| 161 | testOutputDir = new File(testsBaseDir, "output"); |
| 162 | delay = settings.getIntProperty(PropertyConstants.TEST_DELAY); |
| 163 | if (logger.isInfoEnabled()) { |
| 164 | logger.info("jomic.home = \"" + jomicHome + "\""); |
| 165 | logger.info("jomic.delay = " + delay + " ms"); |
| 166 | logger.info("testdata = \"" + testsDataDir + "\""); |
| 167 | } |
| 168 | stringTools = StringTools.instance(); |
| 169 | fileTools = FileTools.instance(); |
| 170 | try { |
| 171 | fileTools.mkdirs(testGeneratedInputDir); |
| 172 | fileTools.mkdirs(testOutputDir); |
| 173 | } catch (FileNotFoundException errorToTunnel) { |
| 174 | IllegalStateException error = new IllegalStateException("cannot create test data directory"); |
| 175 | |
| 176 | error.initCause(errorToTunnel); |
| 177 | throw error; |
| 178 | } |
| 179 | |
| 180 | try { |
| 181 | settings.read(settings.getSettingsFile()); |
| 182 | } catch (IOException errorToTunnel) { |
| 183 | IllegalStateException error = new IllegalStateException("cannot read test settings"); |
| 184 | |
| 185 | error.initCause(errorToTunnel); |
| 186 | throw error; |
| 187 | } |
| 188 | } |
| 189 | |
| 190 | /** |
| 191 | * Setup cache for tests that need it. |
| 192 | */ |
| 193 | public void setupCache() |
| 194 | throws IOException { |
| 195 | ComicCache cache = ComicCache.instance(); |
| 196 | Settings settings = Settings.instance(); |
| 197 | File cacheDir = settings.getCacheDir(); |
| 198 | |
| 199 | cacheDir = new File(cacheDir, "tests"); |
| 200 | cache.setUp(cacheDir); |
| 201 | settings.setFileProperty(PropertyConstants.CACHE_DIR, cacheDir); |
| 202 | } |
| 203 | |
| 204 | /** |
| 205 | * Create a settings file containing <code>keyValuePairs</code> and use it as default settings |
| 206 | * file for <code>Jomic.main()</code>. |
| 207 | * |
| 208 | * @see PropertyConstants#SETTINGS_DIR |
| 209 | * @see StartupTools#getSettingsFile(String) |
| 210 | * @see Jomic#main(String[]) |
| 211 | * @param testCaseClass class of TestCase the settings are for |
| 212 | * @param testMethodName null or name of test method the settings are for |
| 213 | * @param keyValuePairs array of strings of pattern <code>[key1, value1, key2, value2, ...]</code> |
| 214 | * specifying property keys and values the settings file should contain |
| 215 | */ |
| 216 | public void setupTestSettings(Class testCaseClass, String testMethodName, String[] keyValuePairs) |
| 217 | throws IOException { |
| 218 | assert testCaseClass != null; |
| 219 | assert keyValuePairs != null; |
| 220 | assert keyValuePairs.length % 2 == 0; |
| 221 | |
| 222 | File settingsBaseDir = getTestGeneratedInputDir(); |
| 223 | StartupTools startupTools = StartupTools.instance(); |
| 224 | String settingsDirName = testCaseClass.getName(); |
| 225 | |
| 226 | if (testMethodName != null) { |
| 227 | settingsDirName += "." + testMethodName; |
| 228 | } |
| 229 | |
| 230 | File settingsDir = new File(settingsBaseDir, settingsDirName); |
| 231 | |
| 232 | System.setProperty(PropertyConstants.SYSTEM_PROPERTY_PREFIX + PropertyConstants.SETTINGS_DIR, |
| 233 | settingsDir.getAbsolutePath()); |
| 234 | |
| 235 | Properties defaultSettings = new Properties(); |
| 236 | |
| 237 | for (int i = 0; i < keyValuePairs.length; i += 2) { |
| 238 | String key = keyValuePairs[i]; |
| 239 | String value = keyValuePairs[i + 1]; |
| 240 | |
| 241 | defaultSettings.setProperty(key, value); |
| 242 | } |
| 243 | |
| 244 | Settings settings = new Settings(defaultSettings); |
| 245 | |
| 246 | fileTools.mkdirs(settingsDir); |
| 247 | |
| 248 | File settingsFile = startupTools.getSettingsFile("jomic"); |
| 249 | |
| 250 | logger.info("using settings in \"" + settingsFile + "\""); |
| 251 | settings.write(settingsFile); |
| 252 | } |
| 253 | |
| 254 | private void setupAbbotLogging() { |
| 255 | abbot.Log.init(new String[]{"--debug", "all"}); |
| 256 | } |
| 257 | |
| 258 | /** |
| 259 | * Attempt to read logger settings from file. If the file does not exist, use internal |
| 260 | * defaults. |
| 261 | */ |
| 262 | private void setupLogging() |
| 263 | throws IOException { |
| 264 | File loggerSettingsFile = new File("settings", "jomic-tests-logging.properties"); |
| 265 | Properties loggerProperties = new Properties(); |
| 266 | InputStream loggerSettingsStream = null; |
| 267 | boolean readFromFile = false; |
| 268 | org.apache.log4j.Logger root = org.apache.log4j.Logger.getRootLogger(); |
| 269 | |
| 270 | root.setLevel(Level.INFO); |
| 271 | try { |
| 272 | loggerSettingsStream = new FileInputStream(loggerSettingsFile); |
| 273 | loggerProperties.load(loggerSettingsStream); |
| 274 | readFromFile = true; |
| 275 | } catch (FileNotFoundException error) { |
| 276 | loggerProperties.clear(); |
| 277 | loggerProperties.setProperty("log4j.rootLogger", "INFO, stdout"); |
| 278 | loggerProperties.setProperty("log4j.appender.stdout", "org.apache.log4j.ConsoleAppender"); |
| 279 | } finally { |
| 280 | if (loggerSettingsStream != null) { |
| 281 | loggerSettingsStream.close(); |
| 282 | } |
| 283 | } |
| 284 | JomicConfigurator.configure(); |
| 285 | JomicConfigurator.setLevel(loggerProperties); |
| 286 | if (!readFromFile) { |
| 287 | if (logger.isInfoEnabled()) { |
| 288 | logger.info("cannot find logger settings \"" + loggerSettingsFile |
| 289 | + "\"; using internal defaults"); |
| 290 | } |
| 291 | } |
| 292 | } |
| 293 | |
| 294 | /** |
| 295 | * Get a folder where test output can be written to. The folder is created and all possibly |
| 296 | * existing files in it are removed. |
| 297 | */ |
| 298 | public File getCleanTestOutputFolder(String name) { |
| 299 | File result = getTestOutputFile(name); |
| 300 | |
| 301 | fileTools.attemptToDeleteAll(result, logger); |
| 302 | try { |
| 303 | fileTools.mkdirs(result); |
| 304 | } catch (FileNotFoundException error) { |
| 305 | throw new TunneledIOException("cannot create clean test output folder: " + result, error); |
| 306 | } |
| 307 | return result; |
| 308 | } |
| 309 | |
| 310 | /** |
| 311 | * Gets the directory where the local copy of the Jomic CVS is located. In order for this to |
| 312 | * work, you have to set the Java property jomic.home to point to this directory. |
| 313 | */ |
| 314 | public File getJomicHome() { |
| 315 | assert jomicHome != null; |
| 316 | return jomicHome; |
| 317 | } |
| 318 | |
| 319 | /** |
| 320 | * Gets a file containing a genric test comic archive with a few images. |
| 321 | */ |
| 322 | public File getTestComicFile() { |
| 323 | return getTestGeneratedInputFile(TEST_COMIC_FILE_NAME); |
| 324 | } |
| 325 | |
| 326 | /** |
| 327 | * Get a test input file from the testdata directory. |
| 328 | */ |
| 329 | public File getTestExpectedFile(String name) { |
| 330 | return new File(testExpectedDir, name); |
| 331 | } |
| 332 | |
| 333 | /** |
| 334 | * Get existing test input file from either "tests/generated" or "tests/input". |
| 335 | * |
| 336 | * @see #getTestGeneratedInputFile(String) |
| 337 | * @see #getTestInputFile(String) |
| 338 | */ |
| 339 | public File getTestFile(String name) { |
| 340 | File result; |
| 341 | File generatedFile = getTestGeneratedInputFile(name); |
| 342 | boolean generatedExists = generatedFile.exists(); |
| 343 | File inputFile = getTestInputFile(name); |
| 344 | boolean inputExists = inputFile.exists(); |
| 345 | |
| 346 | if (generatedExists) { |
| 347 | if (inputExists) { |
| 348 | throw new IllegalStateException("test file must not exist both in \"generated\" and \"input\": " |
| 349 | + name); |
| 350 | } |
| 351 | result = generatedFile; |
| 352 | } else { |
| 353 | if (!inputExists) { |
| 354 | throw new IllegalStateException("test file must exist either in \"generated\" or \"input\": " + name); |
| 355 | } |
| 356 | result = inputFile; |
| 357 | } |
| 358 | |
| 359 | return result; |
| 360 | } |
| 361 | |
| 362 | /** |
| 363 | * Get a plain file name for a test file. This does not yield a complete path, use <code>getTest*File()</code> |
| 364 | * to access or store an actual test file. |
| 365 | * |
| 366 | * @see #getTestExpectedFile(String) |
| 367 | * @see #getTestInputFile(String) |
| 368 | * @see #getTestOutputFile(String) |
| 369 | * @param testCaseClass the class where the test case resides that uses the file |
| 370 | * @param testMethodName null or the name of the method that uses the file |
| 371 | * @param name null or a short name further describing the file (if the test method |
| 372 | * needs more than one file) |
| 373 | * @param suffix null or file suffix without dot, for example "png" |
| 374 | */ |
| 375 | public String getTestFileName(Class testCaseClass, String testMethodName, String name, String suffix) { |
| 376 | assert testCaseClass != null; |
| 377 | String result = testCaseClass.getName(); |
| 378 | |
| 379 | if (testMethodName != null) { |
| 380 | result += "." + testMethodName; |
| 381 | } |
| 382 | if (name != null) { |
| 383 | result += "-" + name; |
| 384 | } |
| 385 | if (suffix != null) { |
| 386 | result += "." + suffix; |
| 387 | } |
| 388 | return result; |
| 389 | } |
| 390 | |
| 391 | /** |
| 392 | * Get files from generated and static test data directory matching a certain pattern. |
| 393 | * |
| 394 | * @param pattern a regular expression describing the names of the files to get |
| 395 | */ |
| 396 | public File[] getTestFiles(String pattern) { |
| 397 | List result = new LinkedList(); |
| 398 | File[] testDirs = new File[]{testGeneratedInputDir, testInputDir}; |
| 399 | FileFilter filter = new RegExFileFilter(pattern); |
| 400 | |
| 401 | for (int i = 0; i < testDirs.length; i += 1) { |
| 402 | File testDir = testDirs[i]; |
| 403 | File[] filesToAppend = testDir.listFiles(filter); |
| 404 | |
| 405 | if (filesToAppend != null) { |
| 406 | result.addAll(Arrays.asList(filesToAppend)); |
| 407 | } |
| 408 | } |
| 409 | |
| 410 | if (result.size() == 0) { |
| 411 | String message = "test directories must contain at least one file matching \"" |
| 412 | + pattern + "\": " + StringTools.instance().arrayToString(testDirs); |
| 413 | |
| 414 | throw new IllegalStateException(message); |
| 415 | } |
| 416 | return (File[]) result.toArray(new File[0]); |
| 417 | } |
| 418 | |
| 419 | /** |
| 420 | * Get the directory where all the test data are stored. |
| 421 | */ |
| 422 | public File getTestGeneratedInputDir() { |
| 423 | return testsDataDir; |
| 424 | } |
| 425 | |
| 426 | /** |
| 427 | * Get a generated test input file from the test data directory. |
| 428 | */ |
| 429 | public File getTestGeneratedInputFile(String name) { |
| 430 | return new File(testGeneratedInputDir, name); |
| 431 | } |
| 432 | |
| 433 | /** |
| 434 | * Get the test image with the specified <code>fileName</code>. |
| 435 | */ |
| 436 | public RenderedImage getTestImage(String fileName) |
| 437 | throws IOException { |
| 438 | assert fileName != null; |
| 439 | RenderedImage result; |
| 440 | File imageFile = getTestFile(fileName); |
| 441 | ImageInputStream in = createImageInputStream(imageFile); |
| 442 | ImageReader reader = (ImageReader) ImageIO.getImageReaders(in).next(); |
| 443 | |
| 444 | reader.setInput(in); |
| 445 | try { |
| 446 | result = PlanarImage.wrapRenderedImage(reader.read(0)); |
| 447 | } catch (Exception error) { |
| 448 | logger.warn("cannot read image using ImageIO; reverting to JAI", error); |
| 449 | result = JAI.create("fileload", imageFile.getAbsolutePath()); |
| 450 | if (result == null) { |
| 451 | throw new IOException("cannot read image file: " + imageFile); |
| 452 | } |
| 453 | } |
| 454 | return result; |
| 455 | } |
| 456 | |
| 457 | /** |
| 458 | * Get a genric test image representing some dummy comic page. |
| 459 | */ |
| 460 | public RenderedImage getTestImage() |
| 461 | throws IOException { |
| 462 | return getTestImage(TEST_IMAGE_FILE_NAME); |
| 463 | } |
| 464 | |
| 465 | /** |
| 466 | * Get a file containing a genric test image representing some dummy comic page. |
| 467 | */ |
| 468 | public File getTestImageFile() { |
| 469 | return getTestFile(TEST_IMAGE_FILE_NAME); |
| 470 | } |
| 471 | |
| 472 | /** |
| 473 | * Get a test input file from the test data directory. |
| 474 | */ |
| 475 | public File getTestInputFile(String name) { |
| 476 | return new File(testInputDir, name); |
| 477 | } |
| 478 | |
| 479 | /** |
| 480 | * Get a test input file from the test data directory. |
| 481 | */ |
| 482 | public File getTestOutputFile(String name) { |
| 483 | return new File(testOutputDir, name); |
| 484 | } |
| 485 | |
| 486 | /** |
| 487 | * Get a file containing a genric test text. |
| 488 | */ |
| 489 | public File getTestTextFile() { |
| 490 | return getTestFile(TEST_TEXT_NAME); |
| 491 | } |
| 492 | |
| 493 | /** |
| 494 | * Get the base directory where the test source code and stuff is located, typically |
| 495 | * "${build.dir}/tests". |
| 496 | */ |
| 497 | public File getTestsBaseDir() { |
| 498 | return testsBaseDir; |
| 499 | } |
| 500 | |
| 501 | /** |
| 502 | * Get <code>data[i]</code> or null in case <code>data</code> is null or smaller than <code>index</code> |
| 503 | * . |
| 504 | */ |
| 505 | private String getAtOrNull(String[] data, int index) { |
| 506 | assert index >= 0; |
| 507 | String result = null; |
| 508 | |
| 509 | if ((data != null) && (index < data.length)) { |
| 510 | result = data[index]; |
| 511 | } |
| 512 | return result; |
| 513 | } |
| 514 | |
| 515 | /** |
| 516 | * Get accessor. |
| 517 | */ |
| 518 | public static synchronized TestTools instance() { |
| 519 | if (instance == null) { |
| 520 | instance = new TestTools(); |
| 521 | } |
| 522 | return instance; |
| 523 | } |
| 524 | |
| 525 | public void assertEquals(byte[] expected, byte[] actual) { |
| 526 | assert expected != null; |
| 527 | assert actual != null; |
| 528 | int expectedCount = expected.length; |
| 529 | int actualCount = actual.length; |
| 530 | |
| 531 | for (int i = 0; i < Math.min(expectedCount, actualCount); i += 1) { |
| 532 | Assert.assertEquals("data at index " + i + " must be equal", expected[i], actual[i]); |
| 533 | } |
| 534 | Assert.assertEquals("array length must be equal", expectedCount, actualCount); |
| 535 | } |
| 536 | |
| 537 | public void assertEquals(int[] expected, int[] actual) { |
| 538 | assert expected != null; |
| 539 | assert actual != null; |
| 540 | int expectedCount = expected.length; |
| 541 | int actualCount = actual.length; |
| 542 | |
| 543 | for (int i = 0; i < Math.min(expectedCount, actualCount); i += 1) { |
| 544 | Assert.assertEquals("data at index " + i + " must be equal", expected[i], actual[i]); |
| 545 | } |
| 546 | Assert.assertEquals("array length must be equal", expectedCount, actualCount); |
| 547 | } |
| 548 | |
| 549 | public void assertEquals(String[] expected, String[] actual) { |
| 550 | assert expected != null; |
| 551 | assert actual != null; |
| 552 | int expectedCount = expected.length; |
| 553 | int actualCount = actual.length; |
| 554 | |
| 555 | for (int i = 0; i < Math.min(expectedCount, actualCount); i += 1) { |
| 556 | Assert.assertEquals("data at index " + i + " must be equal", expected[i], actual[i]); |
| 557 | } |
| 558 | Assert.assertEquals("array length must be equal", expectedCount, actualCount); |
| 559 | } |
| 560 | |
| 561 | public void assertEquals(Dimension expected, Dimension actual) { |
| 562 | if ((expected == null) || (actual == null)) { |
| 563 | Assert.assertEquals(expected, actual); |
| 564 | } |
| 565 | Assert.assertEquals(expected.width, actual.width); |
| 566 | Assert.assertEquals(expected.height, actual.height); |
| 567 | } |
| 568 | |
| 569 | public void assertFilesEqual(String baseName) { |
| 570 | File expectedFile = getTestExpectedFile(baseName); |
| 571 | File outputFile = getTestOutputFile(baseName); |
| 572 | |
| 573 | if (expectedFile.exists()) { |
| 574 | assertFilesEqual(expectedFile, outputFile); |
| 575 | } else { |
| 576 | logger.warn("cannot find expected file, creating it: " + expectedFile); |
| 577 | try { |
| 578 | fileTools.copyFile(outputFile, expectedFile); |
| 579 | } catch (IOException error) { |
| 580 | String errorMessage = "cannot create expected file"; |
| 581 | IllegalStateException tunneledError = new IllegalStateException(errorMessage); |
| 582 | |
| 583 | tunneledError.initCause(error); |
| 584 | throw tunneledError; |
| 585 | } |
| 586 | } |
| 587 | } |
| 588 | |
| 589 | public void assertFilesEqual(File expectedFile, File actualFile) { |
| 590 | // TODO: Improve performance by changing read() to read(buffer). |
| 591 | // TODO: Improve error message by including hex dump of a few characters surrounding the mismatch. |
| 592 | try { |
| 593 | InputStream expectedStream = new FileInputStream(expectedFile); |
| 594 | |
| 595 | try { |
| 596 | InputStream actualStream = new FileInputStream(actualFile); |
| 597 | long filePosition = 0; |
| 598 | |
| 599 | try { |
| 600 | int expectedChar = 0; |
| 601 | int actualChar = 0; |
| 602 | |
| 603 | while ((expectedChar == actualChar) && (expectedChar >= 0) && (actualChar >= 0)) { |
| 604 | expectedChar = expectedStream.read(); |
| 605 | actualChar = actualStream.read(); |
| 606 | if (expectedChar != actualChar) { |
| 607 | // We are doing this inside an "if" so the message only has to be |
| 608 | // computed in case anythings wrong. This improves performance. |
| 609 | String message = "character at postion " + filePosition + " in " + expectedFile |
| 610 | + " must match " + actualFile; |
| 611 | |
| 612 | Assert.assertEquals(message, expectedChar, actualChar); |
| 613 | } else { |
| 614 | filePosition += 1; |
| 615 | } |
| 616 | } |
| 617 | } finally { |
| 618 | actualStream.close(); |
| 619 | } |
| 620 | } finally { |
| 621 | expectedStream.close(); |
| 622 | } |
| 623 | } catch (IOException error) { |
| 624 | String errorMessage = "cannot compare files " + expectedFile + " and " + actualFile; |
| 625 | |
| 626 | throw new TunneledIOException(errorMessage, error); |
| 627 | } |
| 628 | } |
| 629 | |
| 630 | public void assertGreaterOrEqual(double actual, double limit) { |
| 631 | Assert.assertTrue("" + actual + " >= " + limit, actual >= limit); |
| 632 | } |
| 633 | |
| 634 | public void assertGreaterOrEqual(long actual, long limit) { |
| 635 | Assert.assertTrue("" + actual + " >= " + limit, actual >= limit); |
| 636 | } |
| 637 | |
| 638 | public void assertGreaterThan(double actual, double limit) { |
| 639 | Assert.assertTrue("" + actual + " > " + limit, actual > limit); |
| 640 | } |
| 641 | |
| 642 | public void assertGreaterThan(long actual, long limit) { |
| 643 | Assert.assertTrue("" + actual + " > " + limit, actual > limit); |
| 644 | } |
| 645 | |
| 646 | public void assertLessOrEqual(double actual, double limit) { |
| 647 | Assert.assertTrue("" + actual + " <= " + limit, actual <= limit); |
| 648 | } |
| 649 | |
| 650 | public void assertLessOrEqual(long actual, long limit) { |
| 651 | Assert.assertTrue("" + actual + " <= " + limit, actual <= limit); |
| 652 | } |
| 653 | |
| 654 | public void assertLessThan(double actual, double limit) { |
| 655 | Assert.assertTrue("" + actual + " < " + limit, actual < limit); |
| 656 | } |
| 657 | |
| 658 | public void assertLessThan(long actual, long limit) { |
| 659 | Assert.assertTrue("" + actual + " < " + limit, actual < limit); |
| 660 | } |
| 661 | |
| 662 | public void copyTestFile(String sourceName, String targetName) |
| 663 | throws IOException { |
| 664 | assert sourceName != null; |
| 665 | assert targetName != null; |
| 666 | File source = getTestFile(sourceName); |
| 667 | File target = getTestGeneratedInputFile(targetName); |
| 668 | |
| 669 | fileTools.copyFile(source, target); |
| 670 | } |
| 671 | |
| 672 | /** |
| 673 | * Same as ImageIO.createImageInputStream, but throws a <code>FileNotFoundException</code> if |
| 674 | * <code>imageFile</code> cannot be found (instead of returning <code>null</code>). Why the |
| 675 | * original does not work that way already is beyond me. |
| 676 | */ |
| 677 | public ImageInputStream createImageInputStream(File imageFile) |
| 678 | throws IOException { |
| 679 | assert imageFile != null; |
| 680 | ImageInputStream result = ImageIO.createImageInputStream(imageFile); |
| 681 | |
| 682 | if (result == null) { |
| 683 | throw new FileNotFoundException("cannot find image file: " + imageFile); |
| 684 | } |
| 685 | return result; |
| 686 | } |
| 687 | |
| 688 | /** |
| 689 | * Create a temporary test directory. |
| 690 | */ |
| 691 | public File createTempDir(Class clazz, String prefix) |
| 692 | throws IOException { |
| 693 | File result = fileTools.createTempDir(clazz.getName() + "-" + prefix); |
| 694 | |
| 695 | // TODO: automatically remove directory when done. |
| 696 | return result; |
| 697 | } |
| 698 | |
| 699 | /** |
| 700 | * Create a temporary file that will be deleted on exit unless the system property |
| 701 | * net.sf.jomic.test.keepTempFiles has been set to <code>true</code>. |
| 702 | * |
| 703 | * @see File#createTempFile(java.lang.String, java.lang.String) |
| 704 | * @see File#deleteOnExit() |
| 705 | */ |
| 706 | public File createTempFile(String prefix, String suffix) |
| 707 | throws IOException { |
| 708 | File result = File.createTempFile(prefix, suffix); |
| 709 | |
| 710 | if (Boolean.getBoolean(PropertyConstants.TEST_KEEP_TEMP_FILES)) { |
| 711 | logger.warn("keeping temp file: " + StringTools.instance().sourced(result.getAbsolutePath())); |
| 712 | } else { |
| 713 | result.deleteOnExit(); |
| 714 | } |
| 715 | return result; |
| 716 | } |
| 717 | |
| 718 | /** |
| 719 | * Create a temporary file that will be deleted on exit unless the system property |
| 720 | * net.sf.jomic.test.keepTempFiles has been set to <code>true</code>. The name of the file is |
| 721 | * prefixed by the name of <code>claszz</code> and <code>prefix</code>, separated by a hyphen |
| 722 | * (-) provided <code>prefix</code> is not <code>null</code>. |
| 723 | * |
| 724 | * @see File#createTempFile(java.lang.String, java.lang.String) |
| 725 | * @see File#deleteOnExit() |
| 726 | */ |
| 727 | public File createTempFile(Class clazz, String prefix, String suffix) |
| 728 | throws IOException { |
| 729 | assert clazz != null; |
| 730 | String actualPrefix = clazz.getName(); |
| 731 | |
| 732 | if (prefix != null) { |
| 733 | actualPrefix += "-" + prefix; |
| 734 | } |
| 735 | return createTempFile(actualPrefix, suffix); |
| 736 | } |
| 737 | |
| 738 | /** |
| 739 | * Create a temporary ZIP archive with a file name derived from <code>caller</code> and <code>baseName</code> |
| 740 | * . For <code>inNames</code> and <code>outNames</code> the same things apply as with <code>createTestZipArchive()</code> |
| 741 | * . |
| 742 | * |
| 743 | * @see #createTempFile(Class, String, String) |
| 744 | * @see #createTestZipArchive(File, String[], String[]) |
| 745 | */ |
| 746 | public File createTempZipArchive(Class caller, String baseName, String[] inNames, |
| 747 | String[] outNames) |
| 748 | throws IOException { |
| 749 | File result = createTempFile(caller, baseName, ".cbz"); |
| 750 | |
| 751 | createTestZipArchive(result, inNames, outNames); |
| 752 | return result; |
| 753 | } |
| 754 | |
| 755 | /** |
| 756 | * Create and show a test frame. |
| 757 | * |
| 758 | * @param component the JComponent to show inside the frame |
| 759 | * @param test the class of the TestCase opening the frame; to be shown as title |
| 760 | * @param method the method in the TestCase opening the frame, or <code>null</code> if none |
| 761 | * needed to be shown in the title |
| 762 | */ |
| 763 | public JFrame createTestFrame(JComponent component, Class test, String method) { |
| 764 | assert test != null; |
| 765 | assert component != null; |
| 766 | String title = test.getClass().getName(); |
| 767 | |
| 768 | if (method != null) { |
| 769 | title += "." + method; |
| 770 | } |
| 771 | JFrame result = new JFrame(title); |
| 772 | boolean ok = false; |
| 773 | |
| 774 | try { |
| 775 | result.getContentPane().add(component); |
| 776 | result.setSize(FRAME_WIDTH, FRAME_HEIGHT); |
| 777 | result.pack(); |
| 778 | result.setVisible(true); |
| 779 | ok = true; |
| 780 | } finally { |
| 781 | if (!ok) { |
| 782 | result.dispose(); |
| 783 | } |
| 784 | } |
| 785 | return result; |
| 786 | } |
| 787 | |
| 788 | /** |
| 789 | * Create a test ZIP archive in <code>zipFile</code>. One of <code>inNames</code> or <code>outNames</code> |
| 790 | * can be null or shorter than the other, in which case the missing entries will be filled with |
| 791 | * names derived from the other names. |
| 792 | * |
| 793 | * @see #getTestFile(String) |
| 794 | * @param inNames relative input file names that will be expaned to full paths using <code>getTestFile()</code> |
| 795 | * @param outNames file names to be used in archive (with path); missing names will be filled |
| 796 | * with the plain file name (without path) derived from the corresponding entry in <code>inNames</code> |
| 797 | * . |
| 798 | */ |
| 799 | public void createTestZipArchive(File zipFile, String[] inNames, |
| 800 | String[] outNames) |
| 801 | throws IOException { |
| 802 | assert !((inNames == null) && (outNames == null)); |
| 803 | int inLength = lengthOr0(inNames); |
| 804 | int outLength = lengthOr0(outNames); |
| 805 | |
| 806 | int nameCount = Math.max(inLength, outLength); |
| 807 | String[] filledInNames = new String[nameCount]; |
| 808 | String[] filledOutNames = new String[nameCount]; |
| 809 | |
| 810 | for (int i = 0; i < nameCount; i += 1) { |
| 811 | String inName = getAtOrNull(inNames, i); |
| 812 | String outName = getAtOrNull(outNames, i); |
| 813 | |
| 814 | if (inName == null) { |
| 815 | assert outName != null; |
| 816 | inName = outName; |
| 817 | } else if (outName == null) { |
| 818 | outName = inName; |
| 819 | } else { |
| 820 | assert inName != null; |
| 821 | assert outName != null; |
| 822 | } |
| 823 | filledInNames[i] = getTestFile(inName).getAbsolutePath(); |
| 824 | filledOutNames[i] = outName; |
| 825 | } |
| 826 | |
| 827 | createZipArchive(zipFile, filledInNames, filledOutNames); |
| 828 | } |
| 829 | |
| 830 | public void createZipArchive(File zipFile, String[] inNames, |
| 831 | String[] outNames) |
| 832 | throws IOException { |
| 833 | assert zipFile != null; |
| 834 | assert inNames != null; |
| 835 | assert inNames.length > 0; |
| 836 | assert outNames != null; |
| 837 | assert inNames.length == outNames.length : |
| 838 | "outNames.length must " + inNames.length + " but is " + outNames.length; |
| 839 | boolean done = false; |
| 840 | |
| 841 | if (logger.isInfoEnabled()) { |
| 842 | logger.info("create zip archive: " + zipFile); |
| 843 | } |
| 844 | ZipOutputStream out = new ZipOutputStream(new FileOutputStream(zipFile)); |
| 845 | |
| 846 | try { |
| 847 | for (int i = 0; i < inNames.length; i += 1) { |
| 848 | String inName = inNames[i]; |
| 849 | String outName = null; |
| 850 | |
| 851 | assert inName != null; |
| 852 | if ((outNames != null) && (outNames.length > i)) { |
| 853 | outName = outNames[i]; |
| 854 | } |
| 855 | if (outName == null) { |
| 856 | outName = inName; |
| 857 | } |
| 858 | addZipEntry(out, inName, outName); |
| 859 | done = true; |
| 860 | } |
| 861 | } finally { |
| 862 | out.close(); |
| 863 | if (!done) { |
| 864 | // Delete incomplete archive. |
| 865 | fileTools.deleteOrWarn(zipFile, logger); |
| 866 | } |
| 867 | } |
| 868 | } |
| 869 | |
| 870 | /** |
| 871 | * Waits some time. The exact time can be specified in the property PROPERTY_JOMIC_DELAY. If |
| 872 | * this is missing, use an internal default value. |
| 873 | */ |
| 874 | public void waitSomeTime() { |
| 875 | try { |
| 876 | Thread.sleep(delay); |
| 877 | } catch (InterruptedException interruption) { |
| 878 | logger.warn("interrupted", interruption); |
| 879 | } |
| 880 | } |
| 881 | |
| 882 | public void writeImageFile(File targetImageFile, RenderedImage imageToWrite) |
| 883 | throws IOException { |
| 884 | Iterator writers = ImageIO.getImageWritersByFormatName(fileTools.getSuffix(targetImageFile)); |
| 885 | ImageWriter writer = (ImageWriter) writers.next(); |
| 886 | |
| 887 | if (logger.isInfoEnabled()) { |
| 888 | logger.info("write test image " + stringTools.sourced(targetImageFile.getAbsolutePath())); |
| 889 | } |
| 890 | ImageOutputStream ios = ImageIO.createImageOutputStream(targetImageFile); |
| 891 | |
| 892 | try { |
| 893 | writer.setOutput(ios); |
| 894 | writer.write(imageToWrite); |
| 895 | } finally { |
| 896 | ios.close(); |
| 897 | } |
| 898 | } |
| 899 | |
| 900 | private void addZipEntry(ZipOutputStream out, String inName, String outName) |
| 901 | throws IOException { |
| 902 | byte[] buffer = new byte[BUFFER_SIZE]; |
| 903 | ZipEntry zipEntry = new ZipEntry(outName); |
| 904 | |
| 905 | if (logger.isDebugEnabled()) { |
| 906 | logger.debug("add " + stringTools.sourced(inName) + " + " |
| 907 | + stringTools.sourced(outName)); |
| 908 | } |
| 909 | |
| 910 | out.putNextEntry(zipEntry); |
| 911 | |
| 912 | FileInputStream in = new FileInputStream(inName); |
| 913 | boolean continueReading = true; |
| 914 | |
| 915 | try { |
| 916 | while (continueReading) { |
| 917 | int bytesRead = in.read(buffer); |
| 918 | |
| 919 | continueReading = (bytesRead > 0); |
| 920 | if (continueReading) { |
| 921 | out.write(buffer, 0, bytesRead); |
| 922 | } |
| 923 | } |
| 924 | } finally { |
| 925 | in.close(); |
| 926 | } |
| 927 | out.closeEntry(); |
| 928 | } |
| 929 | |
| 930 | private int lengthOr0(Object[] some) { |
| 931 | int result; |
| 932 | |
| 933 | if (some == null) { |
| 934 | result = 0; |
| 935 | } else { |
| 936 | result = some.length; |
| 937 | } |
| 938 | return result; |
| 939 | } |
| 940 | } |