GitHub base test framework & the DialogManager

Introduce the DialogManager to be used instead of DialogWrapper#show
in the testable code.
Update & modify the TestDialogManager & TestDialogHandler.

Prepare a stub test for GithubShareAction.

Move github tests to a separate module with an extra dependency to
jira-connector - this is a temporary decision against a CNDFE
happening in tests. It will be removed when the problem is solved.
This commit is contained in:
Kirill Likhodedov
2013-07-01 12:27:02 +04:00
parent 8d8e924b91
commit eaf7d487ce
14 changed files with 212 additions and 74 deletions
+1
View File
@@ -59,6 +59,7 @@
<module fileurl="file://$PROJECT_DIR$/plugins/git4idea/git4idea.iml" filepath="$PROJECT_DIR$/plugins/git4idea/git4idea.iml" group="plugins/VCS" />
<module fileurl="file://$PROJECT_DIR$/plugins/git4idea/rt/git4idea-rt.iml" filepath="$PROJECT_DIR$/plugins/git4idea/rt/git4idea-rt.iml" group="plugins/VCS" />
<module fileurl="file://$PROJECT_DIR$/plugins/github/github.iml" filepath="$PROJECT_DIR$/plugins/github/github.iml" group="plugins" />
<module fileurl="file://$PROJECT_DIR$/plugins/github/github-test/github-test.iml" filepath="$PROJECT_DIR$/plugins/github/github-test/github-test.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/google-app-engine/google-app-engine.iml" filepath="$PROJECT_DIR$/plugins/google-app-engine/google-app-engine.iml" group="plugins/GAE" />
<module fileurl="file://$PROJECT_DIR$/plugins/google-app-engine/jps-plugin/google-app-engine-jps-plugin.iml" filepath="$PROJECT_DIR$/plugins/google-app-engine/jps-plugin/google-app-engine-jps-plugin.iml" group="plugins/GAE" />
<module fileurl="file://$PROJECT_DIR$/plugins/gradle/gradle.iml" filepath="$PROJECT_DIR$/plugins/gradle/gradle.iml" group="plugins" />
+2 -1
View File
@@ -160,9 +160,10 @@
serviceImplementation="git4idea.remote.GitRememberedInputs"/>
<applicationService serviceInterface="git4idea.commands.Git"
serviceImplementation="git4idea.commands.GitImpl"/>
<applicationService serviceInterface="git4idea.GitPlatformFacade"
serviceImplementation="git4idea.GitPlatformFacadeImpl" />
<applicationService serviceInterface="git4idea.DialogManager"
serviceImplementation="git4idea.DialogManager" />
<ComponentRoamingType component="Git.Settings" type="DISABLED"/>
<fileTypeFactory implementation="git4idea.vfs.GitFileTypeFactory"/>
@@ -0,0 +1,25 @@
package git4idea;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.ui.DialogWrapper;
import org.jetbrains.annotations.NotNull;
/**
* Use {@link DialogManager#show(DialogWrapper) DialogManager.show(DialogWrapper)} instead of {@link DialogWrapper#show()}
* to make the code testable:
* in the test environment such calls will be transferred to the TestDialogManager and can be handled by tests;
* in the production environment they will be simply delegated to DialogWrapper#show().
*
* @author Kirill Likhodedov
*/
public class DialogManager {
public static void show(@NotNull DialogWrapper dialog) {
ServiceManager.getService(DialogManager.class).showDialog(dialog);
}
protected void showDialog(@NotNull DialogWrapper dialog) {
dialog.show();
}
}
@@ -10,6 +10,9 @@
<applicationService serviceInterface="git4idea.commands.GitHttpAuthService"
serviceImplementation="git4idea.remote.GitHttpAuthTestService"
overrides="true" />
<applicationService serviceInterface="git4idea.DialogManager"
serviceImplementation="git4idea.test.TestDialogManager"
overrides="true" />
<projectService serviceInterface="git4idea.Notificator"
serviceImplementation="git4idea.test.TestNotificator"
@@ -15,6 +15,8 @@
*/
package git4idea.test;
import com.intellij.notification.Notification;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vcs.ProjectLevelVcsManager;
import com.intellij.openapi.vcs.impl.ProjectLevelVcsManagerImpl;
@@ -22,8 +24,11 @@ import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import git4idea.GitUtil;
import git4idea.GitVcs;
import git4idea.Notificator;
import git4idea.repo.GitRepository;
import junit.framework.Assert;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.HashMap;
@@ -35,6 +40,7 @@ import static com.intellij.dvcs.test.TestRepositoryUtil.createDir;
import static com.intellij.dvcs.test.TestRepositoryUtil.createFile;
import static git4idea.test.GitExecutor.git;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNull;
/**
* @author Kirill Likhodedov
@@ -107,4 +113,18 @@ public class GitTestUtil {
assertNotNull("Couldn't find repository for root " + root, repository);
return repository;
}
public static void assertNotification(@NotNull Project project, @Nullable Notification expected) {
if (expected == null) {
assertNull("Notification is unexpected here", expected);
return;
}
Notification actualNotification = ((TestNotificator)ServiceManager.getService(project, Notificator.class)).getLastNotification();
Assert.assertNotNull("No notification was shown", actualNotification);
Assert.assertEquals("Notification has wrong title", expected.getTitle(), actualNotification.getTitle());
Assert.assertEquals("Notification has wrong type", expected.getType(), actualNotification.getType());
Assert.assertEquals("Notification has wrong content", expected.getContent(), actualNotification.getContent());
}
}
@@ -15,16 +15,25 @@
*/
package git4idea.test;
import com.intellij.openapi.ui.DialogWrapper;
/**
* TestDialogHandler is invoked by {@link TestDialogManager} instead of showing a dialog on a screen (which is usually impossible for tests).
* It's purpose is to modify dialog fields and return the dialog exit code which will be transferred to the code which has invoked the dialog.
* TestDialogHandler is invoked by the {@link TestDialogManager} instead of showing a dialog on a screen
* (which is usually impossible for tests).
* It's purpose is to modify dialog fields and return the dialog exit code,
* which will be available to the code which has invoked the dialog.
*
* @author Kirill Likhodedov
*/
public interface TestDialogHandler<T> {
public interface TestDialogHandler<T extends DialogWrapper> {
/**
* Do something with dialog fields and return the exit code - as if user pressed one of exit buttons.
* Do something with the dialog (modify its instance fields, for example)
* and return the exit code - as if user pressed one of exit buttons.
*
* @param dialog dialog to be handled.
* @return DialogWrapper exit code, for example, {@link com.intellij.openapi.ui.DialogWrapper#OK_EXIT_CODE}.
* @return DialogWrapper exit code, for example, {@link DialogWrapper#OK_EXIT_CODE}.
*/
int handleDialog(T dialog);
}
@@ -16,27 +16,16 @@
package git4idea.test;
import com.intellij.openapi.ui.DialogWrapper;
import org.jetbrains.annotations.Nullable;
import com.intellij.util.containers.ContainerUtil;
import git4idea.DialogManager;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
/**
* TestDialogManager instead of showing the dialog, gives the control to a {@link TestDialogHandler} which can specify the dialog exit code
* (thus simulation different user choices) or even change other elements in the dialog.
* To use it a test should:
* <ol>
* <li>register {@link TestDialogManager} as the {@link DialogManager} implementation:
* <pre><code>
* String key = "git4idea.DialogManager";
* MutablePicoContainer picoContainer = (MutablePicoContainer) myProject.getPicoContainer();
* picoContainer.unregisterComponent(key);
* picoContainer.registerComponentImplementation(key, TestDialogManager.class);
* TestDialogManager dialogManager = (TestDialogManager)DialogManager.getInstance(myProject);
* </code></pre></li>
*
* <li>register its {@link TestDialogHandler}:
* <p>TestDialogManager instead of showing the dialog, gives the control to a {@link git4idea.test.TestDialogHandler} which can specify the dialog exit code
* (thus simulation different user choices) or even change other elements in the dialog.</p>
* <p>To use it a test should register the {@link TestDialogHandler} implementation. For example:
* <pre><code>
* myDialogManager.registerDialogHandler(GitConvertFilesDialog.class, new TestDialogHandler<GitConvertFilesDialog>() {
* &#064;Override public int handleDialog(GitConvertFilesDialog dialog) {
@@ -44,48 +33,24 @@ import java.util.Map;
* return GitConvertFilesDialog.OK_EXIT_CODE;
* }
* });
* </code></pre></li></ol>
* </code></pre>
* <p>Only one TestDialogHandler can be registered per test for a certain DialogWrapper class.</p>
* @see TestDialogHandler
* @author Kirill Likhodedov
*/
public class TestDialogManager {
public class TestDialogManager extends DialogManager {
private Map<Class, TestDialogHandler> myHandlers = new HashMap<Class, TestDialogHandler>();
private final Map<Class, TestDialogHandler> myHandlers = ContainerUtil.newHashMap();
private DialogWrapper myLastShownDialog;
public void show(DialogWrapper dialog) throws IllegalAccessException, NoSuchFieldException {
final TestDialogHandler handler = myHandlers.get(dialog.getClass());
int exitCode = DialogWrapper.OK_EXIT_CODE;
if (handler != null) {
exitCode = handler.handleDialog(dialog);
}
closeDialog(dialog, exitCode);
myLastShownDialog = dialog;
@Override
protected void showDialog(@NotNull DialogWrapper dialog) {
TestDialogHandler handler = myHandlers.get(dialog.getClass());
int exitCode = handler != null ? handler.handleDialog(dialog) : DialogWrapper.OK_EXIT_CODE;
dialog.close(exitCode, exitCode == DialogWrapper.OK_EXIT_CODE);
}
private static void closeDialog(DialogWrapper dialog, int exitCode) throws NoSuchFieldException, IllegalAccessException {
Field exitCodeField = DialogWrapper.class.getDeclaredField("myExitCode");
exitCodeField.setAccessible(true);
exitCodeField.set(dialog, exitCode);
Field closedField = DialogWrapper.class.getDeclaredField("myClosed");
closedField.setAccessible(true);
closedField.set(dialog, true);
}
/**
* Registers the dialog handler. Note that a test may register only one handler for one dialog type.
* For different dialog types it may register different handlers.
* @param dialogClass class of the dialog which will be handled instead of showing (dialog itself have to support this in its show() method).
* @param handler handler which will be invoked when dialog is about to show.
*/
public void registerDialogHandler(Class dialogClass, TestDialogHandler handler) {
public void registerDialogHandler(@NotNull Class<? extends DialogWrapper> dialogClass, @NotNull TestDialogHandler handler) {
myHandlers.put(dialogClass, handler);
}
@Nullable
public DialogWrapper getLastShownDialog() {
return myLastShownDialog;
}
}
@@ -29,7 +29,7 @@ import org.jetbrains.annotations.Nullable;
*/
public class TestNotificator extends Notificator {
private static final String TEST_NOTIFICATION_GROUP = "Test";
public static final String TEST_NOTIFICATION_GROUP = "Test";
private Notification myLastNotification;
public TestNotificator(@NotNull Project project) {
@@ -40,6 +40,10 @@ public class TestNotificator extends Notificator {
return myLastNotification;
}
public void notify(@NotNull Notification notification) {
myLastNotification = notification;
}
public void notify(@NotNull NotificationGroup notificationGroup, @NotNull String title, @NotNull String message, @NotNull NotificationType type) {
notify(notificationGroup, title, message, type, null);
}
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="platform-api" scope="TEST" />
<orderEntry type="module" module-name="testFramework" scope="TEST" />
<orderEntry type="module" module-name="git4idea" scope="TEST" />
<orderEntry type="module" module-name="github" scope="TEST" />
<orderEntry type="module" module-name="dvcs" scope="TEST" />
<orderEntry type="module" module-name="jira-connector" scope="TEST" />
</component>
</module>
@@ -0,0 +1,57 @@
package org.jetbrains.plugins.github;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationType;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.testFramework.TestActionEvent;
import git4idea.test.TestDialogHandler;
import git4idea.test.TestNotificator;
import org.jetbrains.plugins.github.test.GithubTest;
import org.jetbrains.plugins.github.ui.GithubShareDialog;
import static git4idea.test.GitTestUtil.assertNotification;
/**
* @author Kirill Likhodedov
*/
public class GithubShareTest extends GithubTest {
private static final String PROJECT_NAME = "new_project_from_test";
@Override
public void setUp() throws Exception {
super.setUp();
}
@Override
public void tearDown() throws Exception {
removeCreatedProject();
super.tearDown();
}
private void removeCreatedProject() {
// TODO: send a request to GitHub to remove the created project
}
public void testSimpleCase() throws Throwable {
myDialogManager.registerDialogHandler(GithubShareDialog.class, new TestDialogHandler<GithubShareDialog>() {
@Override
public int handleDialog(GithubShareDialog dialog) {
dialog.setRepositoryName(PROJECT_NAME);
return DialogWrapper.OK_EXIT_CODE;
}
});
// TODO: refactor the production code to avoid programmatically invoking the action with a fake data context
AnAction shareAction = ActionManager.getInstance().getAction("Github.Share");
shareAction.actionPerformed(new TestActionEvent(shareAction));
assertNotification(myProject, new Notification(TestNotificator.TEST_NOTIFICATION_GROUP, "Success",
"Successfully created project '" + PROJECT_NAME + "' on GitHub",
NotificationType.INFORMATION));
// TODO check that the project was created on github, and the repository was created locally and pushed there
}
}
@@ -15,40 +15,56 @@
*/
package org.jetbrains.plugins.github.test;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.testFramework.PlatformTestCase;
import com.intellij.testFramework.UsefulTestCase;
import com.intellij.testFramework.fixtures.IdeaProjectTestFixture;
import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory;
import com.intellij.util.PlatformUtilsCore;
import git4idea.DialogManager;
import git4idea.Notificator;
import git4idea.config.GitVcsSettings;
import git4idea.test.GitExecutor;
import git4idea.test.TestDialogManager;
import git4idea.test.TestNotificator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.plugins.github.GithubSettings;
import static com.intellij.dvcs.test.Executor.cd;
import static git4idea.test.GitExecutor.git;
import static org.junit.Assume.assumeNotNull;
/**
* The base class for JUnit platform tests of the github plugin.<br/>
* Extend this test to write a test on GitHub which has the following features/limitations:
* <p>The base class for JUnit platform tests of the github plugin.<br/>
* Extend this test to write a test on GitHub which has the following features/limitations:
* <ul>
* <li>This is a "platform test case", which means that IDEA [almost] production platform is set up before the test starts.</li>
* <li>Project base directory is the root of everything. </li>
* </ul>
* </ul></p>
* <p>All tests inherited from this class are required to have a login and a password to access the Github server.
* They are set up in System properties: <br/>
* <code>-Dtest.github.login=mylogin<br/>
* -Dtest.github.password=mypassword</code>
* </p>
*
* @author Kirill Likhodedov
*/
public abstract class GithubTest extends UsefulTestCase {
@NotNull protected static final String HOST = "github.com";
@NotNull protected Project myProject;
@NotNull protected VirtualFile myProjectRoot;
@NotNull protected GitVcsSettings mySettings;
@NotNull protected GitVcsSettings myGitSettings;
@NotNull protected GithubSettings myGitHubSettings;
@NotNull protected TestDialogManager myDialogManager;
@NotNull protected TestNotificator myNotificator;
@NotNull private IdeaProjectTestFixture myProjectFixture;
@SuppressWarnings({"JUnitTestCaseWithNonTrivialConstructors", "UnusedDeclaration"})
protected GithubTest() {
PlatformTestCase.initPlatformLangPrefix();
System.setProperty(PlatformUtilsCore.PLATFORM_PREFIX_KEY, "PlatformLangXml");
}
@Override
@@ -60,11 +76,23 @@ public abstract class GithubTest extends UsefulTestCase {
myProject = myProjectFixture.getProject();
myProjectRoot = myProject.getBaseDir();
cd(myProjectRoot);
git("version");
String login = System.getProperty("test.github.login");
String password = System.getProperty("test.github.password");
mySettings = GitVcsSettings.getInstance(myProject);
mySettings.getAppSettings().setPathToGit(GitExecutor.GIT_EXECUTABLE);
// TODO change to assert when a stable Github testing server is ready
assumeNotNull(login);
assumeNotNull(password);
myGitSettings = GitVcsSettings.getInstance(myProject);
myGitSettings.getAppSettings().setPathToGit(GitExecutor.GIT_EXECUTABLE);
myGitHubSettings = GithubSettings.getInstance();
myGitHubSettings.setHost(HOST);
myGitHubSettings.setLogin(login);
myGitHubSettings.setPassword(password);
myDialogManager = (TestDialogManager)ServiceManager.getService(DialogManager.class);
myNotificator = (TestNotificator)ServiceManager.getService(myProject, Notificator.class);
}
@Override
+1 -1
View File
@@ -5,7 +5,7 @@
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/resources" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/github-test/test" isTestSource="true" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
@@ -28,13 +28,13 @@ import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.VcsShowConfirmationOption;
import com.intellij.openapi.vcs.changes.ui.SelectFilesDialog;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ThrowableConsumer;
import com.intellij.util.containers.HashSet;
import git4idea.DialogManager;
import git4idea.GitLocalBranch;
import git4idea.GitUtil;
import git4idea.GitVcs;
@@ -162,7 +162,8 @@ public class GithubShareAction extends DumbAwareAction {
// Show dialog (window)
final GithubShareDialog shareDialog =
new GithubShareDialog(project, repoNamesRef.get(), userInfoRef.get().getMaxPrivateRepos() > userInfoRef.get().getPrivateRepos());
shareDialog.show();
//shareDialog.show();
DialogManager.show(shareDialog);
if (!shareDialog.isOK()) {
return;
}
@@ -342,7 +343,7 @@ public class GithubShareAction extends DumbAwareAction {
ApplicationManager.getApplication().invokeAndWait(new Runnable() {
@Override
public void run() {
dialog.show();
DialogManager.show(dialog);
}
}, indicator.getModalityState());
final Collection<VirtualFile> files2add = dialog.getSelectedFiles();
@@ -4,6 +4,7 @@ import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.util.text.StringUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.TestOnly;
import javax.swing.*;
import java.util.Set;
@@ -83,4 +84,9 @@ public class GithubShareDialog extends DialogWrapper {
public String getDescription() {
return myGithubSharePanel.getDescription();
}
@TestOnly
public void setRepositoryName(@NotNull String name) {
myGithubSharePanel.setRepositoryName(name);
}
}