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 | } |