added test cases for directorymonitor.
authorerik <erik@77661180-640e-0410-b3a8-9f9b13e6d0e0>
Sun, 30 Mar 2008 12:44:05 +0000 (12:44 +0000)
committererik <erik@77661180-640e-0410-b3a8-9f9b13e6d0e0>
Sun, 30 Mar 2008 12:44:05 +0000 (12:44 +0000)
support/src/main/java/org/wamblee/io/DirectoryMonitor.java
support/src/test/java/org/wamblee/io/DirectoryMonitorTest.java [new file with mode: 0644]
support/src/test/java/org/wamblee/io/FileSystemUtils.java
support/src/test/java/org/wamblee/io/TestData.java

index 74723337f92af2744b2fd6dbfebf0d2b64785e2a..0f84183c1b4478cdb32aad51dcc49b4145eeebc8 100644 (file)
@@ -12,7 +12,7 @@
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
- */ 
+ */
 
 package org.wamblee.io;
 
@@ -21,65 +21,90 @@ import java.io.FileFilter;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
 /**
- * Monitors a directory for changes. 
- *
+ * Monitors a directory for changes.
+ * 
  * @author Erik Brakkee
  */
 public class DirectoryMonitor {
-    
-    private static final Log LOG = LogFactory.getLog(DirectoryMonitor.class);
-    
-    public static interface Listener { 
-        void fileChanged(File aFile); 
-        void fileCreated(File aFile); 
-        void fileDeleted(File aFile);
-    };
-    
-    private File _directory;
-    private FileFilter _filter; 
-    private Listener _listener;
-    private Map<File,Date> _contents; 
-    
-    public DirectoryMonitor(File aDirectory, FileFilter aFilefilter, Listener aListener) {
-        _directory = aDirectory;
-        if ( !_directory.isDirectory()) { 
-            throw new IllegalArgumentException("Directory '" + _directory + "' does not exist");
-        }
-        _filter = aFilefilter; 
-        _listener = aListener;
-        _contents = new HashMap<File,Date>();
-    }
-    
-    public void poll() {
-        LOG.debug("Polling " + _directory);
-        Map<File,Date> newContents = new HashMap<File,Date>();
-        File[] files = _directory.listFiles(_filter);
-        for (File file: files) { 
-            if ( _contents.containsKey(file)) { 
-                Date oldDate = _contents.get(file);
-                if (file.lastModified() != oldDate.getTime()) { 
-                    _listener.fileChanged(file);
-                } else { 
-                    // No change. 
-                }
-                _contents.remove(file);
-                newContents.put(file, new Date(file.lastModified()));
-            } else { 
-                _listener.fileCreated(file);
-                newContents.put(file, new Date(file.lastModified()));
-            }
-        }
-        for (File file: _contents.keySet()) { 
-            _listener.fileDeleted(file);
-        }
-        _contents = newContents;
-    }
+
+       private static final Log LOG = LogFactory.getLog(DirectoryMonitor.class);
+
+       public static interface Listener {
+               
+               void fileChanged(File aFile);
+
+               void fileCreated(File aFile);
+
+               void fileDeleted(File aFile);
+       };
+
+       private File _directory;
+       private FileFilter _filter;
+       private Listener _listener;
+       private Map<File, Date> _contents;
+
+       public DirectoryMonitor(File aDirectory, FileFilter aFilefilter,
+                       Listener aListener) {
+               _directory = aDirectory;
+               if (!_directory.isDirectory()) {
+                       throw new IllegalArgumentException("Directory '" + _directory
+                                       + "' does not exist");
+               }
+               _filter = aFilefilter;
+               _listener = aListener;
+               _contents = new HashMap<File, Date>();
+       }
+
+       /**
+        * Polls the directory for changes and notifies the listener of any changes. 
+        * In case of any exceptions thrown by the listener while handling the changes, 
+        * the next call to this method will invoked the listeners again for the same changes. 
+        */
+       public void poll() {
+               LOG.debug("Polling " + _directory);
+               Map<File, Date> newContents = new HashMap<File, Date>();
+               File[] files = _directory.listFiles(_filter);
+
+               // Check deleted files. 
+               Set<File> deletedFiles = new HashSet<File>(_contents.keySet());
+               for (File file : files) {
+                       if (file.isFile()) {
+                               if (_contents.containsKey(file)) {
+                                       deletedFiles.remove(file);
+                               }
+                       }
+               }
+               for (File file : deletedFiles) {
+                       _listener.fileDeleted(file);
+               }
+               
+               for (File file : files) {
+                       if (file.isFile()) {
+                               if (_contents.containsKey(file)) {
+                                       Date oldDate = _contents.get(file);
+                                       if (file.lastModified() != oldDate.getTime()) {
+                                               _listener.fileChanged(file);
+                                       } else {
+                                               // No change.
+                                       }
+                                       newContents.put(file, new Date(file.lastModified()));
+                               } else {
+                                       _listener.fileCreated(file);
+                                       newContents.put(file, new Date(file.lastModified()));
+                               }
+                       }
+               }
+               
+               _contents = newContents;
+       }
 
 }
diff --git a/support/src/test/java/org/wamblee/io/DirectoryMonitorTest.java b/support/src/test/java/org/wamblee/io/DirectoryMonitorTest.java
new file mode 100644 (file)
index 0000000..b778ada
--- /dev/null
@@ -0,0 +1,116 @@
+package org.wamblee.io;
+
+import org.apache.oro.io.AwkFilenameFilter;
+import org.easymock.EasyMock;
+
+import junit.framework.TestCase;
+
+public class DirectoryMonitorTest extends TestCase {
+
+       private static final String REGEX = "^.*\\.txt$";
+       private static final String FILE1 = "file1.txt";
+
+       private TestData _data;
+       private DirectoryMonitor.Listener _listener;
+       private DirectoryMonitor _monitor;
+
+       @Override
+       protected void setUp() throws Exception {
+               super.setUp();
+               _data = new TestData(this);
+               _data.clean();
+               _listener = EasyMock.createStrictMock(DirectoryMonitor.Listener.class);
+               _monitor = new DirectoryMonitor(_data.getRoot(), new AwkFilenameFilter(
+                               REGEX), _listener);
+       }
+
+       public void testEmptyDir() {
+               // Nothing is expected to be called.
+               for (int i = 0; i < 10; i++) {
+                       EasyMock.replay(_listener);
+                       _monitor.poll();
+                       EasyMock.verify(_listener);
+                       EasyMock.reset(_listener);
+               }
+       }
+
+       public void testFileCreated() {
+               _listener.fileCreated(_data.getFile(FILE1));
+               EasyMock.replay(_listener);
+               _data.createFile(FILE1, "hello");
+               _monitor.poll();
+               EasyMock.verify(_listener);
+       }
+
+       public void testFileDeleted() {
+               _data.createFile(FILE1, "hello");
+               _monitor.poll();
+
+               EasyMock.reset(_listener);
+
+               _data.deleteFile(FILE1);
+               _listener.fileDeleted(_data.getFile(FILE1));
+               EasyMock.replay(_listener);
+               _monitor.poll();
+               EasyMock.verify(_listener);
+       }
+       
+       public void testFileChanged() throws InterruptedException { 
+               _data.createFile(FILE1, "hello");
+               _monitor.poll();
+               EasyMock.reset(_listener);
+               
+               Thread.sleep(2000);
+               _data.deleteFile(FILE1);
+               _data.createFile(FILE1, "bla");
+               
+               _listener.fileChanged(_data.getFile(FILE1));
+               EasyMock.replay(_listener);
+               _monitor.poll();
+               EasyMock.verify(_listener);
+       }
+       
+       public void testFileFilterIsUsed() {
+               _monitor.poll(); 
+               
+               _data.createFile("file.xml", "hello");
+               EasyMock.replay(_listener); 
+               _monitor.poll();
+               EasyMock.verify(_listener);
+       }
+       
+       public void testDirectoryIsIgnored() { 
+               _monitor.poll();
+               _data.createDir(FILE1);
+               EasyMock.replay(_listener); 
+               _monitor.poll();
+               EasyMock.verify(_listener);
+       }
+       
+       public void testExceptionsWIllLeadToRepeatedNotifications() { 
+               _monitor.poll();
+               _data.createFile(FILE1, "hello");
+               
+               _listener.fileCreated(_data.getFile(FILE1));
+               EasyMock.expectLastCall().andThrow(new RuntimeException());
+               EasyMock.replay(_listener);
+               try { 
+                   _monitor.poll();
+               } catch (RuntimeException e) { 
+                       EasyMock.verify(_listener);
+                       EasyMock.reset(_listener);
+                       
+                       // polling again should lead to the same filecreated call. 
+                       // this time no exception is thrown. 
+                       
+                       _listener.fileCreated(_data.getFile(FILE1));
+                       EasyMock.replay(_listener);
+                       _monitor.poll();
+                       EasyMock.verify(_listener);
+                       return; 
+               }
+               fail(); // should not get here. 
+               
+       
+       }
+}
index 3d0f3b3b164cb8744355b4b905791230d3c70355..ad3b4e208d27e51960b18c41ed93c939353da2a9 100644 (file)
@@ -36,205 +36,225 @@ import org.apache.commons.logging.LogFactory;
 
 /**
  * File system utilities.
- *
+ * 
  * @author Erik Brakkee
  */
 public final class FileSystemUtils {
 
-    private static final Log LOG = LogFactory.getLog(FileSystemUtils.class);
-
-    /**
-     * Test output directory relative to the sub project.
-     */
-    private static final String TEST_OUTPUT_DIR = "../target/testoutput";
-    
-    /**
-     * Test input directory relative to the sub project. 
-     */
-    private static final String TEST_INPUT_DIR = "../src/test/resources";
-
-    /*
-     * Disabled.
-     * 
-     */
-    private FileSystemUtils() {
-        // Empty
-    }
-
-    /**
-     * Deletes a directory recursively. The test case will fail if the directory
-     * does not exist or if deletion fails.
-     * 
-     * @param aDir
-     *            Directory to delete.
-     */
-    public static void deleteDirRecursively(String aDir) {
-        deleteDirRecursively(new File(aDir));
-    }
-
-    /**
-     * Deletes a directory recursively. See {@link #deleteDirRecursively}.
-     * 
-     * @param aDir
-     *            Directory.
-     */
-    public static void deleteDirRecursively(File aDir) {
-        TestCase.assertTrue(aDir.isDirectory());
-
-        for (File file : aDir.listFiles()) {
-            if (file.isDirectory()) {
-                deleteDirRecursively(file);
-            } else {
-                delete(file);
-            }
-        }
-
-        delete(aDir);
-    }
-
-    /**
-     * Deletes a file or directory. The test case will fail if the file or
-     * directory does not exist or if deletion fails. Deletion of a non-empty
-     * directory will always fail.
-     * 
-     * @param aFile
-     *            File or directory to delete.
-     */
-    public static void delete(File aFile) {
-        TestCase.assertTrue(aFile.delete());
-    }
-
-    /**
-     * Gets a path relative to a sub project. This utility should be used to
-     * easily access file paths within a subproject without requiring any
-     * specific Eclipse configuration.
-     * 
-     * @param aRelativePath
-     *            Relative path.
-     * @param aTestClass
-     *            Test class.
-     */
-    public static File getPath(String aRelativePath, Class aTestClass) {
-        CodeSource source = aTestClass.getProtectionDomain().getCodeSource();
-        if (source == null) {
-            LOG.warn("Could not obtain path for '" + aRelativePath
-                    + "' for class " + aTestClass
-                    + ", using relative path as is");
-            return new File(aRelativePath);
-        }
-        URL location = source.getLocation();
-        String protocol = location.getProtocol();
-        if (!protocol.equals("file")) {
-            LOG.warn("protocol is not 'file': " + location);
-            return new File(aRelativePath);
-        }
-
-        String path = location.getPath();
-        try {
-            path = URLDecoder.decode(location.getPath(), "UTF-8");
-        } catch (UnsupportedEncodingException e) {
-            // ignore it.. just don't decode
-            LOG.warn("Decoding path failed: '" + location.getPath() + "'", e );
-        }
-
-        return new File(new File(path).getParentFile(), aRelativePath);
-    }
-
-    /**
-     * Ensures that a directory hierarchy exists (recursively if needed). If it
-     * is not possible to create the directory, then the test case will fail.
-     * 
-     * @param aDir
-     *            Directory to create.
-     */
-    public static void createDir(File aDir) {
-        if (aDir.exists() && !aDir.isDirectory()) {
-            TestCase.fail("'" + aDir
-                    + "' already exists and is not a directory");
-        }
-        if (aDir.exists()) {
-            return;
-        }
-        createDir(aDir.getParentFile());
-        TestCase.assertTrue("Could not create '" + aDir + "'", aDir.mkdir());
-    }
-
-    /**
-     * Gets the test output directory for a specific test class.
-     * 
-     * @param aTestClass
-     *            Test class.
-     * @return Test output directory.
-     */
-    public static File getTestOutputDir(Class aTestClass) {
-        File file = getPath(TEST_OUTPUT_DIR, aTestClass);
-        String packageName = aTestClass.getPackage().getName();
-        String packagePath = packageName.replaceAll("\\.", "/");
-        return new File(file, packagePath);
-    }
-    
-    /**
-     * Gets the test input directory for a specific test class.
-     * 
-     * @param aTestClass
-     *            Test class.
-     * @return Test input directory.
-     */
-    public static File getTestInputDir(Class aTestClass) {
-        File file = getPath(TEST_INPUT_DIR, aTestClass);
-        String packageName = aTestClass.getPackage().getName();
-        String packagePath = packageName.replaceAll("\\.", "/");
-        return new File(file, packagePath);
-    }
-
-    /**
-     * Creates a directory hierarchy for the output directory of a test class if
-     * needed.
-     * 
-     * @param aTestClass
-     *            Test class
-     * @return Test directory.
-     */
-    public static File createTestOutputDir(Class aTestClass) {
-        File file = getTestOutputDir(aTestClass);
-        createDir(file);
-        return file;
-    }
-
-    /**
-     * Gets a test output file name. This returns a File object representing the
-     * output file and ensures that the directory where the file will be created
-     * already exists.
-     * 
-     * @param aName
-     *            Name of the file.
-     * @param aTestClass
-     *            Test class.
-     * @return File.
-     */
-    public static File getTestOutputFile(String aName, Class aTestClass) {
-        File file = new File(getTestOutputDir(aTestClass), aName);
-        createDir(file.getParentFile());
-        return file;
-    }
-
-    public static String read(InputStream aIs) throws IOException {
-        try {
-            StringBuffer buffer = new StringBuffer();
-            int c;
-            while ((c = aIs.read()) != -1) {
-                buffer.append((char)c);
-            }
-            return buffer.toString();
-        } finally {
-            aIs.close();
-        }
-    }
-    
-    /**
-        * Copies an input stream to an output stream. 
-        * @param aIs Input stream. 
-        * @param aOs Output stream. 
+       private static final Log LOG = LogFactory.getLog(FileSystemUtils.class);
+
+       /**
+        * Test output directory relative to the sub project.
+        */
+       private static final String TEST_OUTPUT_DIR = "../target/testoutput";
+
+       /**
+        * Test input directory relative to the sub project.
+        */
+       private static final String TEST_INPUT_DIR = "../src/test/resources";
+
+       /*
+        * Disabled.
+        * 
+        */
+       private FileSystemUtils() {
+               // Empty
+       }
+
+       /**
+        * Deletes a directory recursively. The test case will fail if the directory
+        * does not exist or if deletion fails.
+        * 
+        * @param aDir
+        *            Directory to delete.
+        */
+       public static void deleteDirRecursively(String aDir) {
+               deleteDirRecursively(new File(aDir));
+       }
+
+       /**
+        * Deletes a directory recursively. See {@link #deleteDirRecursively}.
+        * 
+        * @param aDir
+        *            Directory.
+        */
+       public static void deleteDirRecursively(File aDir) {
+               TestCase.assertTrue(aDir.isDirectory());
+
+               for (File file : aDir.listFiles()) {
+                       if (file.isDirectory()) {
+                               deleteDirRecursively(file);
+                       } else {
+                               delete(file);
+                       }
+               }
+
+               delete(aDir);
+       }
+
+       /**
+        * Deletes a file or directory. The test case will fail if the file or
+        * directory does not exist or if deletion fails. Deletion of a non-empty
+        * directory will always fail.
+        * 
+        * @param aFile
+        *            File or directory to delete.
+        */
+       public static void delete(File aFile) {
+               TestCase.assertTrue(aFile.delete());
+       }
+
+       /**
+        * Gets a path relative to a sub project. This utility should be used to
+        * easily access file paths within a subproject without requiring any
+        * specific Eclipse configuration.
+        * 
+        * @param aRelativePath
+        *            Relative path.
+        * @param aTestClass
+        *            Test class.
+        */
+       public static File getPath(String aRelativePath, Class aTestClass) {
+               CodeSource source = aTestClass.getProtectionDomain().getCodeSource();
+               if (source == null) {
+                       LOG.warn("Could not obtain path for '" + aRelativePath
+                                       + "' for class " + aTestClass
+                                       + ", using relative path as is");
+                       return new File(aRelativePath);
+               }
+               URL location = source.getLocation();
+               String protocol = location.getProtocol();
+               if (!protocol.equals("file")) {
+                       LOG.warn("protocol is not 'file': " + location);
+                       return new File(aRelativePath);
+               }
+
+               String path = location.getPath();
+               try {
+                       path = URLDecoder.decode(location.getPath(), "UTF-8");
+               } catch (UnsupportedEncodingException e) {
+                       // ignore it.. just don't decode
+                       LOG.warn("Decoding path failed: '" + location.getPath() + "'", e);
+               }
+
+               return new File(new File(path).getParentFile(), aRelativePath);
+       }
+
+       /**
+        * Ensures that a directory hierarchy exists (recursively if needed). If it
+        * is not possible to create the directory, then the test case will fail.
+        * 
+        * @param aDir
+        *            Directory to create.
+        */
+       public static void createDir(File aDir) {
+               if (aDir.exists() && !aDir.isDirectory()) {
+                       TestCase.fail("'" + aDir
+                                       + "' already exists and is not a directory");
+               }
+               if (aDir.exists()) {
+                       return;
+               }
+               createDir(aDir.getParentFile());
+               TestCase.assertTrue("Could not create '" + aDir + "'", aDir.mkdir());
+       }
+
+       /**
+        * Creates a file in a directory.  
+        * @param aDir Directory path. Will be created if it does not exist. 
+        * @param aName Filename. 
+        * @param aContents Contents.
+        */
+       public static void createFile(File aDir, String aName, InputStream aContents) {
+               createDir(aDir);
+               try {
+                       OutputStream os = new FileOutputStream(new File(aDir, aName));
+                       copyStream(aContents, os);
+               } catch (IOException e) {
+                       e.printStackTrace();
+                       TestCase.fail(e.getMessage());
+               }
+       }
+
+       /**
+        * Gets the test output directory for a specific test class.
+        * 
+        * @param aTestClass
+        *            Test class.
+        * @return Test output directory.
+        */
+       public static File getTestOutputDir(Class aTestClass) {
+               File file = getPath(TEST_OUTPUT_DIR, aTestClass);
+               String className = aTestClass.getName();
+               String classRelPath = className.replaceAll("\\.", "/");
+               return new File(file, classRelPath);
+       }
+
+       /**
+        * Gets the test input directory for a specific test class.
+        * 
+        * @param aTestClass
+        *            Test class.
+        * @return Test input directory.
+        */
+       public static File getTestInputDir(Class aTestClass) {
+               File file = getPath(TEST_INPUT_DIR, aTestClass);
+               String packageName = aTestClass.getPackage().getName();
+               String packagePath = packageName.replaceAll("\\.", "/");
+               return new File(file, packagePath);
+       }
+
+       /**
+        * Creates a directory hierarchy for the output directory of a test class if
+        * needed.
+        * 
+        * @param aTestClass
+        *            Test class
+        * @return Test directory.
+        */
+       public static File createTestOutputDir(Class aTestClass) {
+               File file = getTestOutputDir(aTestClass);
+               createDir(file);
+               return file;
+       }
+
+       /**
+        * Gets a test output file name. This returns a File object representing the
+        * output file and ensures that the directory where the file will be created
+        * already exists.
+        * 
+        * @param aName
+        *            Name of the file.
+        * @param aTestClass
+        *            Test class.
+        * @return File.
+        */
+       public static File getTestOutputFile(String aName, Class aTestClass) {
+               File file = new File(getTestOutputDir(aTestClass), aName);
+               createDir(file.getParentFile());
+               return file;
+       }
+
+       public static String read(InputStream aIs) throws IOException {
+               try {
+                       StringBuffer buffer = new StringBuffer();
+                       int c;
+                       while ((c = aIs.read()) != -1) {
+                               buffer.append((char) c);
+                       }
+                       return buffer.toString();
+               } finally {
+                       aIs.close();
+               }
+       }
+
+       /**
+        * Copies an input stream to an output stream.
+        * 
+        * @param aIs
+        *            Input stream.
+        * @param aOs
+        *            Output stream.
         */
        public static void copyStream(InputStream aIs, OutputStream aOs) {
                try {
@@ -243,7 +263,7 @@ public final class FileSystemUtils {
                                aOs.write(c);
                        }
                        aIs.close();
-                       aOs.close(); 
+                       aOs.close();
                } catch (IOException e) {
                        e.printStackTrace();
                        Assert.fail(e.getMessage());
@@ -313,7 +333,7 @@ public final class FileSystemUtils {
                                        + aTarget.getPath() + " failed.", false);
                }
        }
-       
+
        /**
         * Remove all files within a given directory including the directory itself.
         * This only attempts to remove regular files and not directories within the
@@ -327,7 +347,7 @@ public final class FileSystemUtils {
                cleanDir(aDir);
                delete(aDir);
        }
-       
+
        /**
         * Remove all regular files within a given directory.
         * 
index cc815c1bbe4b33083788b941e717f2fbc4ae8506..58bea68ec119becf74222451cf497c40ba3b8f06 100644 (file)
@@ -15,6 +15,7 @@
  */
 package org.wamblee.io;
 
+import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
@@ -27,7 +28,7 @@ import java.nio.channels.FileChannel;
 import junit.framework.Assert;
 
 /**
- * TestData provides a convenient interface for managing test output files.  
+ * TestData provides a convenient interface for managing test output files.
  * 
  * @author Erik Brakkee
  */
@@ -37,13 +38,14 @@ public final class TestData {
        private File _root;
 
        /**
-        * Test data to be constructed in the setUp of a test.
-        * {@link #clean()} must be called to make sure that this 
-        * directory is empty before executing a test. 
+        * Test data to be constructed in the setUp of a test. {@link #clean()} must
+        * be called to make sure that this directory is empty before executing a
+        * test.
         */
        public TestData(Object aTestcase) {
                _testcase = aTestcase;
                _root = getTestRootDir(aTestcase);
+               FileSystemUtils.createDir(_root);
        }
 
        /**
@@ -55,6 +57,28 @@ public final class TestData {
                return FileSystemUtils.getTestOutputDir(aTestcase.getClass());
        }
 
+       public void createFile(String aRelative, String aFile, InputStream aContents) {
+               FileSystemUtils
+                               .createFile(new File(_root, aRelative), aFile, aContents);
+       }
+
+       public void createFile(String aFile, String aContents) {
+               createFile(".", aFile, aContents);
+       }
+
+       public void createFile(String aRelative, String aFile, String aContents) {
+               InputStream is = new ByteArrayInputStream(aContents.getBytes());
+               FileSystemUtils.createFile(new File(_root, aRelative), aFile, is);
+       }
+       
+       public void deleteFile(String aFile) {
+               deleteFile(".", aFile);
+       }
+
+       public void deleteFile(String aRelative, String aFile) {
+               FileSystemUtils.delete(new File(_root, aFile));
+       }
+
        /**
         * Returns a temporary directory.
         * 
@@ -69,6 +93,7 @@ public final class TestData {
         */
        public void clean() {
                FileSystemUtils.deleteDirRecursively(_root);
+               FileSystemUtils.createDir(_root);
        }
 
        /**
@@ -80,7 +105,7 @@ public final class TestData {
        public void copyDir(File aSrc) {
                FileSystemUtils.copyDir(aSrc, _root);
        }
-       
+
        /**
         * Copies a classpath resource to a relative path in the test output
         * directory.
@@ -104,13 +129,19 @@ public final class TestData {
 
        /**
         * Copies a resource to the root directory of the test output.
-        * @param aResource Resource.
+        * 
+        * @param aResource
+        *            Resource.
         */
        public void copyResource(String aResource) {
                String basename = new File(aResource).getName();
                copyResource(aResource, basename);
        }
 
+       public void createDir(String aRelative) {
+               FileSystemUtils.createDir(new File(_root, aRelative));
+       }
+
        /**
         * Deletes a file or directory relative to the test output root.
         * 
@@ -123,25 +154,38 @@ public final class TestData {
        }
 
        /**
-        * Deletes a directory including its contents. 
-        * @param aRelative Relative path. 
+        * Deletes a directory including its contents.
+        * 
+        * @param aRelative
+        *            Relative path.
         */
-       public void deleteDir(String aRelative) { 
+       public void deleteDir(String aRelative) {
                FileSystemUtils.deleteDir(new File(_root, aRelative));
        }
 
        /**
-        * Deletes a directory recursively. 
-        * @param aRelative Relative path. 
+        * Deletes a directory recursively.
+        * 
+        * @param aRelative
+        *            Relative path.
         */
-       public void deleteDirRecursively(String aRelative) { 
+       public void deleteDirRecursively(String aRelative) {
                FileSystemUtils.deleteDir(new File(_root, aRelative));
        }
-       
+
+       /**
+        * Gets the root of the test output directory.
+        * 
+        * @return Root of the test output.
+        */
+       public File getRoot() {
+               return _root;
+       }
+
        /**
-        * Gets a file object for a relative path. 
+        * Gets a file object for a relative path.
         */
-       public File getFile(String aRelative) { 
-               return new File(_root, aRelative); 
+       public File getFile(String aRelative) {
+               return new File(_root, aRelative);
        }
 }