Skip to content
This repository has been archived by the owner on Jan 18, 2021. It is now read-only.

Commit

Permalink
Merge pull request #126 from mockito/ms
Browse files Browse the repository at this point in the history
#85 Automated e2e testing
  • Loading branch information
mockitoguy authored May 15, 2017
2 parents 203712a + e740bde commit 22063cb
Show file tree
Hide file tree
Showing 11 changed files with 336 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ gradle-app.setting
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
# gradle/wrapper/gradle-wrapper.properties

.idea
.idea
9 changes: 8 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
// First, apply the publishing plugin
buildscript {
repositories {
mavenLocal() // for e2e test
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
classpath "com.gradle.publish:plugin-publish-plugin:0.9.6"
classpath "gradle.plugin.org.mockito:mockito-release-tools:0.8.25"
classpath "gradle.plugin.org.mockito:mockito-release-tools:0.8.32"
}
}

Expand Down Expand Up @@ -72,6 +73,12 @@ pluginBundle {
displayName = 'Mockito plugin for checking if release is needed'
tags = ['mockito', 'continuous delivery', 'release']
}

e2eTestPlugin {
id = 'org.mockito.mockito-release-tools.e2e-test'
displayName = 'Mockito e2e tests plugin'
tags = ['mockito', 'continuous delivery', 'release', 'e2e', 'end-to-end', 'end-2-end', 'test']
}
}

mavenCoordinates {
Expand Down
25 changes: 25 additions & 0 deletions e2eTests/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
apply plugin: 'base' // for clean task

// It needs to be commented until first version of release-tools with E2E plugin is released.
// If not, Gradle is unable to configure whole project, because E2E plugin isn't published in any version yet.
// TODO uncomment in next PR
// apply plugin: 'org.mockito.mockito-release-tools.e2e-test'

/*
Before we can push the code, I'd like to clean it up a little bit (TODO)
4. In order for Gradle to neatly paralelize,
let's have the mockito and mockito-release-tools-example be separate subprojects,
just like I have set it up in PR #100.
This way, the integration tests will run in parallel (Gradle parallelizes per project)
5. Let's have completely separate task for cloning which will be incremental
(e.g. does not clone twice when you run build twice).
The task that performs the test will copy from the files from the clone task into some work dir and execute the test there
6. Let's use relatively shallow clone. We don't need all history.
7. I think we're missing task dependency to root's 'publishToMavenLocal'
Integration testing is a big feature of shipkit!
We will morph it into a plugin that will be part of the release tools plugins.
Take your time engineering it and learn as much Gradle as possible during the process :)
*/
4 changes: 3 additions & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
rootProject.name="mockito-release-tools"
rootProject.name="mockito-release-tools"

include "e2eTests"
39 changes: 36 additions & 3 deletions src/main/groovy/org/mockito/release/exec/DefaultProcessRunner.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.gradle.api.GradleException;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.mockito.release.notes.util.IOUtil;
import org.mockito.release.notes.util.ReleaseNotesException;

import java.io.BufferedReader;
Expand All @@ -21,10 +22,24 @@ public class DefaultProcessRunner implements ProcessRunner {

private static final Logger LOG = Logging.getLogger(DefaultProcessRunner.class);
private final File workDir;
private final File outputLogFile;
private String secretValue;

/**
* Calls {@link #DefaultProcessRunner(File, File)}
*/
public DefaultProcessRunner(File workDir) {
this(workDir, null);
}

/**
* Create Process runner
* @param workDir Work directory where to start a process
* @param outputLogFile If process create a long output it's better to save it in file
*/
public DefaultProcessRunner(File workDir, File outputLogFile) {
this.workDir = workDir;
this.outputLogFile = outputLogFile;
}

public String run(String... commandLine) {
Expand All @@ -43,9 +58,7 @@ String run(Logger log, List<String> commandLine) {
ProcessResult result = executeProcess(commandLine, maskedCommandLine);

if (result.getExitValue() != 0) {
throw new GradleException("Execution of command failed (exit code " + result.getExitValue() + "):\n" +
" " + maskedCommandLine + "\n" +
"Captured command output:\n" + result.getOutput());
return executionOfCommandFailed(maskedCommandLine, result);
} else {
return result.getOutput();
}
Expand All @@ -56,6 +69,7 @@ private ProcessResult executeProcess(List<String> commandLine, String maskedComm
try {
Process process = new ProcessBuilder(commandLine).directory(workDir).redirectErrorStream(true).start();
String output = mask(readFully(new BufferedReader(new InputStreamReader(process.getInputStream()))));
storeOutputToFile(output);

//TODO add sanity timeout when we move to Java 1.7
// 1. we can do something like process.waitFor(15, TimeUnit.MINUTES)
Expand Down Expand Up @@ -89,6 +103,25 @@ private static String readFully(BufferedReader reader) throws IOException {
}
}

private void storeOutputToFile(String content) {
if(outputLogFile != null) {
//TODO ms - can we make sure that the output does not have sensitive secret values
//should we mask secret values in the output stored in file, too?
IOUtil.writeFile(outputLogFile, content);
}
}

private String executionOfCommandFailed(String maskedCommandLine, ProcessResult result) {
String message = "Execution of command failed (exit code " + result.getExitValue() + "):\n" +
" " + maskedCommandLine + "\n";
if(outputLogFile == null) {
message = message + " Captured command output:\n" + result.getOutput();
} else {
message = message + " Captured command output stored in " + outputLogFile;
}
throw new GradleException(message);
}

/**
* @param secretValue to be masked from the output and logging
* @return this runner
Expand Down
7 changes: 7 additions & 0 deletions src/main/groovy/org/mockito/release/exec/Exec.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,11 @@ public class Exec {
public static ProcessRunner getProcessRunner(File workDir) {
return new DefaultProcessRunner(workDir);
}

/**
* Provides process runner for given working dir
*/
public static ProcessRunner getProcessRunner(File workDir, File outputLogFile) {
return new DefaultProcessRunner(workDir, outputLogFile);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package org.mockito.release.internal.gradle;

import org.gradle.api.DefaultTask;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.TaskAction;
import org.mockito.release.exec.ProcessRunner;

import java.io.File;

/**
* This task clone git project from repository to target dir.
* It support clone from remote server and from local filesystem.
*
* TODO ms - when you are ready, please move the new task types to the public packages,
* for example "org.mockito.release.*". With 1.0 we need all task types to be public.
* It's because users interface with task types when they work with Gradle build scripts.
* So it makes sense to be explicit that those types are public and we guarantee compatibility.
* See also README.md on the compatibility where I attempted to describe this ;)
*/
public class CloneGitRepositoryTask extends DefaultTask {

private static final Logger LOG = Logging.getLogger(CloneGitRepositoryTask.class);

private String repository;
private File targetDir;

@TaskAction
public void cloneRepository() {
LOG.lifecycle(" Cloning repository {}\n into {}", repository, targetDir);
getProject().getBuildDir().mkdirs(); // build dir can be not created yet
ProcessRunner processRunner = org.mockito.release.exec.Exec.getProcessRunner(getProject().getBuildDir());
processRunner.run("git", "clone", repository, targetDir.getAbsolutePath());
}

//TODO ms - let's put javadoc on all public methods of the task
// No need to put it on "cloneRepository" method because it is not intended to be used by end users.
// It's nice if javadoc for 'repository' demonstrates an example value
// When reading the API by looking at method signature
// I don't know if repository should be a name of repo or valid url to the repo
@Input
public void setRepository(String repository) {
this.repository = repository;
}

public String getRepository() {
return repository;
}

@OutputDirectory
public void setTargetDir(File targetDir) {
this.targetDir = targetDir;
}

public File getTargetDir() {
return targetDir;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package org.mockito.release.internal.gradle;

import org.gradle.api.Plugin;
import org.gradle.api.Project;

import java.io.File;

import static java.util.Arrays.asList;
import static org.mockito.release.internal.gradle.util.StringUtil.capitalize;

/**
* This plugin tests your library end-to-end (e2e) using client projects.
* Plugin clones client projects to '$buildDir/project-name-pristine' first, next clone project from 'pristine' to
* '$buildDir/project-name-work' and execute 'testRelease' task using the newest mockito-release-tools version
*
* Adds tasks:
* <ul>
* <li>cloneProjectFromGitHub$projectName - {@link CloneGitRepositoryTask}</li>
* <li>cloneProjectToWorkDir$projectName - {@link CloneGitRepositoryTask}</li>
* <li>runTest$projectName - {@link RunTestReleaseTask}</li>
* </ul>
*/
public class E2ETestingPlugin implements Plugin<Project> {

public void apply(final Project project) {
E2ETest e2eTest = project.getExtensions().create("e2eTest", E2ETest.class, project);
// TODO hardcoded for now
e2eTest.create("https://github.com/mockito/mockito-release-tools-example");
}

//TODO ms - closer to the finish line we need to make this type public in one of the public packages
//this is how users will interface with configuring e2e tests
public static class E2ETest {

Project project;

public E2ETest(Project project) {
this.project = project;
}

void create(String gitHubRepoUrl) {
String repoName = extractRepoName(gitHubRepoUrl);
CloneGitRepositoryTask clone = createCloneProjectFromGitHub(gitHubRepoUrl, repoName);
CloneGitRepositoryTask workDirCloneTask = createCloneProjectToWorkDirTask(repoName, clone);
createRunTestReleaseTask(repoName, workDirCloneTask);
}

private CloneGitRepositoryTask createCloneProjectFromGitHub(String gitHubRepoUrl, String repoName) {
// TODO add depth clone configuration for shallow clone
CloneGitRepositoryTask clone = project.getTasks().create(
"cloneProjectFromGitHub" + capitalize(repoName),
CloneGitRepositoryTask.class);
clone.setRepository(gitHubRepoUrl);
clone.setTargetDir(new File(project.getBuildDir(), repoName + "-pristine"));
// For now for easier testing
clone.dependsOn("clean");
return clone;
}

private CloneGitRepositoryTask createCloneProjectToWorkDirTask(String repoName, CloneGitRepositoryTask clone) {
// Clone from *-pristine to *-work. Copy task will not work because of ignoring git specific files:
// https://discuss.gradle.org/t/copy-git-specific-files/11970
// Furthermore we can verify push to pristine origin
File workDir = new File(project.getBuildDir(), repoName + "-work");
CloneGitRepositoryTask copy = project.getTasks().create(
"cloneProjectToWorkDir" + capitalize(repoName),
CloneGitRepositoryTask.class);
copy.dependsOn(clone);
copy.setRepository(clone.getTargetDir().getAbsolutePath());
copy.setTargetDir(workDir);
return copy;
}

private void createRunTestReleaseTask(String repoName, CloneGitRepositoryTask copy) {
RunTestReleaseTask run = project.getTasks().create(
"runTestRelease" + capitalize(repoName),
RunTestReleaseTask.class);
run.dependsOn(copy);
run.setWorkDir(copy.getTargetDir());
run.setRepoName(repoName);

// Using Gradle's composite builds ("--include-build") so that we're picking up current version of tools
run.setCommand(asList("./gradlew", "publishToMavenLocal", "testRelease",
"-x", "gitPush", "-x", "bintrayUpload",
"--include-build", project.getRootDir().getAbsolutePath(), "-s"));

// Build log in separate file instead of including it in the console of the parent build
// Otherwise the output will be really messy
run.setBuildOutputFile(new File(project.getBuildDir(), repoName + "-build.log"));
}

private String extractRepoName(String gitHubRepo) {
String text = gitHubRepo.trim();
if(text.lastIndexOf('/') == text.length() - 1) {
// cut last slash
text = text.substring(0, text.length() - 1);
}
return text.substring(text.lastIndexOf('/') + 1, text.length());
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.mockito.release.internal.gradle;

import org.gradle.api.DefaultTask;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import org.mockito.release.exec.ProcessRunner;

import java.io.File;
import java.util.List;

/**
* This task run external process and additionally store output of external process to file.
*/
public class RunTestReleaseTask extends DefaultTask {

private static final Logger LOG = Logging.getLogger(RunTestReleaseTask.class);

private List<String> command;
private File buildOutput;
private File workDir;
private String repoName;

@TaskAction
public void runTest() {
LOG.lifecycle(" Run test of {}. The output will be save in {}", repoName, buildOutput.getAbsoluteFile());
ProcessRunner processRunner = org.mockito.release.exec.Exec.getProcessRunner(workDir, buildOutput);
processRunner.run(command);
}

@Input
public void setWorkDir(File workDir) {
this.workDir = workDir;
}

@Input
public void setCommand(List<String> command) {
this.command = command;
}

@Input
public void setRepoName(String repoName) {
this.repoName = repoName;
}

@OutputFile
public void setBuildOutputFile(File file) {
buildOutput = file;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
implementation-class=org.mockito.release.internal.gradle.E2ETestingPlugin
Loading

0 comments on commit 22063cb

Please sign in to comment.