/**
* @copyright
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, 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.
* ====================================================================
* @endcopyright
*/
package org.apache.subversion.javahl;
import org.apache.subversion.javahl.callback.*;
import org.apache.subversion.javahl.types.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Set;
import java.util.HashSet;
import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import junit.framework.TestCase;
/**
* common base class for the javahl binding tests
*/
class SVNTests extends TestCase
{
/**
* our admin object, mostly used for creating,dumping and loading
* repositories
*/
protected ISVNRepos admin;
/**
* the subversion client, what we want to test.
*/
protected ISVNClient client;
/**
* The root directory for the test data. All other files and
* directories will created under here.
*/
protected File rootDir;
/**
* The Subversion file system type.
*/
private String fsType;
/**
* the base name of the test. Together with the testCounter this will make
* up the directory name of the test.
*/
protected String testBaseName;
/**
* this counter will be incremented for every test in one suite
* (test class)
*/
protected static int testCounter;
/**
* the file in which the sample repository has been dumped.
*/
protected File greekDump;
/**
* the directory of the sample repository.
*/
protected File greekRepos;
/**
* the initial working copy of the sample repository.
*/
protected WC greekWC;
/**
* the directory "local_tmp" in the rootDir. This will be used
* for the sample repository and its dumpfile and for the config
* directory.
*/
protected File localTmp;
/**
* the directory "repositories" in the rootDir. All test repositories will
* be created here.
*/
protected File repositories;
/**
* the directory "working_copies" in the rootDir. All test working copies
* will be created here.
*/
protected File workingCopies;
/**
* the directory "config" in the localTmp. It will be used as the
* configuration directory for all the tests.
*/
protected File conf;
/**
* standard log message. Used for all commits.
*/
protected String logMessage = "Log Message";
/**
* the map of all items expected to be received by the callback for the
* log message. After each commit, this will be cleared
*/
protected Map<String, MyCommitItem> expectedCommitItems;
/**
* Common root directory for all tests. Can be set by the command
* line or by the system property <code>test.rootdir</code>. If
* not set, the current working directory of this process is used.
*/
protected static String rootDirectoryName;
/**
* common root URL for all tests. Can be set by the command line or by the
* system property "test.rooturl". If not set, the file url of the
* rootDirectoryName is used.
*/
protected static String rootUrl;
/**
* Username to use in tests
*/
protected final static String USERNAME = "jrandom";
/**
* Password to use in tests
*/
protected final static String PASSWORD = "rayjandom";
/**
* Create a JUnit <code>TestCase</code> instance.
*/
protected SVNTests()
{
init();
}
/**
* Create a JUnit <code>TestCase</code> instance.
*/
protected SVNTests(String name)
{
super(name);
init();
}
private void init()
{
// if not already set, get a usefull value for rootDir
if (rootDirectoryName == null)
rootDirectoryName = System.getProperty("test.rootdir");
if (rootDirectoryName == null)
rootDirectoryName = System.getProperty("user.dir");
rootDir = new File(rootDirectoryName);
// if not already set, get a useful value for root url
if (rootUrl == null)
rootUrl = System.getProperty("test.rooturl");
if (rootUrl == null || rootUrl.trim().length() == 0)
{
// if no root url, set build a file url
rootUrl = rootDir.toURI().toString();
// The JRE may have a different view about the number of
// '/' characters to follow "file:" in a URL than
// Subversion. We convert to the Subversion view.
if (rootUrl.startsWith("file:///"))
; // this is the form subversion needs
else if (rootUrl.startsWith("file://"))
rootUrl = rootUrl.replaceFirst("file://", "file:///");
else if (rootUrl.startsWith("file:/"))
rootUrl = rootUrl.replaceFirst("file:/", "file:///");
// According to
// http://java.sun.com/j2se/1.5.0/docs/api/java/io/File.html#toURL()
// the URL from rootDir.toURI() may end with a trailing /
// if rootDir exists and is a directory, so depending if
// the test suite has been previously run and rootDir
// exists, then the trailing / may or may not be there.
// The makeReposUrl() method assumes that the rootUrl ends
// in a trailing /, so add it now.
if (!rootUrl.endsWith("/"))
rootUrl = rootUrl + '/';
}
// Determine the Subversion file system type to use.
if (this.fsType == null)
{
this.fsType =
System.getProperty("test.fstype", ISVNRepos.FSFS).toLowerCase();
if (!(ISVNRepos.FSFS.equals(this.fsType) ||
ISVNRepos.BDB.equals(this.fsType)))
{
this.fsType = ISVNRepos.FSFS;
}
}
this.localTmp = new File(this.rootDir, "local_tmp");
this.conf = new File(this.localTmp, "config");
this.repositories = new File(this.rootDir, "repositories");
this.workingCopies = new File(this.rootDir, "working_copies");
}
/**
* Standard initialization of one test
* @throws Exception
*/
protected void setUp() throws Exception
{
super.setUp();
createDirectories();
// create and configure the needed subversion objects
admin = new SVNRepos();
initClient();
// build and dump the sample repository
File greekFiles = buildGreekFiles();
greekRepos = new File(localTmp, "repos");
greekDump = new File(localTmp, "greek_dump");
admin.create(greekRepos, true,false, null, this.fsType);
addExpectedCommitItem(greekFiles.getAbsolutePath(), null, null,
NodeKind.none, CommitItemStateFlags.Add);
client.doImport(greekFiles.getAbsolutePath(),
makeReposUrl(greekRepos).toString(),
Depth.infinity, false, false, null,
new MyCommitMessage(), null);
admin.dump(greekRepos, new FileOutputStream(greekDump),
null, null, false, false, null);
}
/**
* Create a directory for the sample (Greek) repository, config
* files, repositories and working copies.
*/
private void createDirectories()
{
this.rootDir.mkdirs();
if (this.localTmp.exists())
{
removeDirOrFile(this.localTmp);
}
this.localTmp.mkdir();
this.conf.mkdir();
this.repositories.mkdir();
this.workingCopies.mkdir();
}
/**
* Initialize {@link #client}, setting up its notifier, commit
* message handler, user name, password, config directory, and
* expected commit items.
*/
private void initClient()
throws SubversionException
{
this.client = new SVNClient();
this.client.notification2(new MyNotifier());
this.client.setPrompt(new DefaultPromptUserPassword());
this.client.username(USERNAME);
this.client.setProgressCallback(new DefaultProgressListener());
this.client.setConfigDirectory(this.conf.getAbsolutePath());
this.expectedCommitItems = new HashMap<String, MyCommitItem>();
}
/**
* the default prompt : never prompts the user, provides defaults answers
*/
protected static class DefaultPromptUserPassword implements UserPasswordCallback
{
public int askTrustSSLServer(String info, boolean allowPermanently)
{
return UserPasswordCallback.AcceptTemporary;
}
public String askQuestion(String realm, String question, boolean showAnswer)
{
return "";
}
public boolean askYesNo(String realm, String question, boolean yesIsDefault)
{
return yesIsDefault;
}
public String getPassword()
{
return PASSWORD;
}
public String getUsername()
{
return USERNAME;
}
public boolean prompt(String realm, String username)
{
return false;
}
public boolean prompt(String realm, String username, boolean maySave)
{
return false;
}
public String askQuestion(String realm, String question,
boolean showAnswer, boolean maySave)
{
return "";
}
public boolean userAllowedSave()
{
return false;
}
}
private static class DefaultProgressListener implements ProgressCallback
{
public void onProgress(ProgressEvent event)
{
// Do nothing, just receive the event
}
}
/**
* build a sample directory with test files to be used as import for
* the sample repository. Create also the master working copy test set.
* @return the sample repository
* @throws IOException
*/
private File buildGreekFiles() throws IOException
{
File greekFiles = new File(localTmp, "greek_files");
greekFiles.mkdir();
greekWC = new WC();
greekWC.addItem("",null);
greekWC.addItem("iota", "This is the file 'iota'.");
greekWC.addItem("A", null);
greekWC.addItem("A/mu", "This is the file 'mu'.");
greekWC.addItem("A/B", null);
greekWC.addItem("A/B/lambda", "This is the file 'lambda'.");
greekWC.addItem("A/B/E", null);
greekWC.addItem("A/B/E/alpha", "This is the file 'alpha'.");
greekWC.addItem("A/B/E/beta", "This is the file 'beta'.");
greekWC.addItem("A/B/F", null);
greekWC.addItem("A/C", null);
greekWC.addItem("A/D", null);
greekWC.addItem("A/D/gamma", "This is the file 'gamma'.");
greekWC.addItem("A/D/H", null);
greekWC.addItem("A/D/H/chi", "This is the file 'chi'.");
greekWC.addItem("A/D/H/psi", "This is the file 'psi'.");
greekWC.addItem("A/D/H/omega", "This is the file 'omega'.");
greekWC.addItem("A/D/G", null);
greekWC.addItem("A/D/G/pi", "This is the file 'pi'.");
greekWC.addItem("A/D/G/rho", "This is the file 'rho'.");
greekWC.addItem("A/D/G/tau", "This is the file 'tau'.");
greekWC.materialize(greekFiles);
return greekFiles;
}
/**
* Remove a file or a directory and all its content.
*
* @param path The file or directory to be removed.
*/
static final void removeDirOrFile(File path)
{
if (!path.exists())
{
return;
}
if (path.isDirectory())
{
// Recurse (depth-first), deleting contents.
for (File file : path.listFiles())
{
removeDirOrFile(file);
}
}
path.delete();
}
/**
* cleanup after one test
* @throws Exception
*/
protected void tearDown() throws Exception
{
// take care of our subversion objects.
admin.dispose();
client.dispose();
// remove the temporary directory
removeDirOrFile(localTmp);
super.tearDown();
}
/**
* Create the url for the repository to be used for the tests.
* @param file the directory of the repository
* @return the URL for the repository
* @throws SubversionException
*/
protected URI makeReposUrl(File file) throws SubversionException
{
try
{
// split the common part of the root directory
String path = file.getAbsolutePath()
.substring(this.rootDir.getAbsolutePath().length() + 1);
// append to the root url
return new URI(rootUrl + path.replace(File.separatorChar, '/'));
}
catch (URISyntaxException ex)
{
throw new SubversionException(ex.getMessage());
}
}
/**
* add another commit item expected during the callback for the
* log message.
* @param workingCopyPath the path of the of the working
* @param baseUrl the url for the repository
* @param itemPath the path of the item relative the working copy
* @param nodeKind expected node kind (dir or file or none)
* @param stateFlags expected commit state flags
* (see CommitItemStateFlags)
*/
protected void addExpectedCommitItem(String workingCopyPath,
String baseUrl,
String itemPath,
NodeKind nodeKind,
int stateFlags)
{
//determine the full working copy path and the full url of the item.
String path = null;
if (workingCopyPath != null)
if (itemPath != null)
path = workingCopyPath.replace(File.separatorChar, '/') +
'/' + itemPath;
else
path = workingCopyPath.replace(File.separatorChar, '/');
String url = null;
if (baseUrl != null)
if (itemPath != null)
url = baseUrl + '/' + itemPath;
else
url = baseUrl;
// the key of the item is either the url or the path (if no url)
String key;
if (url != null)
key = url;
else
key = path;
expectedCommitItems.put(key, new MyCommitItem(path, nodeKind,
stateFlags, url));
}
/**
* Intended to be called as part of test method execution
* (post-{@link #setUp()}). Calls <code>fail()</code> if the
* directory name cannot be determined.
*
* @return The name of the working copy administrative directory.
* @since 1.3
*/
protected String getAdminDirectoryName() {
String admDirName = null;
if (this.client != null) {
admDirName = client.getAdminDirectoryName();
}
if (admDirName == null) {
fail("Unable to determine the WC admin directory name");
}
return admDirName;
}
/**
* Represents the repository and (possibly) the working copy for
* one test.
*/
protected class OneTest
{
/**
* the file name of repository (used by SVNAdmin)
*/
protected File repository;
/**
* the file name of the working copy directory
*/
protected File workingCopy;
/**
* the url of the repository (used by SVNClient)
*/
protected URI url;
/**
* the expected layout of the working copy after the next subversion
* command
*/
protected WC wc;
/**
* The name of the test.
*/
String testName;
/**
* Build a new test setup with a new repository. If
* <code>createWC</code> is <code>true</code>, create a
* corresponding working copy and expected working copy
* layout.
*
* @param createWC Whether to create the working copy on disk,
* and initialize the expected working copy layout.
* @param loadRepos Whether to load the sample repository, or
* leave it with no initial revisions
* @throws SubversionException If there is a problem
* creating or loading the repository.
* @throws IOException If there is a problem finding the
* dump file.
*/
protected OneTest(boolean createWC, boolean loadRepos)
throws SubversionException, IOException
{
this.testName = testBaseName + ++testCounter;
this.wc = greekWC.copy();
this.repository = createInitialRepository(loadRepos);
this.url = makeReposUrl(repository);
if (createWC)
{
workingCopy = createInitialWorkingCopy(repository);
}
}
/**
* Build a new test setup with a new repository. Create a
* corresponding working copy and expected working copy
* layout.
*
* @param createWC Whether to create the working copy on disk,
* and initialize the expected working copy layout.
*
* @see #OneTest
*/
protected OneTest(boolean createWC)
throws SubversionException, IOException
{
this(createWC,true);
}
/**
* Build a new test setup with a new repository. Create a
* corresponding working copy and expected working copy
* layout.
*
* @see #OneTest
*/
protected OneTest()
throws SubversionException, IOException
{
this(true);
}
/**
* Copy the working copy and the expected working copy layout for tests
* which need multiple working copy
* @param append append to the working copy name of the original
* @return second test object.
* @throws Exception
*/
protected OneTest copy(String append)
throws SubversionException, IOException
{
return new OneTest(this, append);
}
/**
* constructor for create a copy
* @param orig original test
* @param append append this to the directory name of the original
* test
* @throws Exception
*/
private OneTest(OneTest orig, String append)
throws SubversionException, IOException
{
this.testName = testBaseName + testCounter + append;
repository = orig.getRepository();
url = orig.getUrl();
wc = orig.wc.copy();
workingCopy = createInitialWorkingCopy(repository);
}
/**
* Return the directory of the repository
* @return the repository directory name
*/
public File getRepository()
{
return repository;
}
/**
* Return the working copy directory
* @return the working copy directory
*/
public File getWorkingCopy()
{
return workingCopy;
}
/**
* Return the working copy directory name
* @return the name of the working copy directory
*/
public String getWCPath()
{
return workingCopy.getAbsolutePath();
}
/**
* Return the working copy directory name as part of a Set
* @return a Set containing only the working copy directory
*/
public Set<String> getWCPathSet()
{
Set<String> paths = new HashSet<String>(1);
paths.add(workingCopy.getAbsolutePath());
return paths;
}
/**
* Return the working copy subpath as part of a Set
* @return a Set containing only the given working copy path
*/
public Set<String> getWCPathSet(String subpath)
{
Set<String> paths = new HashSet<String>(1);
paths.add(workingCopy.getAbsolutePath() + subpath);
return paths;
}
/**
* Returns the url of repository
* @return the url
*/
public URI getUrl()
{
return url;
}
public Set<String> getUrlSet(String subpath)
{
Set<String> urls = new HashSet<String>(1);
urls.add(url + subpath);
return urls;
}
/**
* Returns the expected working copy content
* @return the expected working copy content
*/
public WC getWc()
{
return wc;
}
/**
* Create the repository for the beginning of the test.
* Assumes that {@link #testName} has been set.
*
* @return the repository directory
* @throws SubversionException If there is a problem
* creating or loading the repository.
* @throws IOException If there is a problem finding the
* dump file.
*/
protected File createInitialRepository(boolean loadGreek)
throws SubversionException, IOException
{
// build a clean repository directory
File repos = new File(repositories, this.testName);
removeDirOrFile(repos);
// create and load the repository from the default repository dump
admin.create(repos, true, false, conf, fsType);
if (loadGreek)
{
admin.load(repos, new FileInputStream(greekDump), false, false,
false, false, null, null);
}
return repos;
}
/**
* Create the working copy for the beginning of the test.
* Assumes that {@link #testName} has been set.
*
* @param repos the repository directory
* @return the directory of the working copy
* @throws Exception
*/
protected File createInitialWorkingCopy(File repos)
throws SubversionException, IOException
{
// build a clean working directory
URI uri = makeReposUrl(repos);
workingCopy = new File(workingCopies, this.testName);
removeDirOrFile(workingCopy);
// checkout the repository
client.checkout(uri.toString(), workingCopy.getAbsolutePath(),
null, null, Depth.infinity, false, false);
// sanity check the working with its expected status
checkStatus();
return workingCopy;
}
/**
* Check if the working copy has the expected status. Does
* not extract "out of date" information from the repository.
*
* @throws SubversionException If there's a problem getting
* WC status.
* @throws IOException If there's a problem comparing the
* WC to the expected state.
* @see #checkStatus(boolean)
*/
public void checkStatus()
throws SubversionException, IOException
{
checkStatus(false);
}
/**
* Check if the working copy has the expected status.
*
* @param checkRepos Whether to check the repository's "out of
* date" information.
* @throws SubversionException If there's a problem getting
* WC status.
* @throws IOException If there's a problem comparing the
* WC to the expected state.
*/
public void checkStatus(boolean checkRepos)
throws SubversionException, IOException
{
MyStatusCallback statusCallback = new MyStatusCallback();
client.status(workingCopy.getAbsolutePath(), Depth.unknown,
checkRepos, true, true, false, null, statusCallback);
wc.check(statusCallback.getStatusArray(),
workingCopy.getAbsolutePath(), checkRepos);
}
/**
* @return The name of this test.
*/
public String toString()
{
return this.testName;
}
}
/**
* internal class to receive the request for the log messages to check if
* the expected commit items are presented
*/
class MyCommitMessage implements CommitMessageCallback
{
/**
* Retrieve a commit message from the user based on the items
* to be committed
* @param elementsToBeCommitted Array of elements to be committed
* @return the log message of the commit.
*/
public String getLogMessage(Set<CommitItem> elementsToBeCommitted)
{
// check all received CommitItems are expected as received
for (CommitItem commitItem : elementsToBeCommitted)
{
// since imports do not provide a url, the key is either url or
// path
String key;
if (commitItem.getUrl() != null)
key = commitItem.getUrl();
else
key = commitItem.getPath();
MyCommitItem myItem = expectedCommitItems.get(key);
// check commit item is expected and has the expected data
assertNotNull("commit item for "+key+ " not expected", myItem);
myItem.test(commitItem, key);
}
// all remaining expected commit items are missing
for (String str : expectedCommitItems.keySet())
{
fail("commit item for "+str+" not found");
}
return logMessage;
}
}
/**
* internal class to describe an expected commit item
*/
class MyCommitItem
{
/**
* the path of the item
*/
String myPath;
/**
* the kind of node (file, directory or none, see NodeKind)
*/
NodeKind myNodeKind;
/**
* the reason why this item is committed (see CommitItemStateFlag)
*/
int myStateFlags;
/**
* the url of the item
*/
String myUrl;
/**
* build one expected commit item
* @param path the expected path
* @param nodeKind the expected node kind
* @param stateFlags the expected state flags
* @param url the expected url
*/
private MyCommitItem(String path, NodeKind nodeKind, int stateFlags,
String url)
{
myPath = path;
myNodeKind = nodeKind;
myStateFlags = stateFlags;
myUrl = url;
}
/**
* Check if the commit item has the expected data
* @param ci the commit item to check
* @param key the key of the item
*/
private void test(CommitItem ci, String key)
{
assertEquals("commit item path", myPath, ci.getPath());
assertEquals("commit item node kind", myNodeKind, ci.getNodeKind());
assertEquals("commit item state flags", myStateFlags,
ci.getStateFlags());
assertEquals("commit item url", myUrl, ci.getUrl());
// after the test, remove the item from the expected map
expectedCommitItems.remove(key);
}
}
class MyNotifier implements ClientNotifyCallback
{
/**
* Handler for Subversion notifications.
* <p/>
* Override this function to allow Subversion to send notifications
*
* @param info everything to know about this event
*/
public void onNotify(ClientNotifyInformation info)
{
}
}
private class MyStatusCallback implements StatusCallback
{
private List<Status> statuses = new ArrayList<Status>();
public void doStatus(String path, Status status)
{
if (status != null)
statuses.add(status);
}
public Status[] getStatusArray()
{
return (Status[]) statuses.toArray(new Status[statuses.size()]);
}
}
}