donderdag 27 februari 2014

Robolectric plugin - Fix the plugin-flaw on a Windows Environment

In the previous blogpost we worked with the Robolectric plugin (developed by Novoda.com) for starting our Robolectric tests from Gradle. This neat little plugin unfortunealty has a flaw which prohibits it from working correctly if you are developing on a Windows environment (or you switch to Mac or Linux, just an idea...).

The problem with the current plugin is that it does not escape the File.Seperator correctly, and although a merge-request is pending on their Git repository, they haven't fixed this to a new stable or snapshot release.

The code of the current (0.0.1 - SNAPSHOT) currently states:
     def buildDir = dir.getAbsolutePath().split(File.separator)
Where it should be
      def buildDir = dir.getAbsolutePath().split(Pattern.quote(File.separator))

Fortuneatly enough, the source code is publicly available as a Groovy based plugin. For ease-of-use (and my lack of understanding on how to compile a plugin :) ), I've posted my Gradle configuration below.

Next to having your normal buildscript and android configuration, you will need to :
  • Import two additional classes, being Callable and Pattern
  • Define the plugin: robolectric
  • Paste-in the code that starts with "class robolectric implements Plugin


Note
With the release of Android Studio 0.5.0, some upgrading of your Gradle build file is required (using the Gradle plugin version 0.9.+). In the dependencies change the instrumentTestCompile command to androidTestCompile. If you'll have exceptions during the Project Gradle Sync, make sure to restart your IDE and invalidate the caches (File > Invalidate Caches/Restart). I've update the build file in the provided examples.

The code

//--- import additional classes ---
import java.util.concurrent.Callable
import java.util.regex.Pattern
//--- end import ---

buildscript {
    repositories {
        mavenCentral()
        maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:0.9.+'
        classpath 'com.novoda.gradle:robolectric-plugin:0.0.1-SNAPSHOT'
    }
}

apply plugin: 'android'
//--- use the new plugin robolectric, which is the classname of the plugin
apply plugin: robolectric

android {
    compileSdkVersion 19
    buildToolsVersion '19.0.1'

    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 19
        versionCode 1
        versionName "1.0"
    }

    sourceSets {
        //the tests are still stored in /src/test
        instrumentTest.setRoot('src/test')
    }

    buildTypes {
    }
}


dependencies {
    compile 'com.android.support:appcompat-v7:+'
    
    androidTestCompile 'org.robolectric:robolectric:2.+'
    androidTestCompile 'junit:junit:4.+'
}

//ignore classes that do not have the extension Test in their classname
tasks.withType(Test) {
    scanForTestClasses = false
    include "**/*Test.class"
}
/**
 * -----------------------------------------------------------
 * ROBOLECTRIC PLUGIN CLAS
 * -----------------------------------------------------------
 */
class robolectric implements Plugin<Project> {

    public static final String ANDROID_PLUGIN_NAME = "android";
    public static final String ANDROID_LIBRARY_PLUGIN_NAME = "android-library";

    public static final String COMPILE_CONFIGURATION_NAME = "compile";
    public static final String TEST_COMPILE_CONFIGURATION_NAME = "robolectricTestCompile";
    public static final String RUNTIME_CONFIGURATION_NAME = "runtime";
    public static final String TEST_RUNTIME_CONFIGURATION_NAME = "robolectricTestRuntime";

    public static final String ROBOLECTRIC_SOURCE_SET_NAME = "robolectric";
    public static final String ROBOLECTRIC_CONFIGURATION_NAME = "robolectric";
    public static final String ROBOLECTRIC_TASK_NAME = "robolectric";

    void apply(Project project) {
        project.getPlugins().apply(JavaBasePlugin.class);
        ensureValidProject(project);

        JavaPluginConvention javaConvention = project.getConvention().getPlugin(JavaPluginConvention.class);
        configureConfigurations(project);
        configureSourceSets(javaConvention);

        configureTest(project, javaConvention);

        project.afterEvaluate {
            configureAndroidDependency(project, javaConvention)
        }
    }

    def configureAndroidDependency(Project project, JavaPluginConvention pluginConvention) {
        SourceSet robolectric = pluginConvention.getSourceSets().findByName(ROBOLECTRIC_SOURCE_SET_NAME);

        (getAndroidPlugin(project)).mainSourceSet.java.srcDirs.each { dir ->
            //Fault: def buildDir = dir.getAbsolutePath().split(File.separator)
            def buildDir = dir.getAbsolutePath().split(Pattern.quote(File.separator))
            buildDir = (buildDir[0..(buildDir.length - 4)] + ['build', 'classes', 'debug']).join(File.separator)
            robolectric.compileClasspath += project.files(buildDir)
            robolectric.runtimeClasspath += project.files(buildDir)
        }

        getAndroidPlugin(project).variantDataList.each {
            it.variantDependency.getJarDependencies().each {
                robolectric.compileClasspath += project.files(it.jarFile)
                robolectric.runtimeClasspath += project.files(it.jarFile)
            }
        }

        // AAR files
        getAndroidPlugin(project).prepareTaskMap.each {
            robolectric.compileClasspath += project.fileTree(dir: it.value.explodedDir, include: '*.jar')
            robolectric.runtimeClasspath += project.fileTree(dir: it.value.explodedDir, include: '*.jar')
        }

        // Default Android jar
        getAndroidPlugin(project).getRuntimeJarList().each {
            robolectric.compileClasspath += project.files(it)
            robolectric.runtimeClasspath += project.files(it)
        }

        robolectric.runtimeClasspath = robolectric.runtimeClasspath.filter {
            it
            true
        }
    }

    private void ensureValidProject(Project project) {
        boolean isAndroidProject = project.getPlugins().hasPlugin(ANDROID_PLUGIN_NAME);
        boolean isAndroidLibProject = project.getPlugins().hasPlugin(ANDROID_LIBRARY_PLUGIN_NAME);
        if (!(isAndroidLibProject | isAndroidProject)) {
            throw new RuntimeException("Not a valid Android project");
        }
    }

    void configureConfigurations(Project project) {
        ConfigurationContainer configurations = project.getConfigurations();
        Configuration compileConfiguration = configurations.getByName(COMPILE_CONFIGURATION_NAME);
        Configuration robolectric = configurations.create(ROBOLECTRIC_CONFIGURATION_NAME);
        robolectric.extendsFrom(compileConfiguration);
    }

    private void configureSourceSets(final JavaPluginConvention pluginConvention) {
        final Project project = pluginConvention.getProject();

        SourceSet robolectric = pluginConvention.getSourceSets().create(ROBOLECTRIC_SOURCE_SET_NAME);

        robolectric.java.srcDir project.file('src/test/java')
        robolectric.compileClasspath += project.configurations.robolectric
        robolectric.runtimeClasspath += robolectric.compileClasspath
    }

    private void configureTest(final Project project, final JavaPluginConvention pluginConvention) {
        project.getTasks().withType(Test.class, new Action<Test>() {
            public void execute(final Test test) {
                test.workingDir 'src/main'
                test.getConventionMapping().map("testClassesDir", new Callable<Object>() {
                    public Object call() throws Exception {
                        return pluginConvention.getSourceSets().getByName("robolectric").getOutput().getClassesDir();
                    }
                });

                test.getConventionMapping().map("classpath", new Callable<Object>() {
                    public Object call() throws Exception {
                        return pluginConvention.getSourceSets().getByName("robolectric").getRuntimeClasspath();
                    }
                });

                test.getConventionMapping().map("testSrcDirs", new Callable<Object>() {
                    public Object call() throws Exception {
                        return new ArrayList<File>(pluginConvention.getSourceSets().getByName("robolectric").getJava().getSrcDirs());
                    }
                });
            }
        });

        Test test = project.getTasks().create(ROBOLECTRIC_TASK_NAME, Test.class);
        project.getTasks().getByName(JavaBasePlugin.CHECK_TASK_NAME).dependsOn(test);
        test.setDescription("Runs the unit tests using robolectric.");
        test.setGroup(JavaBasePlugin.VERIFICATION_GROUP);

        test.dependsOn(project.getTasks().findByName('robolectricClasses'))
        test.dependsOn(project.getTasks().findByName('assemble'))
    }

    private Plugin getAndroidPlugin(Project project) {
        if (project.getPlugins().hasPlugin(ANDROID_LIBRARY_PLUGIN_NAME)) {
            return  project.getPlugins().findPlugin(ANDROID_LIBRARY_PLUGIN_NAME);
        }
        return project.getPlugins().findPlugin(ANDROID_PLUGIN_NAME);
    }

}
//--- END CLASS DEFINITION --

Geen opmerkingen:

Een reactie plaatsen