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

Initial design for Build A/B testing epic story #114

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.mockito.release.internal.comparison.file;

import org.json.simple.Jsonable;
import org.json.simple.Jsoner;

import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;

/**
* Holds the compare result of two directories. The result consists of files which are only available in dirA, files
* which are only available in dirB and files which are available in both directories and their content is different.
*/
public class CompareResult implements Jsonable {

private static final String JSON_FORMAT = "{ \"onlyA\": %s, \"onlyB\": %s, " +
"\"both\": %s }";

private List<File> onlyA;
private List<File> onlyB;
private List<File> bothButDifferent;

public void setOnlyA(List<File> file) {
this.onlyA = file;
}

public void setOnlyB(List<File> file) {
this.onlyB = file;
}

public void setBothButDifferent(List<File> file) {
this.bothButDifferent = file;
}

@Override
public String toJson() {
return String.format(JSON_FORMAT,
Jsoner.serialize(toStringList(onlyA)),
Jsoner.serialize(toStringList(onlyB)),
Jsoner.serialize(toStringList(bothButDifferent)));
}

private List<String> toStringList(List<File> files) {
List<String> ret = new ArrayList<String>(files.size());
for (File file : files) {
ret.add(file.getPath());
}
return ret;
}

@Override
public void toJson(Writer writable) throws IOException {
writable.append(toJson());
}

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

import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.json.simple.DeserializationException;
import org.json.simple.JsonObject;
import org.json.simple.Jsoner;

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

/**
* Json serializer for {@link CompareResult}s.
*/
public class CompareResultSerializer {

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

public String serialize(CompareResult compareResult) {
String json = Jsoner.serialize(compareResult);
LOG.info("Serialize compare result to: {}", json);
return json;
}

public CompareResult deserialize(String json) {
CompareResult compareResult = new CompareResult();
try {
LOG.info("Deserialize compare result from: {}", json);
JsonObject jsonObject = (JsonObject) Jsoner.deserialize(json);
Collection<String> onlyA = jsonObject.getCollection("onlyA");
Collection<String> onlyB = jsonObject.getCollection("onlyB");
Collection<String> both = jsonObject.getCollection("both");
compareResult.setOnlyA(toFileList(onlyA));
compareResult.setOnlyB(toFileList(onlyB));
compareResult.setBothButDifferent(toFileList(both));
} catch (DeserializationException e) {
throw new RuntimeException("Can't deserialize JSON: " + json, e);
}
return compareResult;
}

private List<File> toFileList(Collection<String> onlyA) {
List<File> fileList = new ArrayList<File>(onlyA.size());
for (String filePath : onlyA) {
fileList.add(new File(filePath));
}
return fileList;
}

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

import org.mockito.release.internal.util.Md5;

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


public class FileDifferenceProvider {

public CompareResult getDifference(File dirA, File dirB) {
List<File> dirAFiles = getFilesResursive(dirA.listFiles());
Collections.sort(dirAFiles);
List<File> dirBFiles = getFilesResursive(dirB.listFiles());
Collections.sort(dirBFiles);

List<File> onlyA = new ArrayList<File>();
List<File> onlyB = new ArrayList<File>();
List<File> bothButDifferent = new ArrayList<File>();

int i = 0;
int j = 0;

while (dirAFiles.size() > i && dirBFiles.size() > j) {
String dirASubPath = dirAFiles.get(i).getPath().substring(dirA.getPath().length());
String dirBSubPath = dirBFiles.get(j).getPath().substring(dirB.getPath().length());
int compareResult = dirASubPath.compareTo(dirBSubPath);

if (compareResult < 0) {
onlyA.add(dirAFiles.get(i));
i++;
} else if (compareResult > 0) {
onlyB.add(dirBFiles.get(j));
j++;
} else {
String md5A = Md5.calculate(dirAFiles.get(i));
String md5B = Md5.calculate(dirBFiles.get(j));
boolean sameMd5 = md5A.equals(md5B);

if (dirAFiles.get(i).length() == dirBFiles.get(j).length() && sameMd5) {
// nothing to do, both files are available
} else {
bothButDifferent.add(dirAFiles.get(i));
bothButDifferent.add(dirBFiles.get(j));
}
i++;
j++;
}
}

if (dirAFiles.size() == i && dirBFiles.size() > j) {
while (j < dirBFiles.size()) {
onlyB.add(dirBFiles.get(j));
j++;
}
}

if (dirBFiles.size() == j && dirAFiles.size() > i) {
while (i < dirAFiles.size()) {
onlyA.add(dirAFiles.get(i));
i++;
}
}

CompareResult result = new CompareResult();
result.setOnlyA(onlyA);
result.setOnlyB(onlyB);
result.setBothButDifferent(bothButDifferent);

return result;
}


private List<File> getFilesResursive(File[] files) {
List<File> filesRecursive = new ArrayList<File>();
for (File file : files) {
filesRecursive.add(file);
if (file.isDirectory()) {
filesRecursive.addAll(getFilesResursive(file.listFiles()));
}
}
return filesRecursive;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package org.mockito.release.internal.gradle;

import org.gradle.api.Action;
import org.gradle.api.DefaultTask;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.file.CopySpec;
import org.gradle.api.tasks.*;
import org.mockito.release.exec.DefaultProcessRunner;

import java.io.File;

/**
* See rationale and design at: https://github.com/mockito/mockito-release-tools/issues/113
*/
public class BuildABTestingPlugin implements Plugin<Project> {

public void apply(final Project project) {
//Step 1. Clone the repo for A/B testing
CloneGitRepositoryTask clone = project.getTasks().create("cloneGitRepository", CloneGitRepositoryTask.class);
clone.setRepository("https://github.com/mockito/mockito");
clone.setTargetDir(new File(project.getBuildDir(), "abTesting/mockito/pristine"));

//Step 2. Run A test
RunTestTask runA = project.getTasks().create("runA", RunTestTask.class);
runA.dependsOn(clone);
runA.setSourceDir(clone.getTargetDir());
runA.setWorkDir(new File(project.getBuildDir(), "abTesting/mockito/testA-" + System.currentTimeMillis()));
// using assemble task for now in order to get different results
runA.commandLine("./gradlew", "assemble");
runA.setOutputDir(new File(runA.getWorkDir(), "build"));

//Step 3. Run B test
RunTestTask runB = project.getTasks().create("runB", RunTestTask.class);
runB.dependsOn(clone);
runB.setSourceDir(clone.getTargetDir());
runB.setWorkDir(new File(project.getBuildDir(), "abTesting/mockito/testB-" + System.currentTimeMillis()));
runB.commandLine("./gradlew", "build");
runB.setOutputDir(new File(runB.getWorkDir(), "build"));

//Step 4. Compare test outcomes
CompareDirectoriesTask compare = project.getTasks().create("compareAB", CompareDirectoriesTask.class);
compare.dependsOn(runA, runB);
compare.setDirA(runA.getOutputDir());
compare.setDirB(runB.getOutputDir());
compare.setResultsFile(new File(project.getBuildDir(), "abTesting/mockito/results.json")); //or JSON :)

//Step 5. Analyze comparison results
AnalyzeComparisonResultsTask analyze = project.getTasks().create("analyzeAB", AnalyzeComparisonResultsTask.class);
analyze.dependsOn(compare);
analyze.setComparisonResultsFile(compare.getResultsFile());
}

/**
* BELOW task types are only empty shells.
* They act as suggestions for workflow / API design.
*/

public static class AnalyzeComparisonResultsTask extends DefaultTask {

private File comparisonResultsFile;

@InputFile
public void setComparisonResultsFile(File comparisonResultsFile) {
this.comparisonResultsFile = comparisonResultsFile;
}

@TaskAction
public void analyze() {

}
}

/**
* Gradle task to run a given command line.
* Note: not yet migrated to RunTestReleaseTask because RunTestReleaseTask requires two separate checkout tasks
* which might take some time (e.g. if we are using a repo like mockito and the internet connection is not that fast.
*/
public static class RunTestTask extends DefaultTask {

private File sourceDir;
private File workDir;
private File outputDir;
private String[] arg;

@InputDirectory
public void setSourceDir(File sourceDir) {
this.sourceDir = sourceDir;
}

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

@Input
public void commandLine(String ... arg) {
this.arg = arg;
}

public File getWorkDir() {
return workDir;
}

public void setOutputDir(File outputDir) {
this.outputDir = outputDir;
}

@OutputDirectory
public File getOutputDir() {
return outputDir;
}

@TaskAction
public void executeTestTask() {
if (sourceDir == null || !sourceDir.exists()) {
throw new RuntimeException("Invalid source dir '" + sourceDir + "' given!" );
}
if (outputDir != null && !outputDir.exists()) {
outputDir.mkdirs();
}
getProject().copy(new Action<CopySpec>() {
public void execute(CopySpec copy) {
copy.from(sourceDir).into(workDir);
}
});
new DefaultProcessRunner(workDir).run(arg);
}
}

public static class CompareABTask extends DefaultTask {
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,16 @@ public class CloneGitRepositoryTask extends DefaultTask {

@TaskAction
public void cloneRepository() {
if (repository == null || repository.isEmpty()) {
throw new RuntimeException("Invalid repository '" + repository + "' given!");
}

LOG.lifecycle(" Cloning repository {}\n into {}", repository, targetDir);

if (targetDir.exists()) {
getProject().delete(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());
Expand Down
Loading