Best Practices for Reproducible Research

Session 9

Ingmar Steiner

2017-07-11

Plugin Development

Assignment review

Another example

These slides are built from Markdown to HTML5 using Pandoc with the reveal.js presentation framework.

Of course this is managed via Gradle!

The build logic was refactored into a reusable plugin:

https://github.com/m2ci-msp/gradle-pandoc-reveal-plugin

Main task

src/main/groovy/.../PandocExec.groovy

package org.m2ci.msp.pandocreveal

import org.gradle.api.DefaultTask
import org.gradle.api.tasks.*

class PandocExec extends DefaultTask {

    @InputFile
    File markdownFile

    @OutputFile
    File htmlFile

    @TaskAction
    void compile() {
        project.exec {
            commandLine 'pandoc', '--standalone', '--smart', '--to', 'revealjs', markdownFile, '--output', htmlFile
        }
    }
}

Plugin code

src/main/groovy/.../PandocRevealPlugin.groovy

package org.m2ci.msp.pandocreveal

import org.gradle.api.*
import org.gradle.api.plugins.BasePlugin
import org.gradle.api.tasks.Copy

class PandocRevealPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        project.pluginManager.apply BasePlugin

        project.repositories {
            ivy {
                url 'https://github.com/hakimel'
                layout 'pattern', {
                    artifact '[module]/archive/[revision].[ext]'
                }
            }
        }

        project.configurations {
            revealJS
        }

        project.ext.revealJsVersion = '3.5.0'

        project.dependencies {
            revealJS group: 'se.hakimel.lab', name: 'reveal.js', version: project.revealJsVersion, ext: 'zip'
        }

        project.task('revealJS', type: Copy) {
            from project.configurations.revealJS.collect {
                project.zipTree(it)
            }
            into "$project.buildDir/reveal.js"
            eachFile {
                it.path = it.path - "reveal.js-$project.revealJsVersion/"
            }
            includeEmptyDirs = false
        }

        project.task('compileMarkdown', type: PandocExec) {
            dependsOn project.tasks.findByName('revealJS')
            project.tasks.findByName('assemble').dependsOn it
        }
    }
}

Plugin build script

build.gradle

plugins {
    id 'java-gradle-plugin'
    id 'groovy'
    id 'com.gradle.plugin-publish' version '0.9.7'
}

group 'org.m2ci.msp'
version '0.2.0-SNAPSHOT'
description 'Apply common build logic to a slideshow project using Pandoc and reveal.js'

repositories {
    jcenter()
}

dependencies {
    testCompile group: 'org.testng', name: 'testng', version: '6.9.6'
}

ext {
    pluginId = 'org.m2ci.msp.pandocreveal'
}

gradlePlugin {
    plugins {
        pandocRevealPlugin {
            id = pluginId
            implementationClass = 'org.m2ci.msp.pandocreveal.PandocRevealPlugin'
        }
    }
}

test {
    useTestNG()
    testLogging {
        exceptionFormat = 'full'
    }
}

pluginBundle {
    website = 'https://github.com/m2ci-msp/gradle-pandoc-reveal-plugin'
    vcsUrl = 'https://github.com/m2ci-msp/gradle-pandoc-reveal-plugin'
    description = project.description
    tags = ['slideshow', 'pandoc', 'reveal.js']

    plugins {
        pandocRevealPlugin {
            id = pluginId
            displayName = 'Gradle Pandoc reveal.js plugin'
        }
    }
}

Test resources

src/test/resources/.../build.gradle

plugins {
    id 'org.m2ci.msp.pandocreveal'
}

compileMarkdown{
    markdownFile = file('slides.md')
    htmlFile = file('actual.html')
}

src/test/resources/.../slides.md

# Foo

## Bar

Baz

Functional testing

src/test/groovy/.../PandocRevealPluginTest.groovy

package org.m2ci.msp.pandocreveal

import org.gradle.testkit.runner.GradleRunner
import org.testng.annotations.*

@Test
class PandocRevealPluginTest {

    GradleRunner provideGradle() {
        def projectDir = File.createTempDir()
        ['build.gradle', 'slides.md', 'expected.html'].each { resourceName ->
            new File(projectDir, resourceName).withWriter {
                it << this.class.getResourceAsStream(resourceName)
            }
        }
        GradleRunner.create().withPluginClasspath().withProjectDir(projectDir)
    }

    @Test
    void testPlugin() {
        def gradle = provideGradle()
        def result = gradle.build()
        assert result
    }

    @Test
    void testCompileMarkdown() {
        def gradle = provideGradle()
        def result = gradle.withArguments('compileMarkdown').build()
        assert result
        def actualFile = new File(gradle.projectDir, 'actual.html')
        assert actualFile.exists()
        def expectedFile = new File(gradle.projectDir, 'expected.html')
        assert expectedFile.text == actualFile.text
    }
}

Continuous Integration testing

.travis.yml

# use CI workers based on Ubuntu 14.4
dist: trusty
# use container-based workers (faster)
sudo: false

# restrict CI builds to master branch (and pull requests):
branches:
  only:
    - master

language: groovy
# test on multiple Java versions:
jdk:
  - openjdk7
  - oraclejdk7
  - oraclejdk8

# provide extra packages via apt-get install
addons:
  apt:
    packages:
    - pandoc

Plugin hosting

Next

Upcoming topics

  • Report generation
  • Connecting with R

Questions?