/*
* Copyright 2010 the original author or authors.
*
* Licensed 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.
*/
package org.gradle.foundation;
import junit.framework.Assert;
import junit.framework.AssertionFailedError;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.tasks.TaskContainer;
import org.gradle.foundation.ipc.gradle.ExecuteGradleCommandServerProtocol;
import org.gradle.gradleplugin.foundation.DOM4JSerializer;
import org.gradle.gradleplugin.foundation.GradlePluginLord;
import org.gradle.gradleplugin.foundation.request.ExecutionRequest;
import org.gradle.gradleplugin.foundation.request.RefreshTaskListRequest;
import org.gradle.gradleplugin.foundation.request.Request;
import org.gradle.util.UncheckedException;
import org.jmock.Expectations;
import org.jmock.integration.junit4.JUnit4Mockery;
import javax.swing.filechooser.FileFilter;
import java.io.File;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Utility class for initializing various test objects related.
*
* @author mhunsicker
*/
public class TestUtility {
private static long uniqueNameCounter = 1; //used to make unique names for JMock objects.
/**
* Creates a mock project with the specified properties.
*
* Note: depth is 0 for a root project. 1 for a root project's subproject, etc.
*/
public static Project createMockProject(JUnit4Mockery context, final String name, final String buildFilePath, final int depth, Project[] subProjectArray, Task[] tasks, String[] defaultTasks, Project... dependsOnProjects) {
final Project project = context.mock(Project.class, "[project]_" + name + '_' + uniqueNameCounter++);
context.checking(new Expectations() {{
allowing(project).getName();
will(returnValue(name));
allowing(project).getDescription();
will(returnValue(null));
allowing(project).getBuildFile();
will(returnValue(new File(buildFilePath)));
allowing(project).getDepth();
will(returnValue(depth));
}});
attachSubProjects(context, project, subProjectArray);
attachTasks(context, project, tasks);
assignDefaultTasks(context, project, defaultTasks);
assignDependsOnProjects(context, project, dependsOnProjects);
return project;
}
/**
* This makes the sub projects children of the parent project. If you call this repeatedly on the same
* parentProject, any previous sub projects will be replaced with the new ones.
*
* @param context the mock context
* @param parentProject where to attach the sub projects. This must be a mock object.
* @param subProjectArray the sub projects to attach to the parent. These must be mock objects. Pass in null or an
* empty array to set no sub projects.
*/
public static void attachSubProjects(JUnit4Mockery context, final Project parentProject, Project... subProjectArray) {
final Map<String, Project> childProjects = new LinkedHashMap<String, Project>();
if (subProjectArray != null) {
for (final Project subProject : subProjectArray) {
childProjects.put(String.valueOf(childProjects.size()), subProject);
context.checking(new Expectations() {{
allowing(subProject).getParent();
will(returnValue(parentProject));
}});
}
}
//populate the subprojects (this may be an empty set)
context.checking(new Expectations() {{
allowing(parentProject).getChildProjects();
will(returnValue(childProjects));
}});
}
/**
* Creates a mock task with the specified properites.
*/
public static Task createTask(JUnit4Mockery context, final String name, final String description) {
final Task task = context.mock(Task.class, "[task]_" + name + '_' + uniqueNameCounter++);
context.checking(new Expectations() {{
allowing(task).getName();
will(returnValue(name));
allowing(task).getDescription();
will(returnValue(description));
}});
return task;
}
/**
* This makes the tasks children of the parent project. If you call this repeatedly on the same parentProject, any
* previous tasks will be replaced with the new ones.
*
* @param context the mock context
* @param parentProject where to attach the sub projects. This must be a mock object.
* @param taskArray the tasks to attach to the parent. these must be mock objects. Pass in null or an empty array to
* set no tasks.
*/
public static void attachTasks(JUnit4Mockery context, final Project parentProject, Task... taskArray) {
//first, make our project return our task container
final TaskContainer taskContainer = context.mock(TaskContainer.class, "[taskcontainer]_" + parentProject.getName() + '_' + uniqueNameCounter++);
context.checking(new Expectations() {{
allowing(parentProject).getTasks();
will(returnValue(taskContainer));
}});
final Set<Task> set = new LinkedHashSet<Task>(); //using a LinkedHashSet rather than TreeSet (which is what gradle uses) so I don't have to deal with compareTo() being called on mock objects.
if (taskArray != null && taskArray.length != 0) {
set.addAll(Arrays.asList(taskArray));
//set the parent project of the tasks
for (int index = 0; index < taskArray.length; index++) {
final Task task = taskArray[index];
context.checking(new Expectations() {{
allowing(task).getProject();
will(returnValue(parentProject));
}});
}
}
//populate the task container (this may be an empty set)
context.checking(new Expectations() {{
allowing(taskContainer).iterator();
will(returnIterator(set));
}});
}
private static void assignDefaultTasks(JUnit4Mockery context, final Project project, final String... defaultTasksArray) {
final List<String> defaultTaskList = new ArrayList<String>();
if (defaultTasksArray != null && defaultTasksArray.length != 0) {
defaultTaskList.addAll(Arrays.asList(defaultTasksArray));
}
context.checking(new Expectations() {{
allowing(project).getDefaultTasks();
will(returnValue(defaultTaskList));
}});
}
private static void assignDependsOnProjects(JUnit4Mockery context, final Project project, final Project... dependsOnProjects) {
final Set<Project> set = new LinkedHashSet<Project>(); //using a LinkedHashSet rather than TreeSet (which is what gradle uses) so I don't have to deal with compareTo() being called on mock objects.
if (dependsOnProjects != null && dependsOnProjects.length != 0) {
set.addAll(Arrays.asList(dependsOnProjects));
}
//populate the subprojects (this may be an empty set)
context.checking(new Expectations() {{
allowing(project).getDependsOnProjects();
will(returnValue(set));
}});
}
public static <T> void assertListContents(List<T> actualObjects, T... expectedObjectsArray) {
assertListContents(actualObjects, Arrays.asList(expectedObjectsArray));
}
public static <T> void assertListContents(List<T> actualObjects, List<T> expectedObjects) {
assertUnorderedListContents(actualObjects, expectedObjects);
}
/**
* This asserts the contents of the list are as expected. The important aspect of this function is that we don't
* care about ordering. We just want to make sure the contents are the same.
*
* @param actualObjecs the list to check
* @param expectedObjects what we expect in the list
*/
public static <T> void assertUnorderedListContents(List<T> actualObjecs, List<T> expectedObjects) {
List<T> expectedObjecsList = new ArrayList<T>(expectedObjects); //make a copy of it, so we can modify it.
while (!expectedObjecsList.isEmpty()) {
T expectedObject = expectedObjecsList.remove(0);
if (!actualObjecs.contains(expectedObject)) {
throw new AssertionFailedError(
"Failed to locate object. Sought object:\n" + expectedObject + "\n\nExpected:\n" + dumpList(
expectedObjects) + "\nActual:\n" + dumpList(actualObjecs));
}
}
if (actualObjecs.size() != expectedObjects.size()) {
throw new AssertionFailedError(
"Expected " + expectedObjects.size() + " items but found " + actualObjecs.size() + "\nExpected:\n"
+ dumpList(expectedObjects) + "\nActual:\n" + dumpList(actualObjecs));
}
}
//function for getting a prettier dump of a list.
public static String dumpList(List list) {
if (list == null) {
return "[null]";
}
if (list.isEmpty()) {
return "[empty]";
}
StringBuilder builder = new StringBuilder();
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
Object object = iterator.next();
if (object == null) {
builder.append("**** [null object in list] ****\n");
} else {
builder.append(object.toString()).append('\n');
}
}
return builder.toString();
}
/**
* This is an ExportInteraction implemention meant to be used by tests. You pass it a file to use and we'll return
* that in promptForFile. This also checks to ensure something doesn't happen where we get into an endless loop if
* promptForFile is called repeatedly. This can happen if promptForFile is called and its return value fails some
* form of validation which makes promptForFile get called again or if you deny overwriting the file. You'll get
* prompted again.
*/
public static class TestExportInteraction implements DOM4JSerializer.ExportInteraction {
private File file;
private boolean confirmOverwrite;
private int promptCount;
public TestExportInteraction(File file, boolean confirmOverwrite) {
this.file = file;
this.confirmOverwrite = confirmOverwrite;
}
public File promptForFile(FileFilter fileFilters) {
if (promptCount == 100) {
throw new AssertionFailedError("Possible endless loop. PromptForFile has been called 100 times.");
}
promptCount++;
return file;
}
/**
* The file already exists. Confirm whether or not you want to overwrite it.
*
* @param file the file in question
* @return true to overwrite it, false not to.
*/
public boolean confirmOverwritingExisingFile(File file) {
return confirmOverwrite;
}
public void reportError(String error) {
throw new AssertionFailedError("Unexpected error: " + error);
}
}
/**
* This is an ImportInteraction implementation meant to be used by tests. See TestExportInteraction for more
* information.
*/
public static class TestImportInteraction implements DOM4JSerializer.ImportInteraction {
private File file;
private int promptCount;
public TestImportInteraction(File file) {
this.file = file;
}
public File promptForFile(FileFilter fileFilters) {
if (promptCount == 100) {
throw new AssertionFailedError("Possible endless loop. PromptForFile has been called 100 times.");
}
promptCount++;
return file;
}
public void reportError(String error) {
throw new AssertionFailedError("Unexpected error: " + error);
}
}
//wrapper around File.createTempFile just so we don't have to deal with the exception for tests.
/**
* This refreshes the projects but blocks until it is complete (its being executed in a separate process).
*
* @param gradlePluginLord the plugin lord (will be used to execute the command and store the results).
*/
public static void refreshProjectsBlocking(GradlePluginLord gradlePluginLord, int maximumWaitValue, TimeUnit maximumWaitUnits) {
refreshProjectsBlocking(gradlePluginLord, new ExecuteGradleCommandServerProtocol.ExecutionInteraction() {
public void reportExecutionStarted() {
}
public void reportNumberOfTasksToExecute( int size ) {
}
public void reportExecutionFinished(boolean wasSuccessful, String message, Throwable throwable) {
}
public void reportTaskStarted(String message, float percentComplete) {
}
public void reportTaskComplete(String message, float percentComplete) {
}
public void reportLiveOutput(String message) {
}
}, maximumWaitValue, maximumWaitUnits);
}
private static void refreshProjectsBlocking(GradlePluginLord gradlePluginLord, final ExecuteGradleCommandServerProtocol.ExecutionInteraction executionInteraction, int maximumWaitValue, TimeUnit maximumWaitUnits) {
gradlePluginLord.startExecutionQueue(); //make sure its started
final CountDownLatch complete = new CountDownLatch(1);
GradlePluginLord.RequestObserver observer = new GradlePluginLord.RequestObserver() {
public void executionRequestAdded( ExecutionRequest request ) {}
public void refreshRequestAdded( RefreshTaskListRequest request )
{
request.setExecutionInteraction( executionInteraction );
}
public void aboutToExecuteRequest( Request request ) { }
public void requestExecutionComplete( Request request, int result, String output ) {
complete.countDown();
}
};
gradlePluginLord.addRequestObserver( observer, false ); //add the observer before we add the request due to timing issues. It's possible for it to completely execute before we return from addRefreshRequestToQueue.
Request request = gradlePluginLord.addRefreshRequestToQueue();
//make sure we've got a request
Assert.assertNotNull(request);
//now wait until we're complete, but bail if we wait too long
boolean completed;
try {
completed = complete.await(maximumWaitValue, maximumWaitUnits);
} catch (InterruptedException e) {
throw UncheckedException.asUncheckedException(e);
}
gradlePluginLord.removeRequestObserver( observer );
if (!completed) //its still running. Something is wrong.
{
request.cancel(); //just to clean up after ourselves a little, cancel the request.
throw new AssertionFailedError("Failed to complete refresh in alotted time: " + maximumWaitValue + " " + maximumWaitUnits + ". Considering this failed.");
}
}
/**
This executes a command and waits until it is finished.
@param gradlePluginLord the plugin lord
@param fullCommandLine the command to execute
@param displayName the display name of the command. It doesn't usuall matter.
@param executionInteraction this gets the results of the execution
@param maximumWaitSeconds the maximum time to wait before considering it failed.
*/
public static void executeBlocking(GradlePluginLord gradlePluginLord, String fullCommandLine, String displayName, final ExecuteGradleCommandServerProtocol.ExecutionInteraction executionInteraction, int maximumWaitSeconds) {
gradlePluginLord.startExecutionQueue(); //make sure its started
final AtomicBoolean isComplete = new AtomicBoolean();
GradlePluginLord.RequestObserver observer = new GradlePluginLord.RequestObserver() {
public void executionRequestAdded( ExecutionRequest request )
{
request.setExecutionInteraction( executionInteraction );
}
public void refreshRequestAdded( RefreshTaskListRequest request ) { }
public void aboutToExecuteRequest( Request request ) { }
public void requestExecutionComplete( Request request, int result, String output ) {
isComplete.set(true);
}
};
gradlePluginLord.addRequestObserver( observer, false ); //add the observer before we add the request due to timing issues. It's possible for it to completely execute before we return from addExecutionRequestToQueue.
Request request = gradlePluginLord.addExecutionRequestToQueue( fullCommandLine, displayName );
//make sure we've got a request
Assert.assertNotNull(request);
//now sleep until we're complete, but bail if we wait too long
int totalWaitTime = 0;
while (!isComplete.get() && totalWaitTime <= maximumWaitSeconds) {
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
totalWaitTime += 1;
}
gradlePluginLord.removeRequestObserver( observer );
if (!isComplete.get()) //its still running. Something is wrong.
{
request.cancel(); //just to clean up after ourselves a little, cancel the request.
throw new AssertionFailedError("Failed to comlete execution in alotted time: " + maximumWaitSeconds + " seconds. Considering this failed.");
}
}
}