From d33af236a1a40fa64a233bea20bd93bb89468603 Mon Sep 17 00:00:00 2001 From: John Ahlroos Date: Thu, 28 Mar 2019 17:23:09 +0200 Subject: [PATCH] Add itegration tests --- .travis.yml | 7 +- build.gradle | 14 +- .../com/devsoap/fn/CreateFunctionTest.groovy | 85 +++++++++++ .../groovy/com/devsoap/fn/DeployTest.groovy | 50 ++++++ .../devsoap/fn/FunctionalServerTest.groovy | 48 ++++++ .../com/devsoap/fn/FunctionalTest.groovy | 143 ++++++++++++++++++ .../resources/AllTestsConfig.groovy | 21 +++ .../com/devsoap/fn/tasks/FnDeployTask.groovy | 1 - .../devsoap/fn/tasks/FnInstallCliTask.groovy | 23 ++- .../fn/tasks/FnPrepareDockerTask.groovy | 4 +- .../devsoap/fn/tasks/FnStartServerTask.groovy | 11 +- .../devsoap/fn/tasks/FnStopServerTask.groovy | 10 +- .../com/devsoap/fn/util/DockerUtil.groovy | 5 +- src/main/resources/versions.properties | 1 + 14 files changed, 404 insertions(+), 19 deletions(-) create mode 100644 src/functionalTest/groovy/com/devsoap/fn/CreateFunctionTest.groovy create mode 100644 src/functionalTest/groovy/com/devsoap/fn/DeployTest.groovy create mode 100644 src/functionalTest/groovy/com/devsoap/fn/FunctionalServerTest.groovy create mode 100644 src/functionalTest/groovy/com/devsoap/fn/FunctionalTest.groovy create mode 100644 src/functionalTest/resources/AllTestsConfig.groovy diff --git a/.travis.yml b/.travis.yml index f4cef6d..aabbe34 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,15 @@ language: groovy jdk: - oraclejdk8 -script: "./gradlew --stacktrace --info --parallel check" +script: + - "./gradlew --stacktrace --info --parallel check" +after_script: + - docker ps -a branches: only: - "master" +services: + - docker before_cache: - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ diff --git a/build.gradle b/build.gradle index 85617c0..431cf90 100644 --- a/build.gradle +++ b/build.gradle @@ -47,7 +47,7 @@ targetCompatibility = 1.8 * Sources * **********************************************************************************************************************/ -/* + sourceSets { functionalTest { groovy { @@ -60,7 +60,6 @@ sourceSets { runtimeClasspath += output + compileClasspath } } -*/ processResources { from(sourceSets.main.resources.srcDirs){ @@ -138,7 +137,7 @@ groovydoc { * Testing & Quality * ***********************************************************************************************************************/ -/* + task functionalTest(type: Test) { dependsOn test group = 'Verification' @@ -161,7 +160,6 @@ task functionalTest(type: Test) { } } check.dependsOn functionalTest -*/ codenarc{ toolVersion = '1.1' @@ -169,9 +167,9 @@ codenarc{ maxPriority1Violations = 0 maxPriority2Violations = 0 maxPriority3Violations = 0 - // codenarcFunctionalTest { - // configFile = rootProject.file('config/codenarc/ruleset-test.groovy') - // } + codenarcFunctionalTest { + configFile = rootProject.file('config/codenarc/ruleset-test.groovy') + } codenarcTest { configFile = rootProject.file('config/codenarc/ruleset-test.groovy') } @@ -198,7 +196,7 @@ plugins.withType(GroovyBasePlugin) { * ***********************************************************************************************************************/ gradlePlugin { - //testSourceSets sourceSets.functionalTest + testSourceSets sourceSets.functionalTest plugins { fnPlugin { id = 'com.devsoap.fn' diff --git a/src/functionalTest/groovy/com/devsoap/fn/CreateFunctionTest.groovy b/src/functionalTest/groovy/com/devsoap/fn/CreateFunctionTest.groovy new file mode 100644 index 0000000..335a9a4 --- /dev/null +++ b/src/functionalTest/groovy/com/devsoap/fn/CreateFunctionTest.groovy @@ -0,0 +1,85 @@ +/* + * Copyright 2018-2019 Devsoap Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.devsoap.fn + +import static org.gradle.testkit.runner.TaskOutcome.SUCCESS + +import org.gradle.testkit.runner.BuildResult +import java.nio.file.Paths + +/** + * Tests creation of FN projects + * + * @author John Ahlroos + * @since 1.0 + */ +class CreateFunctionTest extends FunctionalTest { + + void 'default project is created and compiles'() { + setup: + File rootDir = testProjectDir.root + File javaSourceDir = Paths.get(rootDir.canonicalPath, 'src', 'main', 'java').toFile() + File functionFile = Paths.get(javaSourceDir.canonicalPath, 'MyFunction.java').toFile() + when: + BuildResult result = run'fnCreateFunction', 'jar' + then: + result.task(':fnCreateFunction').outcome == SUCCESS + result.task(':jar').outcome == SUCCESS + functionFile.exists() + } + + void 'default Groovy project is created and compiles'() { + setup: + extraPlugins = ['groovy':null] + + buildFile << ''' + dependencies { + compile fn.groovy() + } + '''.stripIndent() + + File rootDir = testProjectDir.root + File javaSourceDir = Paths.get(rootDir.canonicalPath, 'src', 'main', 'groovy').toFile() + File functionFile = Paths.get(javaSourceDir.canonicalPath, 'MyFunction.groovy').toFile() + when: + BuildResult result = run'fnCreateFunction', 'jar' + then: + result.task(':fnCreateFunction').outcome == SUCCESS + result.task(':jar').outcome == SUCCESS + functionFile.exists() + } + + void 'default Kotlin project is created and compiles'() { + setup: + extraPlugins = ['org.jetbrains.kotlin.jvm':'1.3.21'] + + buildFile << ''' + dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib" + } + '''.stripIndent() + + File rootDir = testProjectDir.root + File javaSourceDir = Paths.get(rootDir.canonicalPath, 'src', 'main', 'kotlin').toFile() + File functionFile = Paths.get(javaSourceDir.canonicalPath, 'MyFunction.kt').toFile() + when: + BuildResult result = run'fnCreateFunction', 'jar' + then: + result.task(':fnCreateFunction').outcome == SUCCESS + result.task(':jar').outcome == SUCCESS + functionFile.exists() + } +} diff --git a/src/functionalTest/groovy/com/devsoap/fn/DeployTest.groovy b/src/functionalTest/groovy/com/devsoap/fn/DeployTest.groovy new file mode 100644 index 0000000..6f7eda8 --- /dev/null +++ b/src/functionalTest/groovy/com/devsoap/fn/DeployTest.groovy @@ -0,0 +1,50 @@ +/* + * Copyright 2018-2019 Devsoap Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.devsoap.fn + +import static org.gradle.testkit.runner.TaskOutcome.SUCCESS + +import spock.lang.Ignore +import org.gradle.testkit.runner.BuildResult + +/** + * Tests function deployment + * + * @author John Ahlroos + * @since 1.0 + */ +class DeployTest extends FunctionalServerTest { + + /** + * FIXME For some reason Travis cannot connect to the server after it has started + */ + @Ignore + void 'deploy default function'() { + setup: + run'fnCreateFunction' + when: + BuildResult deployResult = run'fnDeploy' + BuildResult invokeResult = run 'fnInvoke' + then: + deployResult.task(':fnDeploy').outcome == SUCCESS + deployResult.output.contains('Successfully created function') + deployResult.output.contains('Successfully created trigger') + + invokeResult.task(':fnInvoke').outcome == SUCCESS + invokeResult.output.contains('Hello, world!') + } + +} diff --git a/src/functionalTest/groovy/com/devsoap/fn/FunctionalServerTest.groovy b/src/functionalTest/groovy/com/devsoap/fn/FunctionalServerTest.groovy new file mode 100644 index 0000000..4551831 --- /dev/null +++ b/src/functionalTest/groovy/com/devsoap/fn/FunctionalServerTest.groovy @@ -0,0 +1,48 @@ +/* + * Copyright 2018-2019 Devsoap Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.devsoap.fn + +import static junit.framework.TestCase.fail +import static org.gradle.testkit.runner.TaskOutcome.FAILED + +import org.gradle.testkit.runner.BuildResult + +/** + * Base class for functional tests that requires a running FN server + * + * @author John Ahlroos + * @since 1.0 + */ +class FunctionalServerTest extends FunctionalTest { + + protected void setup() { + BuildResult result = run('fnStart') + if (result.task(':fnStart').outcome == FAILED) { + fail('Failed to start FN server') + } else { + println 'Successfully started FN server!' + } + } + + protected void cleanup() { + BuildResult result = run('fnStop') + if (result.task(':fnStop').outcome == FAILED) { + fail('Failed to stio FN server') + } else { + println 'Successfully stopped FN server!' + } + } +} diff --git a/src/functionalTest/groovy/com/devsoap/fn/FunctionalTest.groovy b/src/functionalTest/groovy/com/devsoap/fn/FunctionalTest.groovy new file mode 100644 index 0000000..f3ff48e --- /dev/null +++ b/src/functionalTest/groovy/com/devsoap/fn/FunctionalTest.groovy @@ -0,0 +1,143 @@ +/* + * Copyright 2018-2019 Devsoap Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.devsoap.fn + +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.GradleRunner +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import spock.lang.Specification + +import java.nio.file.Paths +import java.util.concurrent.TimeUnit + +/** + * Base class for functional tests + * + * @author John Ahlroos + * @since 1.0 + */ +class FunctionalTest extends Specification { + + static final String PLUGIN_ID = 'com.devsoap.fn' + + @Rule + protected TemporaryFolder testProjectDir + + protected File buildFile + + protected File settingsFile + + private long testStart + + protected Map extraPlugins + + /** + * Sets up the test + */ + protected void setup() { + extraPlugins = [:] + initBuildFile() + initSettingsFile() + testStart = System.currentTimeMillis() + println "Running test in ${testProjectDir.root}" + } + + /** + * Set additional plugins for the test. + * + * @param plugins + * the plugins to add besides the Vaadin plugin + */ + protected void setExtraPlugins(Map plugins) { + extraPlugins = plugins + buildFile.delete() + initBuildFile() + } + + /** + * Cleans up the test + */ + protected void cleanup() { + long ms = System.currentTimeMillis() - testStart + println "Test took ${TimeUnit.MILLISECONDS.toSeconds(ms)} seconds." + } + + /** + * Runs the project + * + * @param args + * the command line arguments to pass to gradle + * @return + * the result of the build + */ + protected BuildResult run(ConfigureRunner config = { }, String... args) { + GradleRunner runner = GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments(['--stacktrace', '--info'] + (args as List)) + .withPluginClasspath() + config.run(runner) + println "Running gradle ${runner.arguments.join(' ')}" + runner.build() + } + + /** + * Runs the project and is expected to fail + * + * @param args + * the command line arguments to pass to gradle + * @return + * the result of the build + */ + protected BuildResult runAndFail(ConfigureRunner config = { }, String... args) { + GradleRunner runner = GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments(['--stacktrace', '--info'] + (args as List)) + .withPluginClasspath() + config.run(runner) + println "Running gradle ${runner.arguments.join(' ')}" + runner.buildAndFail() + } + + private void initBuildFile() { + String gradlePluginDirectory = Paths.get('.', 'build', 'libs').toFile().canonicalPath + buildFile = testProjectDir.newFile('build.gradle') + buildFile << """ + plugins { + id '$PLUGIN_ID' + ${extraPlugins.collect { it.value ? "id '$it.key' version '$it.value'" : "id '$it.key'" }.join('\n')} + } + + repositories { + flatDir dirs: "$gradlePluginDirectory" + } + + """.stripIndent() + } + + private void initSettingsFile() { + settingsFile = testProjectDir.newFile('settings.gradle') + } + + /** + * Interface representing a GradleRunner configuration closure + */ + @FunctionalInterface + interface ConfigureRunner { + void run(GradleRunner runner) + } + +} diff --git a/src/functionalTest/resources/AllTestsConfig.groovy b/src/functionalTest/resources/AllTestsConfig.groovy new file mode 100644 index 0000000..c72d7f6 --- /dev/null +++ b/src/functionalTest/resources/AllTestsConfig.groovy @@ -0,0 +1,21 @@ +/* + * Copyright 2018-2019 Devsoap Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.devsoap.fn.FunctionalTest + +runner { + include FunctionalTest +} diff --git a/src/main/groovy/com/devsoap/fn/tasks/FnDeployTask.groovy b/src/main/groovy/com/devsoap/fn/tasks/FnDeployTask.groovy index 9fbb605..c2fcdae 100644 --- a/src/main/groovy/com/devsoap/fn/tasks/FnDeployTask.groovy +++ b/src/main/groovy/com/devsoap/fn/tasks/FnDeployTask.groovy @@ -29,7 +29,6 @@ import org.gradle.api.tasks.InputDirectory import org.gradle.api.tasks.Optional import org.gradle.process.internal.ExecException -import java.util.concurrent.TimeUnit import java.util.logging.Level /** diff --git a/src/main/groovy/com/devsoap/fn/tasks/FnInstallCliTask.groovy b/src/main/groovy/com/devsoap/fn/tasks/FnInstallCliTask.groovy index cf16031..916bd18 100644 --- a/src/main/groovy/com/devsoap/fn/tasks/FnInstallCliTask.groovy +++ b/src/main/groovy/com/devsoap/fn/tasks/FnInstallCliTask.groovy @@ -16,6 +16,7 @@ package com.devsoap.fn.tasks import com.devsoap.fn.util.FnUtils +import com.devsoap.fn.util.Versions import groovy.json.JsonSlurper import org.apache.tools.ant.taskdefs.condition.Os import org.gradle.api.DefaultTask @@ -54,8 +55,16 @@ class FnInstallCliTask extends DefaultTask { @TaskAction void install() { logger.info('Fetching latest version tag of FN CLI...') - Object latest = new JsonSlurper().parseText(RELEASES_API.toURL().text) - String tag = latest['tag_name'] + String tag + try { + Object latest = new JsonSlurper().parseText(RELEASES_API.toURL().text) + tag = latest['tag_name'] + } catch (IOException e) { + // Request to Github API failed, maybe rate-limited. Fallback to a known working version + tag = Versions.rawVersion('fn.cli.fallback.version') + logger.warn("Failed to retrieve latest FN CLI version from $RELEASES_API, falling back to $tag") + logger.debug('', e) + } URL downloadLink if (Os.isFamily(Os.FAMILY_MAC)) { @@ -71,10 +80,14 @@ class FnInstallCliTask extends DefaultTask { fnExecutable.parentFile.mkdirs() logger.info("Downloading FN CLI from $downloadLink...") - Channels.newChannel(downloadLink.openStream()).withCloseable { rbc -> - new FileOutputStream(fnExecutable).withStream { fos -> - fos.channel.transferFrom(rbc, 0L, Long.MAX_VALUE) + try { + Channels.newChannel(downloadLink.openStream()).withCloseable { rbc -> + new FileOutputStream(fnExecutable).withStream { fos -> + fos.channel.transferFrom(rbc, 0L, Long.MAX_VALUE) + } } + } catch (IOException e) { + throw new GradleException("Failed to download FN CLI from $downloadLink", e) } fnExecutable.setExecutable(true, true) diff --git a/src/main/groovy/com/devsoap/fn/tasks/FnPrepareDockerTask.groovy b/src/main/groovy/com/devsoap/fn/tasks/FnPrepareDockerTask.groovy index b6885fa..77e91bc 100644 --- a/src/main/groovy/com/devsoap/fn/tasks/FnPrepareDockerTask.groovy +++ b/src/main/groovy/com/devsoap/fn/tasks/FnPrepareDockerTask.groovy @@ -46,6 +46,7 @@ class FnPrepareDockerTask extends DefaultTask { private static final String EOL = '\n' private static final String DOCKER_APP_PATH = '/function/app/' private static final String LIBS_FOLDER = 'libs' + private static final int MAX_TRIGGER_LENGTH = 22 /* * Use a custom function.yaml file @@ -300,7 +301,8 @@ class FnPrepareDockerTask extends DefaultTask { * Get the trigger name */ String getTriggerName() { - String defaultName = getFunctionName().length() >= 22 ? project.name : getFunctionName() + String defaultName = getFunctionName().length() >= MAX_TRIGGER_LENGTH ? + project.name[0..MAX_TRIGGER_LENGTH] : getFunctionName() this.triggerName.getOrElse("$defaultName-tgr") } diff --git a/src/main/groovy/com/devsoap/fn/tasks/FnStartServerTask.groovy b/src/main/groovy/com/devsoap/fn/tasks/FnStartServerTask.groovy index 492a795..cca551f 100644 --- a/src/main/groovy/com/devsoap/fn/tasks/FnStartServerTask.groovy +++ b/src/main/groovy/com/devsoap/fn/tasks/FnStartServerTask.groovy @@ -18,6 +18,7 @@ package com.devsoap.fn.tasks import com.devsoap.fn.util.DockerUtil import com.devsoap.fn.util.FnUtils import com.devsoap.fn.util.LogUtils +import org.gradle.api.GradleException import org.gradle.api.tasks.Exec import java.util.logging.Level @@ -32,10 +33,12 @@ class FnStartServerTask extends Exec { static String NAME = 'fnStart' + private static final String FNSERVER = 'fnserver' + FnStartServerTask() { dependsOn FnInstallCliTask.NAME onlyIf { - !DockerUtil.isContainerRunning(project, 'fnserver') + !DockerUtil.isContainerRunning(project, FNSERVER) } description = 'Starts the local FN Server' group = 'fn' @@ -43,5 +46,11 @@ class FnStartServerTask extends Exec { args 'start', '-d' standardOutput = LogUtils.getLogOutputStream(Level.INFO) errorOutput = LogUtils.getLogOutputStream(Level.SEVERE) + doLast { + while (!DockerUtil.isContainerRunning(project, FNSERVER)) { + logger.info('Waiting for fn server to start...') + Thread.sleep(3000) + } + } } } diff --git a/src/main/groovy/com/devsoap/fn/tasks/FnStopServerTask.groovy b/src/main/groovy/com/devsoap/fn/tasks/FnStopServerTask.groovy index f1785ba..f718fa1 100644 --- a/src/main/groovy/com/devsoap/fn/tasks/FnStopServerTask.groovy +++ b/src/main/groovy/com/devsoap/fn/tasks/FnStopServerTask.groovy @@ -32,10 +32,12 @@ class FnStopServerTask extends Exec { static String NAME = 'fnStop' + private static final String FNSERVER = 'fnserver' + FnStopServerTask() { dependsOn FnInstallCliTask.NAME onlyIf { - DockerUtil.isContainerRunning(project, 'fnserver') + DockerUtil.isContainerRunning(project, FNSERVER) } description = 'Stops the local FN Server' group = 'fn' @@ -43,5 +45,11 @@ class FnStopServerTask extends Exec { args 'stop' standardOutput = LogUtils.getLogOutputStream(Level.INFO) errorOutput = LogUtils.getLogOutputStream(Level.SEVERE) + doLast { + while (DockerUtil.isContainerRunning(project, FNSERVER)) { + logger.info('Waiting for fn server to stop...') + Thread.sleep(3000) + } + } } } diff --git a/src/main/groovy/com/devsoap/fn/util/DockerUtil.groovy b/src/main/groovy/com/devsoap/fn/util/DockerUtil.groovy index cb417d0..900b42a 100644 --- a/src/main/groovy/com/devsoap/fn/util/DockerUtil.groovy +++ b/src/main/groovy/com/devsoap/fn/util/DockerUtil.groovy @@ -15,6 +15,9 @@ */ package com.devsoap.fn.util +import groovy.json.StringEscapeUtils +import org.apache.tools.ant.util.StringUtils +import org.codehaus.groovy.util.StringUtil import org.gradle.api.Project import org.gradle.process.internal.ExecException @@ -52,6 +55,6 @@ class DockerUtil { }.rethrowFailure() } String out = new String(propertyStream.toByteArray(), StandardCharsets.UTF_8) - out[1..-2] // Unquote + out.trim().replace("'", '') } } diff --git a/src/main/resources/versions.properties b/src/main/resources/versions.properties index 3bd0cf1..0d54d07 100644 --- a/src/main/resources/versions.properties +++ b/src/main/resources/versions.properties @@ -2,4 +2,5 @@ fn.plugin.version=@version@ fn.java.fdk.version=1.0.83 fn.java.fdk.baseimage.version=jre11-1.0.83 fn.java.flow.version=1.0.83 +fn.cli.fallback.version=0.5.63 groovy.version=2.5.5 \ No newline at end of file