diff --git a/src/main/groovy/org/jenkinsci/gradle/plugins/jpi/JpiPlugin.groovy b/src/main/groovy/org/jenkinsci/gradle/plugins/jpi/JpiPlugin.groovy index b5613dc4..754b291a 100644 --- a/src/main/groovy/org/jenkinsci/gradle/plugins/jpi/JpiPlugin.groovy +++ b/src/main/groovy/org/jenkinsci/gradle/plugins/jpi/JpiPlugin.groovy @@ -31,11 +31,11 @@ import org.gradle.api.attributes.java.TargetJvmVersion import org.gradle.api.component.AdhocComponentWithVariants import org.gradle.api.execution.TaskExecutionGraph import org.gradle.api.plugins.BasePlugin +import org.gradle.api.plugins.GroovyPlugin import org.gradle.api.plugins.JavaLibraryPlugin import org.gradle.api.plugins.JavaPlugin import org.gradle.api.plugins.JavaPluginConvention import org.gradle.api.plugins.JavaPluginExtension -import org.gradle.api.plugins.GroovyPlugin import org.gradle.api.publish.PublishingExtension import org.gradle.api.publish.maven.MavenPublication import org.gradle.api.publish.maven.plugins.MavenPublishPlugin @@ -48,6 +48,9 @@ import org.gradle.api.tasks.compile.JavaCompile import org.gradle.api.tasks.testing.Test import org.gradle.language.base.plugins.LifecycleBasePlugin import org.gradle.util.GradleVersion +import org.jenkinsci.gradle.plugins.jpi.server.GenerateJenkinsServerHplTask + +import org.jenkinsci.gradle.plugins.jpi.server.InstallJenkinsServerPluginsTask import static org.gradle.api.logging.LogLevel.INFO import static org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME @@ -94,10 +97,28 @@ class JpiPlugin implements Plugin { def ext = gradleProject.extensions.create('jenkinsPlugin', JpiExtension, gradleProject) + def generateHpl = gradleProject.tasks.register(GenerateJenkinsServerHplTask.TASK_NAME, + GenerateJenkinsServerHplTask) { GenerateJenkinsServerHplTask t -> + t.fileName.set(ext.shortName) + t.description = 'Generate hpl (Hudson plugin link) for running locally' + t.group = 'Jenkins Server' + } + + def installPlugins = gradleProject.tasks.register(InstallJenkinsServerPluginsTask.TASK_NAME, + InstallJenkinsServerPluginsTask) { + it.group = 'Jenkins Server' + it.description = 'Install plugins to the server\'s Jenkins Home directory' + it.jenkinsHome.set(ext.workDir) + def serverRuntime = project.configurations.getByName(SERVER_JENKINS_RUNTIME_CLASSPATH_CONFIGURATION_NAME) + it.pluginsConfiguration.set(serverRuntime) + it.hpl.set(generateHpl.flatMap { it.hpl }) + it.dependsOn(generateHpl) + } + gradleProject.tasks.register(ServerTask.TASK_NAME, ServerTask) { it.description = 'Run Jenkins in place with the plugin being developed' it.group = BasePlugin.BUILD_GROUP // TODO - it.dependsOn(ext.mainSourceTree().runtimeClasspath) + it.dependsOn(ext.mainSourceTree().runtimeClasspath, generateHpl, installPlugins) } // set build directory for Jenkins test harness, JENKINS-26331 diff --git a/src/main/groovy/org/jenkinsci/gradle/plugins/jpi/ServerTask.groovy b/src/main/groovy/org/jenkinsci/gradle/plugins/jpi/ServerTask.groovy index e42adf91..9a5f44f0 100644 --- a/src/main/groovy/org/jenkinsci/gradle/plugins/jpi/ServerTask.groovy +++ b/src/main/groovy/org/jenkinsci/gradle/plugins/jpi/ServerTask.groovy @@ -15,13 +15,11 @@ */ package org.jenkinsci.gradle.plugins.jpi -import java.util.jar.JarFile import org.gradle.api.DefaultTask import org.gradle.api.GradleException import org.gradle.api.tasks.TaskAction -import org.gradle.util.GFileUtils -import static JpiPlugin.SERVER_JENKINS_RUNTIME_CLASSPATH_CONFIGURATION_NAME +import java.util.jar.JarFile /** * Task that starts Jenkins in place with the current plugin. @@ -46,9 +44,6 @@ class ServerTask extends DefaultTask { } File war = files.first() - generateHpl() - copyPluginDependencies() - def conv = project.extensions.getByType(JpiExtension) System.setProperty('JENKINS_HOME', conv.workDir.absolutePath) setSystemPropertyIfEmpty('stapler.trace', 'true') @@ -70,26 +65,6 @@ class ServerTask extends DefaultTask { Thread.currentThread().join() } - void generateHpl() { - def m = new JpiHplManifest(project) - def conv = project.extensions.getByType(JpiExtension) - - def hpl = new File(conv.workDir, "plugins/${conv.shortName}.hpl") - hpl.parentFile.mkdirs() - hpl.withOutputStream { m.write(it) } - } - - private copyPluginDependencies() { - def artifacts = project.configurations[SERVER_JENKINS_RUNTIME_CLASSPATH_CONFIGURATION_NAME]. - resolvedConfiguration.resolvedArtifacts - - // copy the resolved HPI/JPI files to the plugins directory - def workDir = project.extensions.getByType(JpiExtension).workDir - artifacts.findAll { it.extension in ['hpi', 'jpi'] }.each { - GFileUtils.copyFile(it.file, new File(workDir, "plugins/${it.name}.${it.extension}")) - } - } - private static void setSystemPropertyIfEmpty(String name, String value) { if (!System.getProperty(name)) { System.setProperty(name, value) diff --git a/src/main/groovy/org/jenkinsci/gradle/plugins/jpi/server/GenerateJenkinsServerHplTask.groovy b/src/main/groovy/org/jenkinsci/gradle/plugins/jpi/server/GenerateJenkinsServerHplTask.groovy new file mode 100644 index 00000000..b1534c2e --- /dev/null +++ b/src/main/groovy/org/jenkinsci/gradle/plugins/jpi/server/GenerateJenkinsServerHplTask.groovy @@ -0,0 +1,32 @@ +package org.jenkinsci.gradle.plugins.jpi.server + +import groovy.transform.CompileStatic +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.jenkinsci.gradle.plugins.jpi.JpiHplManifest + +@CompileStatic +class GenerateJenkinsServerHplTask extends DefaultTask { + static final String TASK_NAME = 'generateJenkinsServerHpl' + @Input + final Property fileName = project.objects.property(String) + + @OutputFile + final Provider hpl = fileName.flatMap { + project.layout.buildDirectory.file("hpl/${it}.hpl") + } + + @TaskAction + void generate() { + def destination = hpl.get().asFile + destination.parentFile.mkdirs() + destination.withOutputStream { + new JpiHplManifest(project).write(it) + } + } +} diff --git a/src/main/groovy/org/jenkinsci/gradle/plugins/jpi/server/InstallJenkinsServerPluginsTask.groovy b/src/main/groovy/org/jenkinsci/gradle/plugins/jpi/server/InstallJenkinsServerPluginsTask.groovy new file mode 100644 index 00000000..6c58459a --- /dev/null +++ b/src/main/groovy/org/jenkinsci/gradle/plugins/jpi/server/InstallJenkinsServerPluginsTask.groovy @@ -0,0 +1,60 @@ +package org.jenkinsci.gradle.plugins.jpi.server + +import org.gradle.api.DefaultTask +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.ResolvedArtifact +import org.gradle.api.file.Directory +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputDirectory + +class InstallJenkinsServerPluginsTask extends DefaultTask { + static final String TASK_NAME = 'installJenkinsServerPlugins' + + @Classpath + final Property pluginsConfiguration = project.objects.property(Configuration) + + @InputFile + final RegularFileProperty hpl = project.objects.fileProperty() + + @Input + final Property jenkinsHome = project.objects.property(File) + + @Internal + final Provider> lookup = pluginsConfiguration.map { + it.resolvedConfiguration + .resolvedArtifacts + .findAll { ['hpi', 'jpi'].contains(it.extension) } + .collectEntries { [(it.file.name): withoutVersion(it)] } as Map + } + + @OutputDirectory + final Provider pluginsDir = jenkinsHome.map { + project.layout.projectDirectory.dir("${it.path}/plugins") + } + + InstallJenkinsServerPluginsTask() { + doLast { + def withoutVersion = lookup.get() + project.sync { + into(pluginsDir) + from(pluginsConfiguration) { + include('*.hpi', '*.jpi') + rename { + withoutVersion[it as String] + } + } + from(hpl) + } + } + } + + private static String withoutVersion(ResolvedArtifact artifact) { + artifact.name + '.' + artifact.extension + } +} diff --git a/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/JpiHplManifestSpec.groovy b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/JpiHplManifestSpec.groovy deleted file mode 100644 index 529ec3b3..00000000 --- a/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/JpiHplManifestSpec.groovy +++ /dev/null @@ -1,52 +0,0 @@ -package org.jenkinsci.gradle.plugins.jpi - -import org.gradle.api.Project -import org.gradle.testfixtures.ProjectBuilder -import spock.lang.Specification - -import java.util.jar.Manifest - -class JpiHplManifestSpec extends Specification { - Project project = ProjectBuilder.builder().build() - - def 'basics'() { - setup: - project.with { - apply plugin: 'jpi' - dependencies.with { - implementation('org.apache.commons:commons-lang3:3.9') - } - evaluate() // trigger 'afterEvaluate { }' configurations - } - def libraries = [ - new File(project.rootDir, 'src/main/resources'), - new File(project.buildDir, 'classes/java/main'), - new File(project.buildDir, 'resources/main'), - ] - libraries*.mkdirs() - libraries += new File(project.gradle.gradleUserHomeDir, - 'caches/modules-2/files-2.1/org.apache.commons/commons-lang3/3.9/' + - '122c7cee69b53ed4a7681c03d4ee4c0e2765da5/commons-lang3-3.9.jar') - - when: - Manifest manifest = new JpiHplManifest(project) - - then: - manifest.mainAttributes.getValue('Resource-Path') == new File(project.projectDir, 'src/main/webapp').path - manifest.mainAttributes.getValue('Libraries') == libraries*.path.join(',') - } - - def 'non-existing libraries are ignored'() { - setup: - project.with { - apply plugin: 'jpi' - } - - when: - JpiHplManifest manifest = new JpiHplManifest(project) - - then: - manifest.mainAttributes.getValue('Resource-Path') == new File(project.projectDir, 'src/main/webapp').path - manifest.mainAttributes.getValue('Libraries') == '' - } -} diff --git a/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/server/GenerateJenkinsServerHplTaskSpec.groovy b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/server/GenerateJenkinsServerHplTaskSpec.groovy new file mode 100644 index 00000000..5b956f5c --- /dev/null +++ b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/server/GenerateJenkinsServerHplTaskSpec.groovy @@ -0,0 +1,162 @@ +package org.jenkinsci.gradle.plugins.jpi.server + +import org.jenkinsci.gradle.plugins.jpi.IntegrationSpec +import org.jenkinsci.gradle.plugins.jpi.TestDataGenerator +import spock.lang.Unroll + +import java.util.jar.Manifest + +import static org.jenkinsci.gradle.plugins.jpi.server.GenerateJenkinsServerHplTask.TASK_NAME + +class GenerateJenkinsServerHplTaskSpec extends IntegrationSpec { + private final String projectName = TestDataGenerator.generateName() + private File settings + private File build + private Map minimalAttributes + + def setup() { + settings = projectDir.newFile('settings.gradle') + settings << """rootProject.name = \"$projectName\"""" + build = projectDir.newFile('build.gradle') + build << '''\ + plugins { + id 'org.jenkins-ci.jpi' + } + '''.stripIndent() + minimalAttributes = [ + 'Long-Name' : 'strawberry', + 'Minimum-Java-Version' : '1.8', + 'Support-Dynamic-Loading': 'true', + 'Resource-Path' : new File(projectDir.root, 'src/main/webapp').path, + 'Libraries' : '', + 'Plugin-Version' : '6.0.13', + 'Jenkins-Version' : '2.222.3', + 'Extension-Name' : 'strawberry', + 'Manifest-Version' : '1.0', + 'Short-Name' : 'strawberry', + ] + } + + @Unroll + def 'should build up manifest with expected attributes'(String inner, String outer, Map modifications) { + given: + build << """ + jenkinsPlugin { + shortName = 'strawberry' + coreVersion = '2.222.3' + $inner + } + $outer + version = '6.0.13' + + java { + targetCompatibility = JavaVersion.VERSION_1_8 + } + """.stripIndent() + modifications.each { k, v -> + minimalAttributes[k] = v + } + + when: + gradleRunner() + .withArguments(TASK_NAME) + .build() + + then: + def file = new File(projectDir.root, 'build/hpl/strawberry.hpl') + file.exists() + new Manifest(file.newInputStream()) == toManifest(minimalAttributes) + + where: + inner | outer | modifications + '' | '' | [:] + '''displayName = 'The Strawberry Plugin' ''' | '' | ['Long-Name': 'The Strawberry Plugin'] + '''url = 'https://example.org/123' ''' | '' | ['Url': 'https://example.org/123'] + '''compatibleSinceVersion = '2.64' ''' | '' | ['Compatible-Since-Version': '2.64'] + '''sandboxStatus = true ''' | '' | ['Sandbox-Status': 'true'] + '''sandboxStatus = false ''' | '' | [:] + '''maskClasses = true ''' | '' | ['Mask-Classes': 'true'] + '''maskClasses = false ''' | '' | ['Mask-Classes': 'false'] + '''pluginFirstClassLoader = true ''' | '' | ['PluginFirstClassLoader': 'true'] + '''pluginFirstClassLoader = false ''' | '' | [:] + '''\ + developers { + developer { + name = 'Avatar' + id = 'a' + email = 'a@example.org' + } + developer { + name = 'Bear' + id = 'b' + } + developer { + name = 'Charlie' + email = 'charlie@example.org' + } + developer { + id = 'd' + email = 'd@example.org' + } + }'''.stripIndent() | '' | ['Plugin-Developers': 'Avatar:a:a@example.org,Bear:b:,Charlie::charlie@example.org,:d:d@example.org'] + '''developers {}''' | '' | [:] + '' | '''group = 'org.example.fancy' ''' | ['Group-Id': 'org.example.fancy'] + // TODO dynamic load + // TODO plugin class + } + + def 'should load libraries and plugin-dependencies'() { + given: + build << """ + jenkinsPlugin { + shortName = 'strawberry' + coreVersion = '2.222.3' + workDir = file('embedded-jenkins') + } + version = '6.0.13' + + java { + targetCompatibility = JavaVersion.VERSION_1_8 + } + + configurations { + wanted + } + + dependencies { + implementation 'com.google.guava:guava:19.0' + implementation 'org.jetbrains:annotations:13.0' + implementation 'org.jenkins-ci.plugins:git:4.0.0' + + wanted 'com.google.guava:guava:19.0' + wanted 'org.jetbrains:annotations:13.0' + } + + tasks.register('depFiles') { + doLast { + print configurations.wanted.resolvedConfiguration.resolvedArtifacts + .collect { it.file }.join(',') + } + } + """.stripIndent() + def depFilesResult = gradleRunner().withArguments('depFiles', '-q').build() + minimalAttributes['Plugin-Dependencies'] = 'git:4.0.0' + minimalAttributes['Libraries'] = depFilesResult.output + + when: + gradleRunner() + .withArguments(TASK_NAME) + .build() + + then: + def file = new File(projectDir.root, 'build/hpl/strawberry.hpl') + file.exists() + new Manifest(file.newInputStream()) == toManifest(minimalAttributes) + } + + private static Manifest toManifest(Map attributes) { + def m = new Manifest() + attributes.each { k, v -> m.mainAttributes.putValue(k, v) } + m + } +} diff --git a/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/server/InstallJenkinsServerPluginsTaskSpec.groovy b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/server/InstallJenkinsServerPluginsTaskSpec.groovy new file mode 100644 index 00000000..f2663e54 --- /dev/null +++ b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/server/InstallJenkinsServerPluginsTaskSpec.groovy @@ -0,0 +1,273 @@ +package org.jenkinsci.gradle.plugins.jpi.server + +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.TaskOutcome +import org.jenkinsci.gradle.plugins.jpi.IntegrationSpec +import org.jenkinsci.gradle.plugins.jpi.TestDataGenerator +import org.jenkinsci.gradle.plugins.jpi.support.CodeBlock +import org.jenkinsci.gradle.plugins.jpi.support.DependenciesBlock +import org.jenkinsci.gradle.plugins.jpi.support.Neptune +import org.jenkinsci.gradle.plugins.jpi.support.PluginsBlock +import org.jenkinsci.gradle.plugins.jpi.support.ProjectFile +import spock.lang.Unroll + +import java.nio.file.Files + +import static InstallJenkinsServerPluginsTask.TASK_NAME + +class InstallJenkinsServerPluginsTaskSpec extends IntegrationSpec { + private static final String TASK_PATH = ':' + TASK_NAME + private static final Set DEFAULT = ['ui-samples-plugin.hpi'] as Set + private static final Set SOLO = DEFAULT + ['apache-httpcomponents-client-4-api.hpi'] as Set + private static final Set TRANSITIVES = DEFAULT + ['apache-httpcomponents-client-4-api.hpi', + 'credentials.hpi', + 'display-url-api.hpi', + 'git.hpi', + 'git-client.hpi', + 'jsch.hpi', + 'junit.hpi', + 'mailer.hpi', + 'scm-api.hpi', + 'ssh-credentials.hpi', + 'structs.hpi', + 'ui-samples-plugin.hpi', + 'workflow-scm-step.hpi', + 'workflow-step-api.hpi'] as Set + private final String projectName = TestDataGenerator.generateName() + private ProjectFile.Builder projectBuilder + + def setup() { + projectBuilder = ProjectFile.newBuilder() + .withName(projectName) + .withPlugins(PluginsBlock.newBuilder() + .withPlugin('org.jenkins-ci.jpi') + .build()) + .withBlock(CodeBlock.newBuilder('jenkinsPlugin') + .addStatement('coreVersion = $S', '2.222.3') + .build()) + } + + def 'should sync plugins without version'(Set dependencies) { + given: + projectBuilder.withDependencies(DependenciesBlock.newBuilder() + .addAllToImplementation(dependencies) + .build()) + Neptune.newBuilder(projectBuilder.build()) + .build() + .writeTo(projectDir) + + when: + def result = runInstallJenkinsServerPlugins() + + then: + result.task(TASK_PATH).outcome == TaskOutcome.SUCCESS + actualPluginsDir() == expectedPluginsDir(dependencies) + + and: + def rerun = runInstallJenkinsServerPlugins() + rerun.task(TASK_PATH).outcome == TaskOutcome.UP_TO_DATE + + where: + dependencies | _ + [] | _ + ['com.google.guava:guava:19.0'] | _ + ['org.jenkins-ci.plugins:apache-httpcomponents-client-4-api:4.5.10-1.0'] | _ + ['org.jenkins-ci.plugins:git:4.0.0'] | _ + } + + @Unroll + def 'should rerun if #config dependencies #description'(String config, + String description, + List starting, + List updated) { + given: + projectBuilder.withDependencies(DependenciesBlock.newBuilder() + .addAllTo(config, starting) + .build()) + Neptune.newBuilder(projectBuilder.build()) + .build() + .writeTo(projectDir) + + when: + def firstResult = runInstallJenkinsServerPlugins() + + then: + firstResult.task(TASK_PATH).outcome == TaskOutcome.SUCCESS + actualPluginsDir() == expectedPluginsDir(starting) + + when: + projectBuilder + .clearDependencies() + .withDependencies(DependenciesBlock.newBuilder() + .addAllTo(config, updated) + .build()) + Neptune.newBuilder(projectBuilder.build()) + .build() + .writeTo(projectDir) + def secondResult = runInstallJenkinsServerPlugins() + + then: + secondResult.task(TASK_PATH).outcome == TaskOutcome.SUCCESS + actualPluginsDir() == expectedPluginsDir(updated) + + where: + config | description | starting | updated + 'api' | 'changed jpi' | ['org.jenkins-ci.plugins:git:4.0.0'] | ['org.jenkins-ci.plugins:git:4.0.1'] + 'api' | 'added jpi' | [] | ['org.jenkins-ci.plugins:git:4.0.0'] + 'api' | 'removed jpi' | ['org.jenkins-ci.plugins:git:4.0.0'] | [] + 'api' | 'changed jar' | ['com.google.guava:guava:19.0'] | ['com.google.guava:guava:20.0'] + 'api' | 'added jar' | [] | ['com.google.guava:guava:20.0'] + 'api' | 'removed jar' | ['com.google.guava:guava:20.0'] | [] + + 'implementation' | 'changed jpi' | ['org.jenkins-ci.plugins:git:4.0.0'] | ['org.jenkins-ci.plugins:git:4.0.1'] + 'implementation' | 'added jpi' | [] | ['org.jenkins-ci.plugins:git:4.0.1'] + 'implementation' | 'removed jpi' | ['org.jenkins-ci.plugins:git:4.0.0'] | [] + 'implementation' | 'changed jar' | ['com.google.guava:guava:19.0'] | ['com.google.guava:guava:20.0'] + 'implementation' | 'added jar' | [] | ['com.google.guava:guava:20.0'] + 'implementation' | 'removed jar' | ['com.google.guava:guava:20.0'] | [] + + 'runtimeOnly' | 'changed jpi' | ['org.jenkins-ci.plugins:git:4.0.0'] | ['org.jenkins-ci.plugins:git:4.0.1'] + 'runtimeOnly' | 'added jpi' | [] | ['org.jenkins-ci.plugins:git:4.0.1'] + 'runtimeOnly' | 'removed jpi' | ['org.jenkins-ci.plugins:git:4.0.0'] | [] + 'runtimeOnly' | 'changed jar' | ['com.google.guava:guava:19.0'] | ['com.google.guava:guava:20.0'] + 'runtimeOnly' | 'added jar' | [] | ['com.google.guava:guava:20.0'] + 'runtimeOnly' | 'removed jar' | ['com.google.guava:guava:20.0'] | [] + + 'testImplementation' | 'changed jpi' | ['org.jenkins-ci.plugins:git:4.0.0'] | ['org.jenkins-ci.plugins:git:4.0.1'] + 'testImplementation' | 'added jpi' | [] | ['org.jenkins-ci.plugins:git:4.0.1'] + 'testImplementation' | 'removed jpi' | ['org.jenkins-ci.plugins:git:4.0.0'] | [] + 'testImplementation' | 'changed jar' | ['com.google.guava:guava:19.0'] | ['com.google.guava:guava:20.0'] + 'testImplementation' | 'added jar' | [] | ['com.google.guava:guava:20.0'] + 'testImplementation' | 'removed jar' | ['com.google.guava:guava:20.0'] | [] + + 'testRuntimeOnly' | 'changed jpi' | ['org.jenkins-ci.plugins:git:4.0.0'] | ['org.jenkins-ci.plugins:git:4.0.1'] + 'testRuntimeOnly' | 'added jpi' | [] | ['org.jenkins-ci.plugins:git:4.0.1'] + 'testRuntimeOnly' | 'removed jpi' | ['org.jenkins-ci.plugins:git:4.0.0'] | [] + 'testRuntimeOnly' | 'changed jar' | ['com.google.guava:guava:19.0'] | ['com.google.guava:guava:20.0'] + 'testRuntimeOnly' | 'added jar' | [] | ['com.google.guava:guava:20.0'] + 'testRuntimeOnly' | 'removed jar' | ['com.google.guava:guava:20.0'] | [] + } + + @Unroll + def 'should be UP-TO-DATE if #config dependencies #description'(String config, + String description, + List starting, + List updated) { + given: + projectBuilder.withDependencies(DependenciesBlock.newBuilder() + .addAllTo(config, starting) + .build()) + Neptune.newBuilder(projectBuilder.build()) + .build() + .writeTo(projectDir) + + when: + def firstResult = runInstallJenkinsServerPlugins() + + then: + firstResult.task(TASK_PATH).outcome == TaskOutcome.SUCCESS + actualPluginsDir() == expectedPluginsDir(starting, config) + + when: + projectBuilder + .clearDependencies() + .withDependencies(DependenciesBlock.newBuilder() + .addAllTo(config, updated) + .build()) + Neptune.newBuilder(projectBuilder.build()) + .build() + .writeTo(projectDir) + def secondResult = runInstallJenkinsServerPlugins() + + then: + secondResult.task(TASK_PATH).outcome == TaskOutcome.UP_TO_DATE + + where: + config | description | starting | updated + 'api' | 'stay jpi' | ['org.jenkins-ci.plugins:git:4.0.0'] | ['org.jenkins-ci.plugins:git:4.0.0'] + 'api' | 'stay jar' | ['com.google.guava:guava:20.0'] | ['com.google.guava:guava:20.0'] + 'implementation' | 'stay jpi' | ['org.jenkins-ci.plugins:git:4.0.0'] | ['org.jenkins-ci.plugins:git:4.0.0'] + 'implementation' | 'stay jar' | ['com.google.guava:guava:20.0'] | ['com.google.guava:guava:20.0'] + 'runtimeOnly' | 'stay jpi' | ['org.jenkins-ci.plugins:git:4.0.0'] | ['org.jenkins-ci.plugins:git:4.0.0'] + 'runtimeOnly' | 'stay jar' | ['com.google.guava:guava:20.0'] | ['com.google.guava:guava:20.0'] + 'testImplementation' | 'stay jpi' | ['org.jenkins-ci.plugins:git:4.0.0'] | ['org.jenkins-ci.plugins:git:4.0.0'] + 'testImplementation' | 'stay jar' | ['com.google.guava:guava:20.0'] | ['com.google.guava:guava:20.0'] + 'testRuntimeOnly' | 'stay jpi' | ['org.jenkins-ci.plugins:git:4.0.0'] | ['org.jenkins-ci.plugins:git:4.0.0'] + 'testRuntimeOnly' | 'stay jar' | ['com.google.guava:guava:20.0'] | ['com.google.guava:guava:20.0'] + + 'compileOnly' | 'changed jpi' | ['org.jenkins-ci.plugins:git:4.0.0'] | ['org.jenkins-ci.plugins:git:4.0.1'] + 'compileOnly' | 'added jpi' | [] | ['org.jenkins-ci.plugins:git:4.0.0'] + 'compileOnly' | 'removed jpi' | ['org.jenkins-ci.plugins:git:4.0.0'] | [] + 'compileOnly' | 'changed jar' | ['com.google.guava:guava:19.0'] | ['com.google.guava:guava:20.0'] + 'compileOnly' | 'added jar' | [] | ['com.google.guava:guava:20.0'] + 'compileOnly' | 'removed jar' | ['com.google.guava:guava:20.0'] | [] + + 'testCompileOnly' | 'changed jpi' | ['org.jenkins-ci.plugins:git:4.0.0'] | ['org.jenkins-ci.plugins:git:4.0.1'] + 'testCompileOnly' | 'added jpi' | [] | ['org.jenkins-ci.plugins:git:4.0.0'] + 'testCompileOnly' | 'removed jpi' | ['org.jenkins-ci.plugins:git:4.0.0'] | [] + 'testCompileOnly' | 'changed jar' | ['com.google.guava:guava:19.0'] | ['com.google.guava:guava:20.0'] + 'testCompileOnly' | 'added jar' | [] | ['com.google.guava:guava:20.0'] + 'testCompileOnly' | 'removed jar' | ['com.google.guava:guava:20.0'] | [] + } + + def 'should rerun if workDir changes'() { + given: + def deps = ['org.jenkins-ci.plugins:git:4.0.0'] + projectBuilder.withDependencies(DependenciesBlock.newBuilder() + .addAllToImplementation(deps) + .build()) + .withBlock(CodeBlock.newBuilder('jenkinsPlugin') + .setStatement('workDir = file($S)', 'work') + .build()) + Neptune.newBuilder(projectBuilder.build()) + .build() + .writeTo(projectDir) + + when: + def firstResult = runInstallJenkinsServerPlugins() + + then: + firstResult.task(TASK_PATH).outcome == TaskOutcome.SUCCESS + actualPluginsDir() == expectedPluginsDir(deps) + + when: + projectBuilder.withBlock(CodeBlock.newBuilder('jenkinsPlugin') + .setStatement('workDir = file($S)', 'jenkins-home') + .build()) + Neptune.newBuilder(projectBuilder.build()).build().writeTo(projectDir) + def secondResult = runInstallJenkinsServerPlugins() + + then: + secondResult.task(TASK_PATH).outcome == TaskOutcome.SUCCESS + actualPluginsDir('jenkins-home') == expectedPluginsDir(deps) + } + + private Set actualPluginsDir(String dir = 'work') { + Files.list(projectDir.root.toPath().resolve(dir).resolve('plugins')) + .collect { it.fileName.toString() } + .toSet() + } + + private Set expectedPluginsDir(Collection dependencies, String configuration = 'implementation') { + def hpl = projectName + '.hpl' + def result = [hpl] as Set + result.addAll(DEFAULT) + if (!['api', 'implementation', 'runtimeOnly', 'testImplementation', 'testRuntimeOnly'].contains(configuration)) { + return result + } + if (dependencies.contains('org.jenkins-ci.plugins:git:4.0.0')) { + result.addAll(TRANSITIVES) + } + if (dependencies.contains('org.jenkins-ci.plugins:git:4.0.1')) { + result.addAll(TRANSITIVES) + } + if (dependencies.contains('org.jenkins-ci.plugins:apache-httpcomponents-client-4-api:4.5.10-1.0')) { + result.addAll(SOLO) + } + result + } + + private BuildResult runInstallJenkinsServerPlugins() { + gradleRunner().withArguments(TASK_NAME).build() + } +} diff --git a/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/CodeBlock.java b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/CodeBlock.java new file mode 100644 index 00000000..8e5766be --- /dev/null +++ b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/CodeBlock.java @@ -0,0 +1,80 @@ +package org.jenkinsci.gradle.plugins.jpi.support; + +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; + +public class CodeBlock implements Comparable, Emitable { + private final String name; + protected final List statements; + + public CodeBlock(String name, List statements) { + this.name = name; + this.statements = statements; + } + + public static Builder newBuilder(String name) { + return new Builder(name); + } + + public String getName() { + return name; + } + + public int filePosition() { + return 0; + } + + @Override + public String emit(Indenter indenter) { + StringBuilder sb = new StringBuilder(indenter.indent()).append(name).append(" {\n"); + Indenter increased = indenter.increase(); + for (Statement statement : statements) { + sb.append(statement.emit(increased)).append("\n"); + } + return sb.append(indenter.indent()).append("}\n").toString(); + } + + public Builder toBuilder() { + Builder b = new Builder(name); + b.statements.addAll(statements); + return b; + } + + @Override + public int compareTo(CodeBlock o) { + return Comparator.comparing(CodeBlock::filePosition) + .compare(this, o); + } + + public static class Builder { + private String name; + private final List statements = new LinkedList<>(); + + public Builder() { + } + + public Builder(String name) { + this.name = name; + } + + public CodeBlock build() { + return new CodeBlock(name, statements); + } + + public Builder addStatement(String statement, Object... args) { + return addStatement(Statement.create(statement, args)); + } + + public Builder addStatement(Statement statement) { + statements.add(statement); + return this; + } + + public Builder setStatement(String template, Object... args) { + statements.removeIf(s -> s.getTemplate().equals(template)); + statements.add(Statement.create(template, args)); + return this; + } + } +} diff --git a/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/CodeBlockSpec.groovy b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/CodeBlockSpec.groovy new file mode 100644 index 00000000..10eb34eb --- /dev/null +++ b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/CodeBlockSpec.groovy @@ -0,0 +1,20 @@ +package org.jenkinsci.gradle.plugins.jpi.support + +import spock.lang.Specification + +class CodeBlockSpec extends Specification { + def 'should replace statement by template'() { + given: + CodeBlock block = CodeBlock.newBuilder('jenkinsPlugins') + .addStatement('workDir = file($S)', 'work') + .build() + + when: + CodeBlock actual = block.toBuilder() + .setStatement('workDir = file($S)', 'work2') + .build() + + then: + actual.statements == [Statement.create('workDir = file($S)', 'work2')] + } +} diff --git a/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/DependenciesBlock.java b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/DependenciesBlock.java new file mode 100644 index 00000000..d093df25 --- /dev/null +++ b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/DependenciesBlock.java @@ -0,0 +1,63 @@ +package org.jenkinsci.gradle.plugins.jpi.support; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +public class DependenciesBlock extends CodeBlock { + public static final String NAME = "dependencies"; + + public DependenciesBlock(List statements) { + super(NAME, statements); + } + + public static Builder newBuilder() { + return new Builder(); + } + + public Builder toBuilder() { + return new Builder(statements); + } + + static class Builder extends CodeBlock.Builder { + private List statements; + + public Builder() { + this(new LinkedList<>()); + } + + public Builder(List statements) { + this.statements = statements; + } + + public Builder add(String configuration, String notation) { + statements.add(Statement.createDependency(configuration, notation)); + return this; + } + + public Builder addAllTo(String configuration, Collection notations) { + notations.stream() + .map(n -> Statement.createDependency(configuration, n)) + .forEach(statements::add); + return this; + } + + public Builder addImplementation(String notation) { + return add("implementation", notation); + } + + public Builder addAllToImplementation(Collection notations) { + return addAllTo("implementation", notations); + } + + public Builder reset() { + statements.clear(); + return this; + } + + @Override + public DependenciesBlock build() { + return new DependenciesBlock(statements); + } + } +} diff --git a/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/DependenciesBlockSpec.groovy b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/DependenciesBlockSpec.groovy new file mode 100644 index 00000000..9280cfa1 --- /dev/null +++ b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/DependenciesBlockSpec.groovy @@ -0,0 +1,42 @@ +package org.jenkinsci.gradle.plugins.jpi.support + +import spock.lang.Specification + +import static org.jenkinsci.gradle.plugins.jpi.support.FourSpaceIndenter.create + +class DependenciesBlockSpec extends Specification { + def 'should emit with indent'(Indenter indenter, String expected) { + given: + def block = DependenciesBlock.newBuilder() + .add('api', 'org.slf4j:slf4j-api:1.7.25') + .build() + + when: + def actual = block.emit(indenter) + + then: + actual == expected + + where: + indenter | expected + create() | '''dependencies {\n api 'org.slf4j:slf4j-api:1.7.25'\n}\n''' + create().increase() | ''' dependencies {\n api 'org.slf4j:slf4j-api:1.7.25'\n }\n''' + } + + def 'should reset'() { + given: + def starting = DependenciesBlock.newBuilder() + .addImplementation('junit:junit:4.12') + .build() + def rebuilt = starting.toBuilder() + .reset() + .addImplementation('com.google.guava:guava:20.0') + .build() + + when: + def actual = rebuilt.emit(create()) + + then: + actual == '''dependencies {\n implementation 'com.google.guava:guava:20.0'\n}\n''' + } +} diff --git a/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/Emitable.java b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/Emitable.java new file mode 100644 index 00000000..f31c9335 --- /dev/null +++ b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/Emitable.java @@ -0,0 +1,5 @@ +package org.jenkinsci.gradle.plugins.jpi.support; + +public interface Emitable { + String emit(Indenter indenter); +} diff --git a/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/FourSpaceIndenter.java b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/FourSpaceIndenter.java new file mode 100644 index 00000000..e72dba45 --- /dev/null +++ b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/FourSpaceIndenter.java @@ -0,0 +1,28 @@ +package org.jenkinsci.gradle.plugins.jpi.support; + +public class FourSpaceIndenter implements Indenter { + private final int level; + private final String indent; + + private FourSpaceIndenter(int level) { + this.level = level; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 4 * level; i++) { + sb.append(' '); + } + this.indent = sb.toString(); + } + + public FourSpaceIndenter increase() { + return new FourSpaceIndenter(level + 1); + } + + @Override + public String indent() { + return indent; + } + + public static FourSpaceIndenter create() { + return new FourSpaceIndenter(0); + } +} diff --git a/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/Indenter.java b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/Indenter.java new file mode 100644 index 00000000..f4a4236b --- /dev/null +++ b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/Indenter.java @@ -0,0 +1,6 @@ +package org.jenkinsci.gradle.plugins.jpi.support; + +public interface Indenter { + Indenter increase(); + String indent(); +} diff --git a/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/Neptune.java b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/Neptune.java new file mode 100644 index 00000000..9441baf6 --- /dev/null +++ b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/Neptune.java @@ -0,0 +1,71 @@ +package org.jenkinsci.gradle.plugins.jpi.support; + +import org.junit.rules.TemporaryFolder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +public class Neptune { + private static final Logger LOGGER = LoggerFactory.getLogger(Neptune.class); + private final ProjectFile root; + private final SettingsFile settings; + private final Indenter indenter; + + public Neptune(ProjectFile root, SettingsFile settings, Indenter indenter) { + this.root = root; + this.settings = settings; + this.indenter = indenter; + } + + public void writeTo(TemporaryFolder directory) { + writeTo(directory.getRoot().toPath()); + } + + public void writeTo(Path directory) { + write(directory.resolve("build.gradle"), root.emit(indenter)); + write(directory.resolve("settings.gradle"), settings.emit(indenter)); + } + + private static void write(Path path, String contents) { + try { + Files.write(path, contents.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + LOGGER.error("Failed to write to " + path, e); + } + } + + public static Neptune.Builder newBuilder() { + return new Neptune.Builder(); + } + + public static Neptune.Builder newBuilder(ProjectFile root) { + return new Neptune.Builder() + .withRootProject(root); + } + + public static class Builder { + private ProjectFile root; + private SettingsFile settings; + private final Indenter indenter = FourSpaceIndenter.create(); + + private Builder() { + } + + public Builder withRootProject(ProjectFile root) { + this.root = root; + this.settings = SettingsFile.builder() + .withRootProjectName(root.getName()) + .build(); + return this; + } + + public Neptune build() { + return new Neptune(root, settings, indenter); + } + } +} diff --git a/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/NeptuneSpec.groovy b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/NeptuneSpec.groovy new file mode 100644 index 00000000..fd1b18e5 --- /dev/null +++ b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/NeptuneSpec.groovy @@ -0,0 +1,26 @@ +package org.jenkinsci.gradle.plugins.jpi.support + +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import spock.lang.Specification + +class NeptuneSpec extends Specification { + @Rule + protected final TemporaryFolder projectDir = new TemporaryFolder() + + def 'should create build and settings'() { + given: + def neptune = Neptune.newBuilder() + .withRootProject(ProjectFile.newBuilder() + .withName('avacado') + .build()) + .build() + + when: + neptune.writeTo(projectDir) + + then: + new File(projectDir.root, 'build.gradle').exists() + new File(projectDir.root, 'settings.gradle').exists() + } +} diff --git a/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/PluginsBlock.java b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/PluginsBlock.java new file mode 100644 index 00000000..70c547ea --- /dev/null +++ b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/PluginsBlock.java @@ -0,0 +1,40 @@ +package org.jenkinsci.gradle.plugins.jpi.support; + +import java.util.LinkedList; +import java.util.List; + +public class PluginsBlock extends CodeBlock { + public static final String NAME = "plugins"; + + public PluginsBlock(List statements) { + super(NAME, statements); + } + + @Override + public int filePosition() { + return -100; + } + + public static PluginsBlock.Builder newBuilder() { + return new Builder(); + } + + public static class Builder extends CodeBlock.Builder { + private final List statements = new LinkedList<>(); + + public Builder withPlugin(String id) { + statements.add(Statement.createPlugin(id)); + return this; + } + + public Builder withPlugin(String id, String version) { + statements.add(Statement.createPlugin(id, version)); + return this; + } + + @Override + public PluginsBlock build() { + return new PluginsBlock(statements); + } + } +} diff --git a/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/ProjectFile.java b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/ProjectFile.java new file mode 100644 index 00000000..42a97460 --- /dev/null +++ b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/ProjectFile.java @@ -0,0 +1,63 @@ +package org.jenkinsci.gradle.plugins.jpi.support; + +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +public class ProjectFile implements Emitable { + private final String name; + private final List blocks; + + public ProjectFile(String name, List blocks) { + this.name = name; + this.blocks = blocks; + } + + public static Builder newBuilder() { + return new Builder(); + } + + public String getName() { + return name; + } + + @Override + public String emit(Indenter indenter) { + return blocks.stream() + .sorted() + .map(b -> b.emit(indenter)) + .collect(Collectors.joining("\n")); + } + + public static class Builder { + private String name; + private final List blocks = new LinkedList<>(); + + public ProjectFile build() { + return new ProjectFile(name, blocks); + } + + public Builder withPlugins(PluginsBlock block) { + return withBlock(block); + } + + public Builder withDependencies(DependenciesBlock block) { + return withBlock(block); + } + + public Builder withBlock(CodeBlock block) { + blocks.add(block); + return this; + } + + public Builder withName(String name) { + this.name = name; + return this; + } + + public Builder clearDependencies() { + blocks.removeIf(block -> block instanceof DependenciesBlock); + return this; + } + } +} diff --git a/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/ProjectFileSpec.groovy b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/ProjectFileSpec.groovy new file mode 100644 index 00000000..50555db0 --- /dev/null +++ b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/ProjectFileSpec.groovy @@ -0,0 +1,125 @@ +package org.jenkinsci.gradle.plugins.jpi.support + +import spock.lang.Specification + +class ProjectFileSpec extends Specification { + Indenter indenter = FourSpaceIndenter.create() + + def 'should write empty'() { + expect: + ProjectFile.newBuilder().build().emit(indenter) == '' + } + + def 'should write file with plugin'() { + expect: + ProjectFile.newBuilder() + .withPlugins(PluginsBlock.newBuilder() + .withPlugin('org.jenkins-ci.jpi') + .build()) + .build() + .emit(indenter) == '''\ + plugins { + id 'org.jenkins-ci.jpi' + } + '''.stripIndent() + } + + def 'should handle plugins with version'() { + expect: + ProjectFile.newBuilder() + .withPlugins(PluginsBlock.newBuilder() + .withPlugin('org.jenkins-ci.jpi') + .withPlugin('nebula.maven-publish', '17.0.5') + .build()) + .build() + .emit(indenter) == '''\ + plugins { + id 'org.jenkins-ci.jpi' + id 'nebula.maven-publish' version '17.0.5' + } + '''.stripIndent() + } + + def 'should configure extension with replacements'() { + expect: + ProjectFile.newBuilder() + .withBlock(CodeBlock.newBuilder('jenkinsPlugin') + .addStatement('configurePublishing = $L', true) + .addStatement('fileExtension = $S', 'jpi') + .build()) + .build() + .emit(indenter) == '''\ + jenkinsPlugin { + configurePublishing = true + fileExtension = 'jpi' + } + '''.stripIndent() + } + + def 'should handle dependencies'() { + expect: + ProjectFile.newBuilder() + .withDependencies(DependenciesBlock.newBuilder() + .addImplementation('com.google.guava:guava:19.0') + .add('testImplementation', 'junit:junit:4.12') + .build()) + .build() + .emit(indenter) == '''\ + dependencies { + implementation 'com.google.guava:guava:19.0' + testImplementation 'junit:junit:4.12' + } + '''.stripIndent() + } + + def 'should clear dependencies'() { + given: + def builder = ProjectFile.newBuilder() + .withDependencies(DependenciesBlock.newBuilder() + .addImplementation('com.google.guava:guava:19.0') + .add('testImplementation', 'junit:junit:4.12') + .build()) + .clearDependencies() + .withDependencies(DependenciesBlock.newBuilder() + .addImplementation('com.squareup.okio:okio:2.6.0') + .build()) + + when: + def actual = builder.build().emit(indenter) + + then: + actual == '''\ + dependencies { + implementation 'com.squareup.okio:okio:2.6.0' + } + '''.stripIndent() + } + + def 'should do it all'() { + expect: + ProjectFile.newBuilder() + .withPlugins(PluginsBlock.newBuilder() + .withPlugin('org.jenkins-ci.jpi') + .build()) + .withBlock(CodeBlock.newBuilder('jenkinsPlugin') + .addStatement('coreVersion = $S', '2.222.3') + .build()) + .withDependencies(DependenciesBlock.newBuilder() + .addImplementation('com.google.guava:guava:19.0') + .build()) + .build() + .emit(indenter) == '''\ + plugins { + id 'org.jenkins-ci.jpi' + } + + jenkinsPlugin { + coreVersion = '2.222.3' + } + + dependencies { + implementation 'com.google.guava:guava:19.0' + } + '''.stripIndent() + } +} diff --git a/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/SettingsFile.java b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/SettingsFile.java new file mode 100644 index 00000000..e66bab70 --- /dev/null +++ b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/SettingsFile.java @@ -0,0 +1,40 @@ +package org.jenkinsci.gradle.plugins.jpi.support; + +import java.util.LinkedList; +import java.util.List; + +public class SettingsFile implements Emitable { + private final List statements; + + public SettingsFile(List statements) { + this.statements = statements; + } + + @Override + public String emit(Indenter indenter) { + StringBuilder sb = new StringBuilder(); + for (Statement statement : statements) { + sb.append(indenter.indent()).append(statement.emit(indenter)); + } + return sb.append('\n').toString(); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String name; + + public Builder withRootProjectName(String name) { + this.name = name; + return this; + } + + public SettingsFile build() { + List statements = new LinkedList<>(); + statements.add(Statement.create("rootProject.name = $S", name)); + return new SettingsFile(statements); + } + } +} diff --git a/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/SettingsFileSpec.groovy b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/SettingsFileSpec.groovy new file mode 100644 index 00000000..ca583d0e --- /dev/null +++ b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/SettingsFileSpec.groovy @@ -0,0 +1,18 @@ +package org.jenkinsci.gradle.plugins.jpi.support + +import spock.lang.Specification + +class SettingsFileSpec extends Specification { + Indenter indenter = FourSpaceIndenter.create() + + def 'should create with given root name'() { + expect: + SettingsFile.builder() + .withRootProjectName('orange') + .build() + .emit(indenter) == '''\ + rootProject.name = 'orange' + '''.stripIndent() + } + +} diff --git a/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/Statement.java b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/Statement.java new file mode 100644 index 00000000..476e7806 --- /dev/null +++ b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/Statement.java @@ -0,0 +1,83 @@ +package org.jenkinsci.gradle.plugins.jpi.support; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.Objects; +import java.util.Queue; + +class Statement implements Emitable { + private final String template; + private final String rendered; + + private Statement(String template, String rendered) { + this.template = template; + this.rendered = rendered; + } + + public String getTemplate() { + return template; + } + + public static Statement create(String template, Object... args) { + Queue q = new LinkedList<>(); + Collections.addAll(q, args); + StringBuilder replaced = new StringBuilder(); + boolean makingReplacement = false; + for (char c : template.toCharArray()) { + if (makingReplacement) { + makingReplacement = false; + switch (c) { + case 'L': + replaced.append(q.poll()); + break; + case 'S': + replaced.append("'").append(q.poll()).append("'"); + break; + default: + throw new IllegalArgumentException("Cannot replace $" + c); + } + } else if (c != '$') { + replaced.append(c); + } else { + makingReplacement = true; + } + } + return new Statement(template, replaced.toString()); + } + + static Statement createPlugin(String id) { + return create("id $S", id); + } + + static Statement createPlugin(String id, String version) { + return create("id $S version $S", id, version); + } + + static Statement createDependency(String configuration, String notation) { + return create("$L $S", configuration, notation); + } + + @Override + public String toString() { + return rendered; + } + + @Override + public String emit(Indenter indenter) { + return indenter.indent() + rendered; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Statement statement = (Statement) o; + return Objects.equals(template, statement.template) && + Objects.equals(rendered, statement.rendered); + } + + @Override + public int hashCode() { + return Objects.hash(template, rendered); + } +} diff --git a/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/StatementSpec.groovy b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/StatementSpec.groovy new file mode 100644 index 00000000..a9ad1736 --- /dev/null +++ b/src/test/groovy/org/jenkinsci/gradle/plugins/jpi/support/StatementSpec.groovy @@ -0,0 +1,41 @@ +package org.jenkinsci.gradle.plugins.jpi.support + +import spock.lang.Specification +import spock.lang.Unroll + +class StatementSpec extends Specification { + @Unroll + def 'should replace #token token'(String token, String format, Object arg, String expected) { + expect: + Statement.create(format, arg).toString() == expected + + where: + token | format | arg | expected + '$L' | 'configurePublishing = $L' | true | 'configurePublishing = true' + '$L' | '$L = false' | 'configurePublishing' | 'configurePublishing = false' + '$L' | 'someNumber($L)' | 6 | 'someNumber(6)' + '$L' | 'someNumber($L)' | 'moon' | 'someNumber(moon)' + '$S' | 'someMethod($S)' | true | 'someMethod(\'true\')' + '$S' | 'someMethod($S)' | 6 | 'someMethod(\'6\')' + '$S' | 'someMethod($S)' | 'moon' | 'someMethod(\'moon\')' + } + + def 'should replace multiple tokens'() { + expect: + def expected = "6 + 'true' + implementation('sun')" + def statement = Statement.create('$L + $S + $L($S)', 6, true, 'implementation', 'sun') + statement.toString() == expected + } + + @Unroll + def 'should emit with indent'(Indenter indenter, String expected) { + expect: + Statement.create('def a = 1').emit(indenter) == expected + + where: + indenter | expected + FourSpaceIndenter.create() | 'def a = 1' + FourSpaceIndenter.create().increase() | ' def a = 1' + FourSpaceIndenter.create().increase().increase() | ' def a = 1' + } +}