From 9f3afdc8fb14dc76f5911f8d7e2ba96f2378720b Mon Sep 17 00:00:00 2001 From: Haroon Sheikh Date: Sat, 28 Oct 2023 18:32:24 +0100 Subject: [PATCH] Upgrading plugin to use latest Gradle Apis (#80) --- .github/dependabot.yml | 10 + .github/workflows/build.yml | 34 ++- .github/workflows/release.yml | 18 +- .gitignore | 16 +- Readme.md => README.md | 106 ++++--- build.gradle | 16 - gradle.properties | 3 + gradle/wrapper/gradle-wrapper.jar | Bin 58694 -> 63721 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 282 +++++++++++------- gradlew.bat | 35 +-- plugin.properties | 12 - plugin/build.gradle | 86 ++++-- plugin/gradle.properties | 1 + plugin/gradlePortal.gradle | 29 -- plugin/settings.gradle | 3 +- .../java/org/gauge/gradle/ApplyTest.java | 23 ++ .../java/org/gauge/gradle/Base.java | 64 ++++ .../java/org/gauge/gradle/ClasspathTest.java | 29 ++ .../java/org/gauge/gradle/RunTest.java | 200 +++++++++++++ .../java/org/gauge/gradle/UpToDateTest.java | 49 +++ .../java/org/gauge/gradle/ValidateTest.java | 36 +++ .../testProjects/project1/.gitignore | 8 + .../project1/env/default/default.properties | 17 ++ .../project1/env/default/java.properties | 16 + .../project1/env/dev/dev.properties | 1 + .../testProjects/project1/libs/.gitkeep | 0 .../testProjects/project1/manifest.json | 6 + .../project1/multipleSpecs/example1.spec | 19 ++ .../project1/multipleSpecs/example2.spec | 19 ++ .../project1/multipleSpecs/example3.spec | 19 ++ .../project1/multipleSpecs/example4.spec | 6 + .../testProjects/project1/specs/example.spec | 19 ++ .../src/test/java/StepImplementation.java | 48 +++ .../gauge/gradle/ClasspathTask.java | 26 -- .../gauge/gradle/GaugeExtension.java | 83 ------ .../gauge/gradle/GaugePlugin.java | 23 -- .../thoughtworks/gauge/gradle/GaugeTask.java | 58 ---- .../GaugeExecutionFailedException.java | 19 -- .../gradle/util/ProcessBuilderFactory.java | 138 --------- .../gauge/gradle/util/PropertyManager.java | 118 -------- .../thoughtworks/gauge/gradle/util/Util.java | 28 -- .../org/gauge/gradle/GaugeClasspathTask.java | 23 ++ .../java/org/gauge/gradle/GaugeCommand.java | 120 ++++++++ .../java/org/gauge/gradle/GaugeConstants.java | 12 + .../java/org/gauge/gradle/GaugeExtension.java | 61 ++++ .../java/org/gauge/gradle/GaugePlugin.java | 24 ++ .../java/org/gauge/gradle/GaugeProperty.java | 30 ++ .../main/java/org/gauge/gradle/GaugeTask.java | 45 +++ .../org/gauge/gradle/GaugeValidateTask.java | 35 +++ .../com.thoughtworks.gauge.properties | 1 - .../META-INF/gradle-plugins/gauge.properties | 1 - .../gauge/gradle/GaugeExtensionTest.java | 28 -- .../gauge/gradle/GaugePluginTest.java | 44 --- .../gauge/gradle/GaugeTaskTest.java | 87 ------ .../gradle/util/PropertyManagerTest.java | 69 ----- .../org/gauge/gradle/GaugeCommandTest.java | 132 ++++++++ .../org/gauge/gradle/GaugeExtensionTest.java | 62 ++++ .../org/gauge/gradle/GaugePluginTest.java | 52 ++++ sample/build.gradle | 72 +++-- sample/env/ci/user.properties | 16 - sample/env/default/default.properties | 2 +- sample/env/dev/user.properties | 6 +- sample/env/test/user.properties | 16 - sample/settings.gradle | 7 + sample/specs/hello_world1.spec | 25 +- sample/specs/hello_world2.spec | 25 +- sample/specs/hello_world3.spec | 25 +- sample/specs/hello_world4.spec | 25 +- sample/specs/specific/hello_world.spec | 27 +- .../gauge/gradle/sample/Main.java | 8 - .../gauge/gradle/sample/Test.java | 13 - .../java/org/gauge/gradle/sample/Sample.java | 13 + .../gauge/gradle/sample/SampleSteps.java} | 14 +- scripts/pre_build.sh | 5 - scripts/test.sh | 3 - settings.gradle | 10 +- 77 files changed, 1676 insertions(+), 1189 deletions(-) create mode 100644 .github/dependabot.yml rename Readme.md => README.md (63%) create mode 100644 gradle.properties delete mode 100644 plugin.properties create mode 100644 plugin/gradle.properties delete mode 100644 plugin/gradlePortal.gradle create mode 100644 plugin/src/integrationTest/java/org/gauge/gradle/ApplyTest.java create mode 100644 plugin/src/integrationTest/java/org/gauge/gradle/Base.java create mode 100644 plugin/src/integrationTest/java/org/gauge/gradle/ClasspathTest.java create mode 100644 plugin/src/integrationTest/java/org/gauge/gradle/RunTest.java create mode 100644 plugin/src/integrationTest/java/org/gauge/gradle/UpToDateTest.java create mode 100644 plugin/src/integrationTest/java/org/gauge/gradle/ValidateTest.java create mode 100644 plugin/src/integrationTest/resources/testProjects/project1/.gitignore create mode 100644 plugin/src/integrationTest/resources/testProjects/project1/env/default/default.properties create mode 100644 plugin/src/integrationTest/resources/testProjects/project1/env/default/java.properties create mode 100644 plugin/src/integrationTest/resources/testProjects/project1/env/dev/dev.properties create mode 100644 plugin/src/integrationTest/resources/testProjects/project1/libs/.gitkeep create mode 100644 plugin/src/integrationTest/resources/testProjects/project1/manifest.json create mode 100644 plugin/src/integrationTest/resources/testProjects/project1/multipleSpecs/example1.spec create mode 100644 plugin/src/integrationTest/resources/testProjects/project1/multipleSpecs/example2.spec create mode 100644 plugin/src/integrationTest/resources/testProjects/project1/multipleSpecs/example3.spec create mode 100644 plugin/src/integrationTest/resources/testProjects/project1/multipleSpecs/example4.spec create mode 100644 plugin/src/integrationTest/resources/testProjects/project1/specs/example.spec create mode 100644 plugin/src/integrationTest/resources/testProjects/project1/src/test/java/StepImplementation.java delete mode 100644 plugin/src/main/java/com/thoughtworks/gauge/gradle/ClasspathTask.java delete mode 100644 plugin/src/main/java/com/thoughtworks/gauge/gradle/GaugeExtension.java delete mode 100644 plugin/src/main/java/com/thoughtworks/gauge/gradle/GaugePlugin.java delete mode 100644 plugin/src/main/java/com/thoughtworks/gauge/gradle/GaugeTask.java delete mode 100644 plugin/src/main/java/com/thoughtworks/gauge/gradle/exception/GaugeExecutionFailedException.java delete mode 100644 plugin/src/main/java/com/thoughtworks/gauge/gradle/util/ProcessBuilderFactory.java delete mode 100644 plugin/src/main/java/com/thoughtworks/gauge/gradle/util/PropertyManager.java delete mode 100644 plugin/src/main/java/com/thoughtworks/gauge/gradle/util/Util.java create mode 100644 plugin/src/main/java/org/gauge/gradle/GaugeClasspathTask.java create mode 100644 plugin/src/main/java/org/gauge/gradle/GaugeCommand.java create mode 100644 plugin/src/main/java/org/gauge/gradle/GaugeConstants.java create mode 100644 plugin/src/main/java/org/gauge/gradle/GaugeExtension.java create mode 100644 plugin/src/main/java/org/gauge/gradle/GaugePlugin.java create mode 100644 plugin/src/main/java/org/gauge/gradle/GaugeProperty.java create mode 100644 plugin/src/main/java/org/gauge/gradle/GaugeTask.java create mode 100644 plugin/src/main/java/org/gauge/gradle/GaugeValidateTask.java delete mode 100644 plugin/src/main/resources/META-INF/gradle-plugins/com.thoughtworks.gauge.properties delete mode 100644 plugin/src/main/resources/META-INF/gradle-plugins/gauge.properties delete mode 100644 plugin/src/test/java/com/thoughtworks/gauge/gradle/GaugeExtensionTest.java delete mode 100644 plugin/src/test/java/com/thoughtworks/gauge/gradle/GaugePluginTest.java delete mode 100644 plugin/src/test/java/com/thoughtworks/gauge/gradle/GaugeTaskTest.java delete mode 100644 plugin/src/test/java/com/thoughtworks/gauge/gradle/util/PropertyManagerTest.java create mode 100644 plugin/src/test/java/org/gauge/gradle/GaugeCommandTest.java create mode 100644 plugin/src/test/java/org/gauge/gradle/GaugeExtensionTest.java create mode 100644 plugin/src/test/java/org/gauge/gradle/GaugePluginTest.java delete mode 100644 sample/env/ci/user.properties delete mode 100644 sample/env/test/user.properties create mode 100644 sample/settings.gradle delete mode 100644 sample/src/spec/java/com/thoughtworks/gauge/gradle/sample/Main.java delete mode 100644 sample/src/spec/java/com/thoughtworks/gauge/gradle/sample/Test.java create mode 100644 sample/src/spec/java/org/gauge/gradle/sample/Sample.java rename sample/src/test/java/{com/thoughtworks/gauge/gradle/sample/StepImplementation.java => org/gauge/gradle/sample/SampleSteps.java} (65%) delete mode 100644 scripts/pre_build.sh delete mode 100644 scripts/test.sh diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..354ca9c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: monthly + - package-ecosystem: maven + directory: "plugin/" + schedule: + interval: monthly \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 81f45c4..4f9b05d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,26 +1,38 @@ name: Build -on: [push, pull_request] +on: + push: + branches: + - master + pull_request: + branches: + - master jobs: build: - runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest] - steps: - - uses: actions/checkout@v1 - - name: Set up JDK - uses: actions/setup-java@v1 + - uses: actions/checkout@v4 + - uses: actions/setup-java@v3 with: - java-version: '12.x.x' - + distribution: 'temurin' + java-version: '11' + - uses: getgauge/setup-gauge@master + with: + gauge-plugins: java, html-report, xml-report + - uses: gradle/gradle-build-action@v2 + with: + gradle-version: '8.2' - name: Build with Gradle on ubuntu if: matrix.os != 'windows-latest' - run: ./gradlew clean build - + run: | + ./gradlew plugin:build + ./gradlew gaugeValidate gaugeDevRepeat - name: Build with Gradle on windows if: matrix.os == 'windows-latest' - run: ./gradlew.bat clean build + run: | + .\gradlew.bat plugin:build + .\gradlew.bat gaugeDevRepeat \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c8e315d..a9980c8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,16 +12,16 @@ jobs: GRADLE_PUBLISHKEY: '${{ secrets.GRADLE_PUBLISHKEY }}' GRADLE_SECRET: '${{ secrets.GRADLE_SECRET }}' steps: - - uses: actions/checkout@v1 - - name: Set up JDK - uses: actions/setup-java@v1 + - uses: actions/checkout@v4 + - uses: actions/setup-java@v3 with: - java-version: '12.x.x' - + distribution: 'temurin' + java-version: '11' + - uses: gradle/gradle-build-action@v2 + with: + gradle-version: '8.2' - name: Build artifacts - run: | - ./gradlew clean build - + run: ./gradlew plugin:build - name: Upload to gradle portal run: | - ./gradlew publishPlugins -Pgradle.publish.key=$GRADLE_PUBLISHKEY -Pgradle.publish.secret=$GRADLE_SECRET \ No newline at end of file + ./gradlew plugin:publishPlugins -Pgradle.publish.key=$GRADLE_PUBLISHKEY -Pgradle.publish.secret=$GRADLE_SECRET \ No newline at end of file diff --git a/.gitignore b/.gitignore index 11f2b82..09b745d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,18 +5,20 @@ build/ report* log* repo -sample/libs -sample/report* -sample/log* -sample/.gauge -local.properties -plugin/out -sample/out bin/ **/build/ .java-version .classpath .project + +sample/libs +sample/reports +sample/logs +sample/.gauge +local.properties +plugin/out +sample/out + .settings/org.eclipse.buildship.core.prefs plugin/.settings/org.eclipse.buildship.core.prefs sample/.settings/org.eclipse.buildship.core.prefs diff --git a/Readme.md b/README.md similarity index 63% rename from Readme.md rename to README.md index 116e200..d554ee5 100644 --- a/Readme.md +++ b/README.md @@ -17,26 +17,21 @@ Maven Central & Bintray; with out-of-date versions available on the Gradle Plugi You can use this plugin on a new project via a Gauge [project template](https://docs.gauge.org/latest/installation.html#project-templates): -``` +```bash gauge init java_gradle ``` ### Using the plugins DSL -If you have an existing project and you would like to add the plugin manually you can add it like the below +If you have an existing project, and you would like to add the plugin manually you can add it like the below -````groovy +```groovy plugins { id 'java' - id 'org.gauge' version '1.8.0' + id 'org.gauge' version '2.0.0' } -group = 'my-gauge-tests' -version = '1.0-SNAPSHOT' - -description = "My Gauge Tests" - repositories { mavenCentral() } @@ -55,33 +50,28 @@ gauge { additionalFlags = '--verbose' gaugeRoot = '/opt/gauge' } -```` +``` ### Using legacy plugin 'apply' style -* apply plugin `gauge` * update the `buildscript` to add the Gradle plugins repo and classpath +* apply plugin `org.gauge` -````groovy -apply plugin: 'java' -apply plugin: 'gauge' - -group = "my-gauge-tests" -version = "1.0-SNAPSHOT" - -description = "My Gauge Tests" - +```groovy buildscript { repositories { maven { - url "https://plugins.gradle.org/m2/" + url = uri("https://plugins.gradle.org/m2/") } } dependencies { - classpath "gradle.plugin.org.gauge.gradle:gauge-gradle-plugin:1.8.0" + classpath("org.gauge.gradle:gauge-gradle-plugin:2.0.0") } } +apply plugin: 'java' +apply plugin: 'org.gauge' + repositories { mavenCentral() } @@ -97,32 +87,46 @@ gauge { nodes = 2 env = 'dev' tags = 'tag1' - additionalFlags = '--verbose' + additionalFlags = '--simple-console --verbose' gaugeRoot = '/opt/gauge' + // additional environment variables to pass onto the gauge process + environmentVariables = ["gauge_reports_dir": "custom/reports/", "logs_directory": "custom/logs/"] } - -```` +``` ## Usage +### Validating + +```bash +gradle gaugeValidate +``` + ### Running -```` + +```bash gradle gauge -```` -#### Execute list of specs ``` + +#### Execute list of specs + +```bash gradle gauge -PspecsDir="specs/first.spec specs/second.spec" ``` + #### Execute specs in parallel -``` + +```bash gradle gauge -PinParallel=true -PspecsDir=specs ``` #### Execute specs by tags -``` + +```bash gradle gauge -Ptags="!in-progress" -PspecsDir=specs ``` #### Specifying execution environment -``` + +```bash gradle gauge -Penv="dev" -PspecsDir=specs ``` @@ -131,20 +135,22 @@ Note : Pass specsDir parameter as the last one. ### All additional Properties The following plugin properties can be additionally set: -|Property name|Usage|Description| -|-------------|-----|-----------| -|specsDir| -PspecsDir=specs| Gauge specs directory path. Required for executing specs| -|tags | -Ptags="tag1 & tag2" |Filter specs by specified tags expression| -|inParallel| -PinParallel=true | Execute specs in parallel| -|nodes | -Pnodes=3 | Number of parallel execution streams. Use with ```parallel```| -|env | -Penv=qa | gauge env to run against | -|additionalFlags| -PadditionalFlags="--verbose" | Add additional gauge flags to execution| -|gaugeRoot| -PgaugeRoot="/opt/gauge" | Path to gauge installation root| +| Property name | Usage | Description | +|-----------------|--------------------------------|---------------------------------------------------------------| +| specsDir | -PspecsDir=specs | Gauge specs directory path. Required for executing specs | +| tags | -Ptags="tag1 & tag2" | Filter specs by specified tags expression | +| inParallel | -PinParallel=true | Execute specs in parallel | +| nodes | -Pnodes=3 | Number of parallel execution streams. Use with ```parallel``` | +| env | -Penv=qa | gauge env to run against | +| additionalFlags | -PadditionalFlags="--verbose" | Add additional gauge flags to execution separated by space | +| dir | -Pdir="/path/to/gauge/project" | Path to gauge project directory | +| gaugeRoot | -PgaugeRoot="/opt/gauge" | Path to gauge installation root | ### Adding/configuring custom Gauge tasks It is possible to define new custom Gauge tasks specific for different environments. For example, -````groovy +```groovy +import org.gauge.gradle.GaugeTask task gaugeDev(type: GaugeTask) { doFirst { @@ -153,7 +159,7 @@ task gaugeDev(type: GaugeTask) { inParallel = true nodes = 2 env = 'dev' - additionalFlags = '--verbose' + additionalFlags = '--simple-console --verbose' } } } @@ -165,23 +171,25 @@ task gaugeTest(type: GaugeTask) { inParallel = true nodes = 4 env = 'test' - additionalFlags = '--verbose' + additionalFlags = '--simple-console --verbose' } } } -```` +``` ### Running gauge task with source code of gradle plugin run the gauge command with - -```` + +```bash gradle gauge --include-build {PATH_TO_GRADLE_PLUGIN} -```` +``` or add this property in `settings.gradle` -```` + +```bash includeBuild {PATH_TO_GRADLE_PLUGIN} -```` +``` ## License -Gauge is released under the Apache License, Version 2.0. See [LICENSE](LICENSE) for the full license text. +Gauge is released under the Apache License, Version 2.0. See [LICENSE](LICENSE.txt) for the full license text. \ No newline at end of file diff --git a/build.gradle b/build.gradle index 6935c02..e69de29 100644 --- a/build.gradle +++ b/build.gradle @@ -1,16 +0,0 @@ -plugins { - id "java" - id "idea" -} - -sourceCompatibility = 1.8 -targetCompatibility = 1.8 - -repositories { - mavenCentral() -} - -Properties plugin = new Properties() -plugin.load(project.file('./plugin.properties').newDataInputStream()) -group plugin.getProperty("groupId") -version = plugin.getProperty("version") \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..8d3cf05 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.configureondemand=true +org.gradle.daemon=false +org.gradle.console=plain \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 490fda8577df6c95960ba7077c43220e5bb2c0d9..7f93135c49b765f8051ef9d0a6055ff8e46073d8 100644 GIT binary patch delta 44832 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 agh8O80uQ~P|J4;iKprmiHhuWlfBz3&BC9R{ delta 39743 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 I0x&TC2akg(od5s; diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6623300..62f495d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 2fe81a7..0adc8e1 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,78 +17,111 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -97,87 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 9109989..6689b85 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -54,7 +55,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -64,38 +65,26 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/plugin.properties b/plugin.properties deleted file mode 100644 index a1c9892..0000000 --- a/plugin.properties +++ /dev/null @@ -1,12 +0,0 @@ -siteUrl=https://github.com/getgauge/gauge-gradle-plugin -gitUrl=https://github.com/getgauge/gauge-gradle-plugin.git - -version=1.8.2 -name=gauge-gradle-plugin -displayName=Gauge -description=Gradle plugin for Gauge, the open source test automation tool developed by ThoughtWorks. -versionDescription=Remove specsDir if --failed or --repeat flags are given - -groupId=org.gauge.gradle -pluginKey=org.gauge -artifactId=gauge-gradle-plugin diff --git a/plugin/build.gradle b/plugin/build.gradle index fb5df67..05a42b0 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -1,28 +1,76 @@ plugins { - id 'java' - id 'groovy' - id 'maven' - id "java-gradle-plugin" - id "com.gradle.plugin-publish" version "0.12.0" + id 'java-gradle-plugin' + id 'jvm-test-suite' + id 'com.gradle.plugin-publish' version '1.2.1' + id 'pmd' } -sourceCompatibility = 1.8 -targetCompatibility = 1.8 +repositories { + mavenLocal() + mavenCentral() +} -dependencies { - implementation gradleApi() - testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-core:3.7.7' +group = 'org.gauge.gradle' + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } -repositories { - mavenCentral() - jcenter() +pmd { + consoleOutput = true } -apply from: 'gradlePortal.gradle' +testing { + suites { + configureEach { + useJUnitJupiter() + dependencies { + implementation "org.junit.jupiter:junit-jupiter:5.+" + runtimeOnly "org.junit.platform:junit-platform-launcher" + } + } + integrationTest(JvmTestSuite) { + dependencies { + implementation project() + implementation 'commons-io:commons-io:2.15.+' + implementation 'org.hamcrest:hamcrest:2.2' + } + targets { + all { + testTask.configure { + shouldRunAfter(test) + } + } + } + } + } +} + +tasks.withType(Test).configureEach { + testLogging { + showStandardStreams = true + showStackTraces = true + exceptionFormat = 'full' + events = ["passed", "failed", "skipped"] + } +} + +tasks.named('check') { + dependsOn(testing.suites.named("integrationTest")) +} -Properties plugin = new Properties() -plugin.load(project.file('../plugin.properties').newDataInputStream()) -group plugin.getProperty("groupId") -version = plugin.getProperty("version") +gradlePlugin { + testSourceSets(sourceSets.integrationTest) + plugins { + vcsUrl = "https://github.com/getgauge/gauge-gradle-plugin.git" + website = "https://github.com/getgauge/gauge-gradle-plugin" + gauge { + id = "org.gauge" + implementationClass = "org.gauge.gradle.GaugePlugin" + displayName = "Gauge" + description = "Gradle plugin for Gauge, the open source test automation tool developed by ThoughtWorks." + tags.addAll('gauge', 'thoughtworks', 'testing') + } + } +} \ No newline at end of file diff --git a/plugin/gradle.properties b/plugin/gradle.properties new file mode 100644 index 0000000..e997a9a --- /dev/null +++ b/plugin/gradle.properties @@ -0,0 +1 @@ +version=2.0.0 \ No newline at end of file diff --git a/plugin/gradlePortal.gradle b/plugin/gradlePortal.gradle deleted file mode 100644 index 5ac2556..0000000 --- a/plugin/gradlePortal.gradle +++ /dev/null @@ -1,29 +0,0 @@ -Properties plugin = new Properties() -plugin.load(project.file('../plugin.properties').newDataInputStream()) - -gradlePlugin { - plugins { - gaugePlugin { - id = plugin.getProperty("pluginKey") - implementationClass = 'com.thoughtworks.gauge.gradle.GaugePlugin' - } - } -} - -pluginBundle { - website = plugin.getProperty("siteUrl") - vcsUrl = plugin.getProperty("gitUrl") - description = plugin.getProperty("description") - tags = ['gauge', 'thoughtworks'] - - plugins { - gaugePlugin { - displayName = plugin.getProperty("displayName") - } - } - - mavenCoordinates { - groupId = group - artifactId = plugin.getProperty("artifactId") - } -} \ No newline at end of file diff --git a/plugin/settings.gradle b/plugin/settings.gradle index d736502..fe18a1d 100644 --- a/plugin/settings.gradle +++ b/plugin/settings.gradle @@ -1,2 +1 @@ -rootProject.name = 'gauge-gradle-plugin' - +rootProject.name = "gauge-gradle-plugin" diff --git a/plugin/src/integrationTest/java/org/gauge/gradle/ApplyTest.java b/plugin/src/integrationTest/java/org/gauge/gradle/ApplyTest.java new file mode 100644 index 0000000..2e1b3e2 --- /dev/null +++ b/plugin/src/integrationTest/java/org/gauge/gradle/ApplyTest.java @@ -0,0 +1,23 @@ +package org.gauge.gradle; + +import org.gradle.testkit.runner.BuildResult; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.gauge.gradle.GaugeConstants.GAUGE_TASK; +import static org.gradle.testkit.runner.TaskOutcome.NO_SOURCE; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ApplyTest extends Base { + + @Test + void testCanApplyPlugin() throws IOException { + // Given plugin is applied + writeFile(buildFile, getApplyPluginsBlock()); + // Then I should be able to run the gauge task + BuildResult result = defaultGradleRunner().withArguments(GAUGE_TASK).build(); + assertEquals(NO_SOURCE, result.task(GAUGE_TASK_PATH).getOutcome()); + } + +} diff --git a/plugin/src/integrationTest/java/org/gauge/gradle/Base.java b/plugin/src/integrationTest/java/org/gauge/gradle/Base.java new file mode 100644 index 0000000..c97cfd3 --- /dev/null +++ b/plugin/src/integrationTest/java/org/gauge/gradle/Base.java @@ -0,0 +1,64 @@ +package org.gauge.gradle; + +import org.apache.commons.io.FileUtils; +import org.gradle.testkit.runner.GradleRunner; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.io.TempDir; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Path; + +class Base { + + @TempDir + File primaryProjectDir; + File settingsFile; + File buildFile; + + protected static final String GAUGE_TASK_PATH = ":gauge"; + + @BeforeEach + public void setup() { + settingsFile = new File(primaryProjectDir, "settings.gradle"); + buildFile = new File(primaryProjectDir, "build.gradle"); + } + + protected void writeFile(File destination, String content) throws IOException { + try (BufferedWriter output = new BufferedWriter(new FileWriter(destination))) { + output.write(content); + } + } + + protected void copyGaugeProjectToTemp(final String project) { + copyGaugeProjectToTemp(project, primaryProjectDir); + } + + protected void copyGaugeProjectToTemp(final String project, final File testProjectDir) { + final Path gaugeProjectPath = Path.of("testProjects", project); + try { + final URL gaugeProject = Thread.currentThread().getContextClassLoader().getResource(gaugeProjectPath.toString()); + Assertions.assertNotNull(gaugeProject, "Could not find the gauge project"); + FileUtils.copyDirectory(new File(gaugeProject.toURI()), testProjectDir); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + protected String getApplyPluginsBlock() { + return "plugins {id 'org.gauge'}\n" + + "repositories {mavenLocal()\nmavenCentral()}\n" + + "dependencies {testImplementation 'com.thoughtworks.gauge:gauge-java:+'}\n"; + } + + protected GradleRunner defaultGradleRunner() { + return GradleRunner.create() + .withProjectDir(primaryProjectDir) + .withPluginClasspath(); + } + +} diff --git a/plugin/src/integrationTest/java/org/gauge/gradle/ClasspathTest.java b/plugin/src/integrationTest/java/org/gauge/gradle/ClasspathTest.java new file mode 100644 index 0000000..144f9b8 --- /dev/null +++ b/plugin/src/integrationTest/java/org/gauge/gradle/ClasspathTest.java @@ -0,0 +1,29 @@ +package org.gauge.gradle; + +import org.gradle.testkit.runner.BuildResult; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.gauge.gradle.GaugeConstants.GAUGE_CLASSPATH_TASK; +import static org.gradle.testkit.runner.TaskOutcome.SUCCESS; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ClasspathTest extends Base { + + @Test + void testCanRunGaugeClasspathTaskWithGaugeDependency() throws IOException { + copyGaugeProjectToTemp("project1"); + // Given plugin is applied without gauge extension + // And gauge-java dependency included + writeFile(buildFile, getApplyPluginsBlock()); + // Then I should be able to run the classpath task + BuildResult result = defaultGradleRunner().withArguments(GAUGE_CLASSPATH_TASK).build(); + assertEquals(SUCCESS, result.task(":" + GAUGE_CLASSPATH_TASK).getOutcome()); + assertThat(result.getOutput(), containsString("gauge-java")); + assertThat(result.getOutput(), containsString("assertj-core")); + } + +} diff --git a/plugin/src/integrationTest/java/org/gauge/gradle/RunTest.java b/plugin/src/integrationTest/java/org/gauge/gradle/RunTest.java new file mode 100644 index 0000000..a5dc03a --- /dev/null +++ b/plugin/src/integrationTest/java/org/gauge/gradle/RunTest.java @@ -0,0 +1,200 @@ +package org.gauge.gradle; + +import org.gradle.testkit.runner.BuildResult; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; + +import static org.gauge.gradle.GaugeConstants.GAUGE_TASK; +import static org.gradle.testkit.runner.TaskOutcome.FAILED; +import static org.gradle.testkit.runner.TaskOutcome.SUCCESS; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class RunTest extends Base { + + private static final String GAUGE_PROJECT_ONE = "project1"; + + @BeforeEach + public void setUp() { + copyGaugeProjectToTemp(GAUGE_PROJECT_ONE); + } + + @Test + void testCanRunGaugeTasksWithDefaultConfigurations() throws IOException { + // Given plugin is applied + writeFile(buildFile, getApplyPluginsBlock()); + // Then I should be able to run the gauge task + BuildResult result = defaultGradleRunner().withArguments(GAUGE_TASK).build(); + assertEquals(SUCCESS, result.task(GAUGE_TASK_PATH).getOutcome()); + assertThat(result.getOutput(), containsString("Successfully generated html-report")); + } + + @Test + void testCanRunGaugeTestsWhenDirPropertySet() throws IOException { + final File subProject = new File(Path.of(defaultGradleRunner().getProjectDir().getPath(), "subProject").toString()); + copyGaugeProjectToTemp(GAUGE_PROJECT_ONE, subProject); + // Given plugin is applied + writeFile(buildFile, getApplyPluginsBlock()); + // Then I should be able to run the gauge task + BuildResult resultWithDirProperty = defaultGradleRunner().withArguments(GAUGE_TASK, "-Pdir=" + subProject.getAbsolutePath()).build(); + assertEquals(SUCCESS, resultWithDirProperty.task(GAUGE_TASK_PATH).getOutcome()); + } + + @Test + void testCanRunGaugeTestsWhenDirSetInExtension() throws IOException { + final File subProject = new File(Path.of(defaultGradleRunner().getProjectDir().getPath(), "subProject").toString()); + copyGaugeProjectToTemp(GAUGE_PROJECT_ONE, subProject); + // Given plugin is applied + writeFile(buildFile, getApplyPluginsBlock() + "gauge {dir=\"subProject\"}"); + // Then I should be able to run the gauge task + BuildResult resultWithExtensionProperty = defaultGradleRunner().withArguments(GAUGE_TASK).build(); + assertEquals(SUCCESS, resultWithExtensionProperty.task(GAUGE_TASK_PATH).getOutcome()); + } + + @Test + void testCanRunGaugeTestsWhenSpecsDirSet() throws IOException { + // Given plugin is applied + // When specsDir is set in the extension with an invalid/non-existing directory + writeFile(buildFile, getApplyPluginsBlock() + "gauge {specsDir=\"invalid\"}\n"); + // Then I should be able to run the gauge task + BuildResult resultWithExtension = defaultGradleRunner().withArguments(GAUGE_TASK).buildAndFail(); + // And I should get a failure with missing specs directory + assertEquals(FAILED, resultWithExtension.task(GAUGE_TASK_PATH).getOutcome()); + assertThat(resultWithExtension.getOutput(), containsString("Specs directory invalid does not exist.")); + // When specsDir is set to multiple specs directory with one being an invalid/non-existing directory + BuildResult resultWithProperty = defaultGradleRunner().withArguments(GAUGE_TASK, "-PspecsDir=specs specs2").buildAndFail(); + // And I should get a failure with missing specs directory + assertEquals(FAILED, resultWithProperty.task(GAUGE_TASK_PATH).getOutcome()); + assertThat(resultWithProperty.getOutput(), containsString("Specs directory specs2 does not exist.")); + } + + @Test + void testCanRunGaugeTestsWhenEnvVariablesAndAdditionalFlagsSet() throws IOException { + // Given plugin is applied + // When environmentVariables is set in extension + // And additionalFlags include the --verbose flag + writeFile(buildFile, getApplyPluginsBlock() + + "gauge {environmentVariables=['customVariable': 'customValue']\n" + + "additionalFlags='--simple-console --verbose'}\n"); + // Then I should be able to run the gauge task + BuildResult resultWithExtension = defaultGradleRunner().withArguments(GAUGE_TASK).build(); + assertEquals(SUCCESS, resultWithExtension.task(GAUGE_TASK_PATH).getOutcome()); + // And I should see custom environment was set correctly + assertThat(resultWithExtension.getOutput(), containsString("customVariable is set to customValue in build.gradle")); + // And I should see the step names included in console output with --verbose flag set + assertThat(resultWithExtension.getOutput(), containsString("The word \"gauge\" has \"3\" vowels.")); + } + + @Test + void testCanRunGaugeTestsWhenInParallelSet() throws IOException { + // Given plugin is applied + // When inParallel=true is set in extension + // And additionalFlags include the --verbose flag + writeFile(buildFile, getApplyPluginsBlock() + + "gauge {specsDir='specs multipleSpecs'\n" + + "inParallel=true\n" + + "additionalFlags='--simple-console'}\n"); + // Then I should be able to run the gauge task + BuildResult resultWithExtension = defaultGradleRunner().withArguments(GAUGE_TASK).build(); + assertEquals(SUCCESS, resultWithExtension.task(GAUGE_TASK_PATH).getOutcome()); + // And I should see tests running in default parallel streams + assertThat(resultWithExtension.getOutput(), containsString("parallel streams.")); + // And I should see all 4 specifications were executed + assertThat(resultWithExtension.getOutput(), containsString("Specifications:\t4 executed")); + // When nodes=2 project property is set + BuildResult resultWithProperty = defaultGradleRunner().withArguments(GAUGE_TASK, "-Pnodes=2").build(); + assertEquals(SUCCESS, resultWithProperty.task(GAUGE_TASK_PATH).getOutcome()); + // Then I should see tests running in 2 parallel streams + assertThat(resultWithProperty.getOutput(), containsString("Executing in 2 parallel streams.")); + // And I should see all 4 specifications were executed + assertThat(resultWithProperty.getOutput(), containsString("Specifications:\t4 executed")); + } + + @Test + void testCanRunGaugeTestsWhenTagsSet() throws IOException { + // Given plugin is applied + // When inParallel=true is set in extension + // And additionalFlags include the --verbose flag + // And tags=example1 set to run + writeFile(buildFile, getApplyPluginsBlock() + + "gauge {specsDir='specs multipleSpecs'\n" + + "inParallel=true\n" + + "additionalFlags='--simple-console'\n" + + "tags='example1'}"); + // Then I should be able to run the gauge task + BuildResult resultWithExtension = defaultGradleRunner().withArguments(GAUGE_TASK).build(); + assertEquals(SUCCESS, resultWithExtension.task(GAUGE_TASK_PATH).getOutcome()); + // And I should see tests running only with specified tag + assertThat(resultWithExtension.getOutput(), containsString("parallel streams.")); + assertThat(resultWithExtension.getOutput(), containsString("Specifications:\t2 executed")); + // When nodes=2 project property is set + // And tags project property is set to run either scenarios with example1 or example2 tags + BuildResult resultWithProperty = defaultGradleRunner().withArguments(GAUGE_TASK, "-Pnodes=2", "-Ptags=example1|example2").build(); + assertEquals(SUCCESS, resultWithProperty.task(GAUGE_TASK_PATH).getOutcome()); + // Then I should see tests running in 2 parallel streams + assertThat(resultWithProperty.getOutput(), containsString("Executing in 2 parallel streams.")); + // And I should see all matching 3 specifications were executed + assertThat(resultWithProperty.getOutput(), containsString("Specifications:\t3 executed")); + } + + @Test + void testCanRunGaugeTestsWhenEnvSet() throws IOException { + // Given plugin is applied + // When inParallel=true is set in extension + // And additionalFlags include the --verbose flag + // When env is set to invalid/non-existing + writeFile(buildFile, getApplyPluginsBlock() + + "gauge {inParallel=true\n" + + "additionalFlags='--simple-console'\n" + + "env='invalid'}"); + // Then I should be able to run the gauge task + BuildResult resultWithExtension = defaultGradleRunner().withArguments(GAUGE_TASK).buildAndFail(); + assertEquals(FAILED, resultWithExtension.task(GAUGE_TASK_PATH).getOutcome()); + // And I should see environment does not exist error + assertThat(resultWithExtension.getOutput(), containsString("invalid environment does not exist")); + // When env=dev project property is set + BuildResult resultWithProperty = defaultGradleRunner().withArguments(GAUGE_TASK, "-Penv=dev").build(); + assertEquals(SUCCESS, resultWithProperty.task(GAUGE_TASK_PATH).getOutcome()); + // And I should see tests ran against the dev environment + assertThat(resultWithProperty.getOutput(), containsString(getExpectedReportPath("dev"))); + } + + @Test + void testCanRunGaugeTestsWhenRepeatFlagSet() throws IOException { + // Given plugin is applied + // When inParallel=true is set in extension + // And additionalFlags include the --verbose flag + // When env is set to dev + writeFile(buildFile, getApplyPluginsBlock() + + "gauge {inParallel=true\n" + + "additionalFlags='--simple-console'\n" + + "nodes=2\n" + + "env='dev'}"); + // Then I should be able to run the gauge task + BuildResult resultWithExtension = defaultGradleRunner().withArguments(GAUGE_TASK, "--info").build(); + assertEquals(SUCCESS, resultWithExtension.task(GAUGE_TASK_PATH).getOutcome()); + // And I should see environment and parallel flags with specs in the command + assertThat(resultWithExtension.getOutput(), containsString("--simple-console --parallel --n 2 --env dev specs")); + // When additionalFlags include the --repeat flag + BuildResult resultWithProperty = defaultGradleRunner() + .withArguments(GAUGE_TASK, "-PadditionalFlags=--repeat --simple-console", "--info").build(); + assertEquals(SUCCESS, resultWithProperty.task(GAUGE_TASK_PATH).getOutcome()); + // Then I should not see environment and parallel flags and specs include the command + assertThat(resultWithProperty.getOutput(), not(containsString("--parallel --n 2 --env dev specs"))); + // And I should only see repeat and simple-console included + assertThat(resultWithProperty.getOutput(), containsString("--repeat --simple-console")); + // And I should see tests ran against the dev environment + assertThat(resultWithProperty.getOutput(), containsString(getExpectedReportPath("dev"))); + } + + private String getExpectedReportPath(final String env) { + return Path.of("reports", env, "html-report", "index.html").toString(); + } + +} diff --git a/plugin/src/integrationTest/java/org/gauge/gradle/UpToDateTest.java b/plugin/src/integrationTest/java/org/gauge/gradle/UpToDateTest.java new file mode 100644 index 0000000..e526707 --- /dev/null +++ b/plugin/src/integrationTest/java/org/gauge/gradle/UpToDateTest.java @@ -0,0 +1,49 @@ +package org.gauge.gradle; + +import org.gradle.testkit.runner.GradleRunner; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.gauge.gradle.GaugeConstants.GAUGE_CLASSPATH_TASK; +import static org.gauge.gradle.GaugeConstants.GAUGE_TASK; +import static org.gradle.testkit.runner.TaskOutcome.SUCCESS; +import static org.gradle.testkit.runner.TaskOutcome.UP_TO_DATE; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class UpToDateTest extends Base { + + @Test + void testGaugeTaskIsNotCached() throws IOException { + copyGaugeProjectToTemp("project1"); + // Given plugin is applied + writeFile(buildFile, getApplyPluginsBlock()); + // Then I should be able to run the gauge task + GradleRunner runner = defaultGradleRunner().withArguments(GAUGE_TASK); + assertEquals(SUCCESS, runner.build().task(GAUGE_TASK_PATH).getOutcome()); + assertEquals(SUCCESS, runner.build().task(GAUGE_TASK_PATH).getOutcome()); + } + + @Test + void testGaugeValidateTaskIsNotCached() throws IOException { + copyGaugeProjectToTemp("project1"); + // Given plugin is applied + writeFile(buildFile, getApplyPluginsBlock()); + // Then I should be able to run the gauge task + GradleRunner runner = defaultGradleRunner().withArguments("gaugeValidate"); + assertEquals(SUCCESS, runner.build().task(":gaugeValidate").getOutcome()); + assertEquals(SUCCESS, runner.build().task(":gaugeValidate").getOutcome()); + } + + @Test + void testGaugeClasspathTaskIsCached() throws IOException { + copyGaugeProjectToTemp("project1"); + // Given plugin is applied + writeFile(buildFile, getApplyPluginsBlock()); + // Then I should be able to run the gauge task + GradleRunner runner = defaultGradleRunner().withArguments(GAUGE_CLASSPATH_TASK); + assertEquals(SUCCESS, runner.build().task(":" + GAUGE_CLASSPATH_TASK).getOutcome()); + assertEquals(UP_TO_DATE, runner.build().task(":" + GAUGE_CLASSPATH_TASK).getOutcome()); + } + +} diff --git a/plugin/src/integrationTest/java/org/gauge/gradle/ValidateTest.java b/plugin/src/integrationTest/java/org/gauge/gradle/ValidateTest.java new file mode 100644 index 0000000..9b989e1 --- /dev/null +++ b/plugin/src/integrationTest/java/org/gauge/gradle/ValidateTest.java @@ -0,0 +1,36 @@ +package org.gauge.gradle; + +import org.gradle.testkit.runner.BuildResult; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.gauge.gradle.GaugeConstants.GAUGE_VALIDATE_TASK; +import static org.gradle.testkit.runner.TaskOutcome.FAILED; +import static org.gradle.testkit.runner.TaskOutcome.SUCCESS; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ValidateTest extends Base { + + @Test + void testCanRunGaugeValidateTask() throws IOException { + copyGaugeProjectToTemp("project1"); + // Given plugin is applied without gauge extension + writeFile(buildFile, getApplyPluginsBlock()); + // Then I should be able to run the classpath task + BuildResult result = defaultGradleRunner().withArguments(GAUGE_VALIDATE_TASK).build(); + // And I should see no validation errors + assertEquals(SUCCESS, result.task(":" + GAUGE_VALIDATE_TASK).getOutcome()); + assertThat(result.getOutput(), containsString("No errors found.")); + // When specsDir is set in extension + // And example4.spec contains a missing step implementation + writeFile(buildFile, getApplyPluginsBlock() + "gauge {specsDir=\"multipleSpecs/example4.spec\"}\n"); + BuildResult resultWithExtension = defaultGradleRunner().withArguments(GAUGE_VALIDATE_TASK).buildAndFail(); + // Then I should see a failure with a validation error + assertEquals(FAILED, resultWithExtension.task(":" + GAUGE_VALIDATE_TASK).getOutcome()); + assertThat(resultWithExtension.getOutput(), containsString("example4.spec:6 Step implementation not found")); + } + +} diff --git a/plugin/src/integrationTest/resources/testProjects/project1/.gitignore b/plugin/src/integrationTest/resources/testProjects/project1/.gitignore new file mode 100644 index 0000000..ae34fbd --- /dev/null +++ b/plugin/src/integrationTest/resources/testProjects/project1/.gitignore @@ -0,0 +1,8 @@ +# Gauge - metadata dir +.gauge +# Gauge - log files dir +logs +# Gauge - java class output directory +gauge_bin +# Gauge - reports generated by reporting plugins +reports \ No newline at end of file diff --git a/plugin/src/integrationTest/resources/testProjects/project1/env/default/default.properties b/plugin/src/integrationTest/resources/testProjects/project1/env/default/default.properties new file mode 100644 index 0000000..a18b641 --- /dev/null +++ b/plugin/src/integrationTest/resources/testProjects/project1/env/default/default.properties @@ -0,0 +1,17 @@ +# default.properties +# The path to the gauge reports directory. Should be either relative to the project directory or an absolute path +gauge_reports_dir = reports +# Set as false if gauge reports should not be overwritten on each execution. A new time-stamped directory will be created on each execution. +overwrite_reports = true +# Set to false to disable screenshots on failure in reports. +screenshot_on_failure = false +# The path to the gauge logs directory. Should be either relative to the project directory or an absolute path +logs_directory = logs +# Set to true to use multithreading for parallel execution +enable_multithreading = true +# The path the gauge specifications directory. Takes a comma separated list of specification files/directories. +gauge_specs_dir = specs +# The default delimiter used read csv files. +csv_delimiter = , +# Allows steps to be written in multiline +allow_multiline_step = false \ No newline at end of file diff --git a/plugin/src/integrationTest/resources/testProjects/project1/env/default/java.properties b/plugin/src/integrationTest/resources/testProjects/project1/env/default/java.properties new file mode 100644 index 0000000..32b261a --- /dev/null +++ b/plugin/src/integrationTest/resources/testProjects/project1/env/default/java.properties @@ -0,0 +1,16 @@ +# Specify an alternate Java home if you want to use a custom version +gauge_java_home = +# IntelliJ and Eclipse out directory will be usually autodetected +# Use the below property if you need to override the build path +gauge_custom_build_path = +# specify the directory where additional libs are kept +# you can specify multiple directory names separated with a comma (,) +gauge_additional_libs = libs/* +# JVM arguments passed to java while launching. Enter multiple values separated by comma (,) eg. Xmx1024m, Xms128m +gauge_jvm_args = +# specify the directory containing java files to be compiled +# you can specify multiple directory names separated with a comma (,) +gauge_custom_compile_dir = +# specify the level at which the objects should be cleared +# Possible values are suite, spec and scenario. Default value is scenario. +gauge_clear_state_level = scenario diff --git a/plugin/src/integrationTest/resources/testProjects/project1/env/dev/dev.properties b/plugin/src/integrationTest/resources/testProjects/project1/env/dev/dev.properties new file mode 100644 index 0000000..3fd0305 --- /dev/null +++ b/plugin/src/integrationTest/resources/testProjects/project1/env/dev/dev.properties @@ -0,0 +1 @@ +gauge_reports_dir = reports/dev \ No newline at end of file diff --git a/plugin/src/integrationTest/resources/testProjects/project1/libs/.gitkeep b/plugin/src/integrationTest/resources/testProjects/project1/libs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/plugin/src/integrationTest/resources/testProjects/project1/manifest.json b/plugin/src/integrationTest/resources/testProjects/project1/manifest.json new file mode 100644 index 0000000..abae8b6 --- /dev/null +++ b/plugin/src/integrationTest/resources/testProjects/project1/manifest.json @@ -0,0 +1,6 @@ +{ + "Language": "java", + "Plugins": [ + "html-report" + ] +} diff --git a/plugin/src/integrationTest/resources/testProjects/project1/multipleSpecs/example1.spec b/plugin/src/integrationTest/resources/testProjects/project1/multipleSpecs/example1.spec new file mode 100644 index 0000000..90319a8 --- /dev/null +++ b/plugin/src/integrationTest/resources/testProjects/project1/multipleSpecs/example1.spec @@ -0,0 +1,19 @@ +# Specification Heading +* Vowels in English language are "aeiou". + +## Vowel counts in single word + +tags: example1 + +* The word "gauge" has "3" vowels. + +## Vowel counts in multiple word +* Almost all words have vowels + + |Word |Vowel Count| + |------|-----------| + |Gauge |3 | + |Mingle|2 | + |Snap |1 | + |GoCD |1 | + |Rhythm|0 | diff --git a/plugin/src/integrationTest/resources/testProjects/project1/multipleSpecs/example2.spec b/plugin/src/integrationTest/resources/testProjects/project1/multipleSpecs/example2.spec new file mode 100644 index 0000000..6f7a681 --- /dev/null +++ b/plugin/src/integrationTest/resources/testProjects/project1/multipleSpecs/example2.spec @@ -0,0 +1,19 @@ +# Specification Heading +* Vowels in English language are "aeiou". + +## Vowel counts in single word + +tags: example2 + +* The word "gauge" has "3" vowels. + +## Vowel counts in multiple word +* Almost all words have vowels + + |Word |Vowel Count| + |------|-----------| + |Gauge |3 | + |Mingle|2 | + |Snap |1 | + |GoCD |1 | + |Rhythm|0 | diff --git a/plugin/src/integrationTest/resources/testProjects/project1/multipleSpecs/example3.spec b/plugin/src/integrationTest/resources/testProjects/project1/multipleSpecs/example3.spec new file mode 100644 index 0000000..ea23cb0 --- /dev/null +++ b/plugin/src/integrationTest/resources/testProjects/project1/multipleSpecs/example3.spec @@ -0,0 +1,19 @@ +# Specification Heading +* Vowels in English language are "aeiou". + +## Vowel counts in single word + +tags: example3 + +* The word "gauge" has "3" vowels. + +## Vowel counts in multiple word +* Almost all words have vowels + + |Word |Vowel Count| + |------|-----------| + |Gauge |3 | + |Mingle|2 | + |Snap |1 | + |GoCD |1 | + |Rhythm|0 | diff --git a/plugin/src/integrationTest/resources/testProjects/project1/multipleSpecs/example4.spec b/plugin/src/integrationTest/resources/testProjects/project1/multipleSpecs/example4.spec new file mode 100644 index 0000000..805b1ef --- /dev/null +++ b/plugin/src/integrationTest/resources/testProjects/project1/multipleSpecs/example4.spec @@ -0,0 +1,6 @@ +# Specification Heading +* Vowels in English language are "aeiou". + +## Vowel counts in single word + +* The word "gauge" has 3 vowels. diff --git a/plugin/src/integrationTest/resources/testProjects/project1/specs/example.spec b/plugin/src/integrationTest/resources/testProjects/project1/specs/example.spec new file mode 100644 index 0000000..ca05050 --- /dev/null +++ b/plugin/src/integrationTest/resources/testProjects/project1/specs/example.spec @@ -0,0 +1,19 @@ +# Specification Heading +* Vowels in English language are "aeiou". + +## Vowel counts in single word + +tags: single word, example1 + +* The word "gauge" has "3" vowels. + +## Vowel counts in multiple word +* Almost all words have vowels + + |Word |Vowel Count| + |------|-----------| + |Gauge |3 | + |Mingle|2 | + |Snap |1 | + |GoCD |1 | + |Rhythm|0 | diff --git a/plugin/src/integrationTest/resources/testProjects/project1/src/test/java/StepImplementation.java b/plugin/src/integrationTest/resources/testProjects/project1/src/test/java/StepImplementation.java new file mode 100644 index 0000000..8b9dd9e --- /dev/null +++ b/plugin/src/integrationTest/resources/testProjects/project1/src/test/java/StepImplementation.java @@ -0,0 +1,48 @@ +import com.thoughtworks.gauge.Step; +import com.thoughtworks.gauge.Table; +import com.thoughtworks.gauge.TableRow; + +import java.util.HashSet; + +import static org.assertj.core.api.Assertions.assertThat; + +public class StepImplementation { + + private HashSet vowels; + + @Step("Vowels in English language are .") + public void setLanguageVowels(String vowelString) { + vowels = new HashSet<>(); + for (char ch : vowelString.toCharArray()) { + vowels.add(ch); + } + } + + @Step("The word has vowels.") + public void verifyVowelsCountInWord(String word, int expectedCount) { + int actualCount = countVowels(word); + assertThat(expectedCount).isEqualTo(actualCount); + System.out.println(String.format("customVariable is set to %s in build.gradle", System.getenv("customVariable"))); + } + + @Step("Almost all words have vowels ") + public void verifyVowelsCountInMultipleWords(Table wordsTable) { + for (TableRow row : wordsTable.getTableRows()) { + String word = row.getCell("Word"); + int expectedCount = Integer.parseInt(row.getCell("Vowel Count")); + int actualCount = countVowels(word); + + assertThat(expectedCount).isEqualTo(actualCount); + } + } + + private int countVowels(String word) { + int count = 0; + for (char ch : word.toCharArray()) { + if (vowels.contains(ch)) { + count++; + } + } + return count; + } +} diff --git a/plugin/src/main/java/com/thoughtworks/gauge/gradle/ClasspathTask.java b/plugin/src/main/java/com/thoughtworks/gauge/gradle/ClasspathTask.java deleted file mode 100644 index 873a473..0000000 --- a/plugin/src/main/java/com/thoughtworks/gauge/gradle/ClasspathTask.java +++ /dev/null @@ -1,26 +0,0 @@ -/*---------------------------------------------------------------- - * Copyright (c) ThoughtWorks, Inc. - * Licensed under the Apache License, Version 2.0 - * See LICENSE.txt in the project root for license information. - *----------------------------------------------------------------*/ - -package com.thoughtworks.gauge.gradle; - -import com.thoughtworks.gauge.gradle.util.PropertyManager; - -import org.gradle.api.Project; -import org.gradle.api.tasks.TaskAction; -import org.gradle.api.tasks.testing.Test; - -@SuppressWarnings("WeakerAccess") -public class ClasspathTask extends Test { - - @TaskAction - public void classpath() { - Project project = getProject(); - GaugeExtension extension = project.getExtensions().findByType(GaugeExtension.class); - PropertyManager propertyManager = new PropertyManager(project, extension); - propertyManager.setProperties(); - System.out.println(extension.getClasspath()); - } -} \ No newline at end of file diff --git a/plugin/src/main/java/com/thoughtworks/gauge/gradle/GaugeExtension.java b/plugin/src/main/java/com/thoughtworks/gauge/gradle/GaugeExtension.java deleted file mode 100644 index 357316e..0000000 --- a/plugin/src/main/java/com/thoughtworks/gauge/gradle/GaugeExtension.java +++ /dev/null @@ -1,83 +0,0 @@ -/*---------------------------------------------------------------- - * Copyright (c) ThoughtWorks, Inc. - * Licensed under the Apache License, Version 2.0 - * See LICENSE.txt in the project root for license information. - *----------------------------------------------------------------*/ - -package com.thoughtworks.gauge.gradle; - -public class GaugeExtension { - - private String specsDir; - private Boolean inParallel = false; - private Integer nodes; - private String env; - private String tags; - private String classpath; - private String additionalFlags; - private String gaugeRoot; - - public String getSpecsDir() { - return specsDir; - } - - public void setSpecsDir(String specsDir) { - this.specsDir = specsDir; - } - - public Boolean isInParallel() { - return inParallel; - } - - public void setInParallel(Boolean inParallel) { - this.inParallel = inParallel; - } - - public String getTags() { - return tags; - } - - public void setTags(String tags) { - this.tags = tags; - } - - public String getEnv() { - return env; - } - - public void setEnv(String env) { - this.env = env; - } - - public Integer getNodes() { - return nodes; - } - - public void setNodes(Integer nodes) { - this.nodes = nodes; - } - - public String getClasspath() { - return classpath; - } - - public void setClasspath(String classpath) { - this.classpath = classpath; - } - - public String getAdditionalFlags() { - return additionalFlags; - } - - public void setAdditionalFlags(String additionalFlags) { - this.additionalFlags = additionalFlags; - } - - public String getGaugeRoot() { - return gaugeRoot; - } - - public void setGaugeRoot(String gaugeRoot) { - this.gaugeRoot = gaugeRoot; - } -} diff --git a/plugin/src/main/java/com/thoughtworks/gauge/gradle/GaugePlugin.java b/plugin/src/main/java/com/thoughtworks/gauge/gradle/GaugePlugin.java deleted file mode 100644 index 6448210..0000000 --- a/plugin/src/main/java/com/thoughtworks/gauge/gradle/GaugePlugin.java +++ /dev/null @@ -1,23 +0,0 @@ -/*---------------------------------------------------------------- - * Copyright (c) ThoughtWorks, Inc. - * Licensed under the Apache License, Version 2.0 - * See LICENSE.txt in the project root for license information. - *----------------------------------------------------------------*/ - - package com.thoughtworks.gauge.gradle; - -import org.gradle.api.Plugin; -import org.gradle.api.Project; - -public class GaugePlugin implements Plugin { - - private static final String GAUGE = "gauge"; - private static final String CLASSPATH = "classpath"; - - @Override - public void apply(Project project) { - project.getExtensions().create(GAUGE, GaugeExtension.class); - project.getTasks().create(GAUGE, GaugeTask.class); - project.getTasks().create(CLASSPATH, ClasspathTask.class); - } -} \ No newline at end of file diff --git a/plugin/src/main/java/com/thoughtworks/gauge/gradle/GaugeTask.java b/plugin/src/main/java/com/thoughtworks/gauge/gradle/GaugeTask.java deleted file mode 100644 index 4cbbe2d..0000000 --- a/plugin/src/main/java/com/thoughtworks/gauge/gradle/GaugeTask.java +++ /dev/null @@ -1,58 +0,0 @@ -/*---------------------------------------------------------------- - * Copyright (c) ThoughtWorks, Inc. - * Licensed under the Apache License, Version 2.0 - * See LICENSE.txt in the project root for license information. - *----------------------------------------------------------------*/ - -package com.thoughtworks.gauge.gradle; - -import com.thoughtworks.gauge.gradle.exception.GaugeExecutionFailedException; -import com.thoughtworks.gauge.gradle.util.ProcessBuilderFactory; -import com.thoughtworks.gauge.gradle.util.PropertyManager; -import com.thoughtworks.gauge.gradle.util.Util; -import org.gradle.api.Project; -import org.gradle.api.tasks.TaskAction; -import org.gradle.api.tasks.testing.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; - -@SuppressWarnings("WeakerAccess") -public class GaugeTask extends Test { - private final Logger log = LoggerFactory.getLogger("gauge"); - - @TaskAction - public void gauge() { - Project project = getProject(); - GaugeExtension extension = project.getExtensions().findByType(GaugeExtension.class); - PropertyManager propertyManager = new PropertyManager(project, extension); - propertyManager.setProperties(); - - ProcessBuilderFactory processBuilderFactory = new ProcessBuilderFactory(extension, project); - ProcessBuilder builder = processBuilderFactory.create(); - log.info("Executing command => " + builder.command()); - - try { - Process process = builder.start(); - executeGaugeSpecs(process); - } catch (IOException e) { - if(e.getMessage().contains("Cannot run program \"gauge\": error=2, No such file or directory")){ - throw new GaugeExecutionFailedException("Gauge or Java runner is not installed! Refer https://docs.gauge.org/getting_started/installing-gauge.html"); - } - log.error(e.getMessage() + e.getStackTrace()); - } - } - - public void executeGaugeSpecs(Process process) throws GaugeExecutionFailedException { - try { - Util.inheritIO(process.getInputStream(), System.out); - Util.inheritIO(process.getErrorStream(), System.err); - if (process.waitFor() != 0) { - throw new GaugeExecutionFailedException("Execution failed for one or more tests!"); - } - } catch (InterruptedException | NullPointerException e) { - throw new GaugeExecutionFailedException(e); - } - } -} \ No newline at end of file diff --git a/plugin/src/main/java/com/thoughtworks/gauge/gradle/exception/GaugeExecutionFailedException.java b/plugin/src/main/java/com/thoughtworks/gauge/gradle/exception/GaugeExecutionFailedException.java deleted file mode 100644 index 167d0c4..0000000 --- a/plugin/src/main/java/com/thoughtworks/gauge/gradle/exception/GaugeExecutionFailedException.java +++ /dev/null @@ -1,19 +0,0 @@ -/*---------------------------------------------------------------- - * Copyright (c) ThoughtWorks, Inc. - * Licensed under the Apache License, Version 2.0 - * See LICENSE.txt in the project root for license information. - *----------------------------------------------------------------*/ - -package com.thoughtworks.gauge.gradle.exception; - -import org.gradle.api.GradleException; - -public class GaugeExecutionFailedException extends GradleException { - public GaugeExecutionFailedException(Throwable throwable) { - super(throwable.getMessage(), throwable); - } - - public GaugeExecutionFailedException(String message) { - super(message); - } -} diff --git a/plugin/src/main/java/com/thoughtworks/gauge/gradle/util/ProcessBuilderFactory.java b/plugin/src/main/java/com/thoughtworks/gauge/gradle/util/ProcessBuilderFactory.java deleted file mode 100644 index 92cc1f6..0000000 --- a/plugin/src/main/java/com/thoughtworks/gauge/gradle/util/ProcessBuilderFactory.java +++ /dev/null @@ -1,138 +0,0 @@ -/*---------------------------------------------------------------- - * Copyright (c) ThoughtWorks, Inc. - * Licensed under the Apache License, Version 2.0 - * See LICENSE.txt in the project root for license information. - *----------------------------------------------------------------*/ - -package com.thoughtworks.gauge.gradle.util; - -import com.thoughtworks.gauge.gradle.GaugeExtension; -import org.gradle.api.Project; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; - -public class ProcessBuilderFactory { - private static final String GAUGE = "gauge"; - private static final String RUN_SUBCOMMAND = "run"; - private static final String ENV_FLAG = "--env"; - private static final String NODE_FLAG = "-n"; - private static final String TAGS_FLAG = "--tags"; - private static final String PARALLEL_FLAG = "--parallel"; - private static final String CUSTOM_CLASSPATH = "gauge_custom_classpath"; - private static final String WORKING_DIRECTORY_FLAG = "--dir"; - - private final Logger log = LoggerFactory.getLogger(GAUGE); - private GaugeExtension extension; - private Project project; - - public ProcessBuilderFactory(GaugeExtension extension, Project project) { - this.extension = extension; - this.project = project; - } - - public ProcessBuilder create() { - ProcessBuilder builder = new ProcessBuilder(); - builder.command(createGaugeCommand()); - - setClasspath(builder); - return builder; - } - - private void setClasspath(ProcessBuilder builder) { - String classpath = extension.getClasspath(); - - log.debug("Setting Custom classpath => {}", classpath); - builder.environment().put(CUSTOM_CLASSPATH, classpath); - } - - private ArrayList createGaugeCommand() { - ArrayList command = new ArrayList<>(); - addGaugeExecutable(command); - command.add(RUN_SUBCOMMAND); - addTags(command); - addParallelFlags(command); - addEnv(command); - addWorkingDirectory(command); - addAdditionalFlags(command); - addSpecsDir(command); - return command; - } - - private void addEnv(ArrayList command) { - String env = extension.getEnv(); - if (env != null && !env.isEmpty()) { - command.add(ENV_FLAG); - command.add(env); - } - } - - private void addAdditionalFlags(ArrayList command) { - String flags = extension.getAdditionalFlags(); - if (flags != null) { - command.addAll(Arrays.asList(flags.split(" "))); - } - } - - private void addParallelFlags(ArrayList command) { - Boolean inParallel = extension.isInParallel(); - Integer nodes = extension.getNodes(); - if (inParallel != null && inParallel) { - command.add(PARALLEL_FLAG); - if (nodes != null && nodes != 0) { - command.add(NODE_FLAG); - command.add(Integer.toString(nodes)); - } - } - } - - private void addSpecsDir(ArrayList command) { - String specsDirectoryPath = extension.getSpecsDir(); - - if (specsDirectoryPath != null) { - validateSpecsDirectory(specsDirectoryPath, command); - } - } - - private void validateSpecsDirectory(String specsDirectoryPath, ArrayList command) { - if (specsDirectoryPath.contains(" ")) { - command.addAll(Arrays.asList(specsDirectoryPath.split(" "))); - } else if (specsDirectoryPath.contains(",")) { - command.addAll(Arrays.asList(specsDirectoryPath.split(","))); - } else { - command.add(specsDirectoryPath); - } - } - - private String getCurrentProjectPath() { - return project.getProjectDir().getAbsolutePath(); - } - - private void addWorkingDirectory(ArrayList command) { - String workingDirectory = getCurrentProjectPath(); - command.add(WORKING_DIRECTORY_FLAG); - command.add(workingDirectory); - } - - private void addTags(ArrayList command) { - String tags = extension.getTags(); - if (tags != null && !tags.isEmpty()) { - command.add(TAGS_FLAG); - command.add(tags); - } - } - - private void addGaugeExecutable(ArrayList command) { - String gauge; - String gaugeRoot = extension.getGaugeRoot(); - if (gaugeRoot != null && !gaugeRoot.isEmpty()) { - gauge = Paths.get(gaugeRoot, "bin", GAUGE).toString(); - } else { - gauge = GAUGE; - } - command.add(gauge); - } -} diff --git a/plugin/src/main/java/com/thoughtworks/gauge/gradle/util/PropertyManager.java b/plugin/src/main/java/com/thoughtworks/gauge/gradle/util/PropertyManager.java deleted file mode 100644 index 032cce0..0000000 --- a/plugin/src/main/java/com/thoughtworks/gauge/gradle/util/PropertyManager.java +++ /dev/null @@ -1,118 +0,0 @@ -/*---------------------------------------------------------------- - * Copyright (c) ThoughtWorks, Inc. - * Licensed under the Apache License, Version 2.0 - * See LICENSE.txt in the project root for license information. - *----------------------------------------------------------------*/ - -package com.thoughtworks.gauge.gradle.util; - -import com.thoughtworks.gauge.gradle.GaugeExtension; -import org.gradle.api.Project; -import org.gradle.api.tasks.SourceSetContainer; -import java.io.File; -import java.util.Map; -import java.util.Set; - -@SuppressWarnings("ConstantConditions") -public class PropertyManager { - private static final String ENV = "env"; - private static final String TAGS = "tags"; - private static final String NODES = "nodes"; - private static final String SPECS_DIR = "specsDir"; - private static final String IN_PARALLEL = "inParallel"; - private static final String ADDITIONAL_FLAGS = "additionalFlags"; - private static final String GAUGE_ROOT = "gaugeRoot"; - private static final String FAILED = "--failed"; - private static final String REPEAT = "--repeat"; - - private Project project; - private GaugeExtension extension; - private Map properties; - - public PropertyManager(Project project, GaugeExtension extension) { - this.project = project; - this.extension = extension; - this.properties = project.getProperties(); - } - - public void setProperties() { - setTags(); - setInParallel(); - setNodes(); - setEnv(); - setAdditionalFlags(); - setClasspath(); - setSpecsDir(); - setGaugeRoot(); - } - - private void setSpecsDir() { - String specsDir = (String) properties.get(SPECS_DIR); - if (specsDir != null) { - extension.setSpecsDir(specsDir); - } - } - - private void setInParallel() { - String inParallel = (String) properties.get(IN_PARALLEL); - if (inParallel != null) { - extension.setInParallel("true".equals(inParallel)); - } - } - - private void setNodes() { - String nodes = (String) properties.get(NODES); - if (nodes != null) { - extension.setNodes(Integer.parseInt(nodes)); - } - } - - private void setTags() { - String tags = (String) properties.get(TAGS); - if (tags != null) { - extension.setTags(tags); - } - } - - private void setEnv() { - String env = (String) properties.get(ENV); - if (env != null) { - extension.setEnv(env); - } - } - - private void setAdditionalFlags() { - String flags = (String) properties.get(ADDITIONAL_FLAGS); - if (flags != null) { - extension.setAdditionalFlags(flags); - if (flags.contains(FAILED) || flags.contains(REPEAT)) { - extension.setSpecsDir(null); - } - } - } - - private void setClasspath() { - String runtimeClasspath = ((SourceSetContainer) properties.get("sourceSets")).getByName("test").getRuntimeClasspath().getAsPath(); - extension.setClasspath(runtimeClasspath); - } - - private void setGaugeRoot() { - String gaugeRoot = (String) properties.get(GAUGE_ROOT); - if (gaugeRoot != null) { - extension.setGaugeRoot(gaugeRoot); - } - } - - private void findFiles(String dir, Set classPaths) { - File files = new File(dir); - if (!files.exists()) { - return; - } - for (File file : files.listFiles()) { - if (file.isDirectory()) { - classPaths.add(file.getAbsolutePath()); - findFiles(file.getAbsolutePath(), classPaths); - } - } - } -} diff --git a/plugin/src/main/java/com/thoughtworks/gauge/gradle/util/Util.java b/plugin/src/main/java/com/thoughtworks/gauge/gradle/util/Util.java deleted file mode 100644 index 44bef17..0000000 --- a/plugin/src/main/java/com/thoughtworks/gauge/gradle/util/Util.java +++ /dev/null @@ -1,28 +0,0 @@ -/*---------------------------------------------------------------- - * Copyright (c) ThoughtWorks, Inc. - * Licensed under the Apache License, Version 2.0 - * See LICENSE.txt in the project root for license information. - *----------------------------------------------------------------*/ - -package com.thoughtworks.gauge.gradle.util; - -import java.io.InputStream; -import java.io.PrintStream; -import java.util.Scanner; - -public class Util { - public static void inheritIO(final InputStream src, final PrintStream dest) { - new Thread(new Runnable() { - public void run() { - try { - Scanner sc = new Scanner(src); - while (sc.hasNextLine()) { - dest.println(sc.nextLine()); - } - } catch (NullPointerException ignored) { - - } - } - }).start(); - } -} diff --git a/plugin/src/main/java/org/gauge/gradle/GaugeClasspathTask.java b/plugin/src/main/java/org/gauge/gradle/GaugeClasspathTask.java new file mode 100644 index 0000000..3b5a8c3 --- /dev/null +++ b/plugin/src/main/java/org/gauge/gradle/GaugeClasspathTask.java @@ -0,0 +1,23 @@ +/*---------------------------------------------------------------- + * Copyright (c) ThoughtWorks, Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE.txt in the project root for license information. + *----------------------------------------------------------------*/ + +package org.gauge.gradle; + +import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.testing.Test; + +public abstract class GaugeClasspathTask extends Test { + + public GaugeClasspathTask() { + this.setGroup(GaugeConstants.GAUGE_TASK_GROUP); + this.setDescription("Gets the classpath."); + } + + @TaskAction + public void classpath() { + System.out.println(getClasspath().getAsPath()); + } +} \ No newline at end of file diff --git a/plugin/src/main/java/org/gauge/gradle/GaugeCommand.java b/plugin/src/main/java/org/gauge/gradle/GaugeCommand.java new file mode 100644 index 0000000..796538f --- /dev/null +++ b/plugin/src/main/java/org/gauge/gradle/GaugeCommand.java @@ -0,0 +1,120 @@ +package org.gauge.gradle; + +import org.gradle.api.Project; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +class GaugeCommand { + + private final GaugeExtension extension; + private final Map properties; + private final Project project; + + public GaugeCommand(final GaugeExtension extension, final Project project) { + this.extension = extension; + this.project = project; + this.properties = project.getProperties(); + } + + public String getExecutable() { + final String binary = "gauge"; + return project.hasProperty(GaugeProperty.GAUGE_ROOT.getKey()) + ? getExecutablePath(properties.get(GaugeProperty.GAUGE_ROOT.getKey()).toString()).toString() + : extension.getGaugeRoot().isPresent() + ? getExecutablePath(extension.getGaugeRoot().get()).toString() + : binary; + } + + private Path getExecutablePath(final String gaugeRoot) { + return Paths.get(gaugeRoot, "bin", "gauge"); + } + + public List getProjectDir() { + return List.of(GaugeProperty.PROJECT_DIR.getFlag(), getDir()); + } + + private String getDir() { + return project.hasProperty(GaugeProperty.PROJECT_DIR.getKey()) + ? getProjectPath(properties.get(GaugeProperty.PROJECT_DIR.getKey()).toString()).toString() + : getProjectPath(extension.getDir().getOrElse(project.getProjectDir().getAbsolutePath())).toString(); + } + + private Path getProjectPath(final String projectDir) { + return project.getProjectDir().toPath().resolve(Path.of(projectDir)).toAbsolutePath(); + } + + public List getEnvironment() { + return List.of(GaugeProperty.ENV.getFlag(), getEnv().trim()); + } + + private String getEnv() { + return project.hasProperty(GaugeProperty.ENV.getKey()) + ? properties.get(GaugeProperty.ENV.getKey()).toString() + : extension.getEnv().get(); + } + + public boolean isNotFailedOrRepeatFlagProvided() { + final List flags = getAdditionalFlags(); + return !flags.contains("--failed") && !flags.contains("--repeat"); + } + + public List getFlags() { + final List flags = new ArrayList<>(getAdditionalFlags()); + // --repeat and --failed flags cannot be run with other flags + if (isNotFailedOrRepeatFlagProvided()) { + if (isInParallel()) { + flags.add(GaugeProperty.IN_PARALLEL.getFlag()); + final int nodes = getNodes(); + if (nodes != 0) { + flags.addAll(List.of(GaugeProperty.NODES.getFlag(), nodes)); + } + } + } + return flags; + } + + private List getAdditionalFlags() { + return project.hasProperty(GaugeProperty.ADDITIONAL_FLAGS.getKey()) + ? getListFromString(properties.get(GaugeProperty.ADDITIONAL_FLAGS.getKey()).toString()) + : extension.getAdditionalFlags().isPresent() + ? getListFromString(extension.getAdditionalFlags().get()) + : Collections.emptyList(); + } + + private List getListFromString(final String value) { + return Arrays.stream(value.split("\\s+")).map(String::trim).collect(Collectors.toList()); + } + + private int getNodes() { + return project.hasProperty(GaugeProperty.NODES.getKey()) + ? Integer.parseInt(properties.get(GaugeProperty.NODES.getKey()).toString()) + : extension.getNodes().isPresent() ? extension.getNodes().get() : 0; + } + + private boolean isInParallel() { + return project.hasProperty(GaugeProperty.IN_PARALLEL.getKey()) + ? Boolean.parseBoolean(project.getProperties().get(GaugeProperty.IN_PARALLEL.getKey()).toString()) + : extension.getInParallel().get(); + } + + public List getSpecsDir() { + final var specs = properties.containsKey(GaugeProperty.SPECS_DIR.getKey()) + ? properties.get(GaugeProperty.SPECS_DIR.getKey()).toString() : extension.getSpecsDir().get(); + return getListFromString(specs.trim()); + } + + public List getTags() { + final String tags = project.hasProperty(GaugeProperty.TAGS.getKey()) + ? properties.get(GaugeProperty.TAGS.getKey()).toString() + : extension.getTags().isPresent() ? extension.getTags().get() : ""; + return !tags.isEmpty() ? List.of(GaugeProperty.TAGS.getFlag(), tags) : Collections.emptyList(); + } + +} diff --git a/plugin/src/main/java/org/gauge/gradle/GaugeConstants.java b/plugin/src/main/java/org/gauge/gradle/GaugeConstants.java new file mode 100644 index 0000000..eec78df --- /dev/null +++ b/plugin/src/main/java/org/gauge/gradle/GaugeConstants.java @@ -0,0 +1,12 @@ +package org.gauge.gradle; + +class GaugeConstants { + public static final String GAUGE_PLUGIN_ID = "org.gauge"; + public static final String GAUGE_EXTENSION_ID = "gauge"; + public static final String GAUGE_TASK = GAUGE_EXTENSION_ID; + public static final String GAUGE_TASK_GROUP = GAUGE_EXTENSION_ID; + public static final String GAUGE_CLASSPATH_TASK = "classpath"; + public static final String GAUGE_VALIDATE_TASK = "gaugeValidate"; + public static final String GAUGE_CUSTOM_CLASSPATH = "gauge_custom_classpath"; + +} diff --git a/plugin/src/main/java/org/gauge/gradle/GaugeExtension.java b/plugin/src/main/java/org/gauge/gradle/GaugeExtension.java new file mode 100644 index 0000000..4949cc3 --- /dev/null +++ b/plugin/src/main/java/org/gauge/gradle/GaugeExtension.java @@ -0,0 +1,61 @@ +package org.gauge.gradle; + +import org.gradle.api.provider.MapProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.provider.ProviderFactory; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; + +import javax.inject.Inject; + +public abstract class GaugeExtension { + @Inject + public GaugeExtension() { + getEnv().convention(gradleProperty(GaugeProperty.ENV.getKey()).getOrElse("default")); + getSpecsDir().convention(gradleProperty(GaugeProperty.SPECS_DIR.getKey()).getOrElse("specs")); + getInParallel().convention(gradleProperty(GaugeProperty.IN_PARALLEL.getKey()).map(Boolean::parseBoolean).getOrElse(false)); + } + + @Inject + protected abstract ProviderFactory getProviders(); + @Input + @Optional + public abstract Property getDir(); + @Input + @Optional + public abstract Property getEnv(); + + @Input + @Optional + public abstract Property getTags(); + + @Input + @Optional + public abstract Property getSpecsDir(); + + @Input + @Optional + public abstract Property getInParallel(); + + @Input + @Optional + public abstract Property getNodes(); + + @Input + @Optional + public abstract MapProperty getEnvironmentVariables(); + + @Input + @Optional + public abstract Property getAdditionalFlags(); + + @Input + @Optional + public abstract Property getGaugeRoot(); + + private Provider gradleProperty(String name) { + return getProviders().gradleProperty(name); + } + +} diff --git a/plugin/src/main/java/org/gauge/gradle/GaugePlugin.java b/plugin/src/main/java/org/gauge/gradle/GaugePlugin.java new file mode 100644 index 0000000..69dfece --- /dev/null +++ b/plugin/src/main/java/org/gauge/gradle/GaugePlugin.java @@ -0,0 +1,24 @@ +/*---------------------------------------------------------------- + * Copyright (c) ThoughtWorks, Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE.txt in the project root for license information. + *----------------------------------------------------------------*/ + +package org.gauge.gradle; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.plugins.JavaLibraryPlugin; + +public class GaugePlugin implements Plugin { + + @Override + public void apply(Project project) { + // java plugin needs to be applied as Gauge relies on java test classpath + project.getPluginManager().apply(JavaLibraryPlugin.class); + project.getExtensions().create(GaugeConstants.GAUGE_EXTENSION_ID, GaugeExtension.class); + project.getTasks().create(GaugeConstants.GAUGE_TASK, GaugeTask.class); + project.getTasks().create(GaugeConstants.GAUGE_VALIDATE_TASK, GaugeValidateTask.class); + project.getTasks().create(GaugeConstants.GAUGE_CLASSPATH_TASK, GaugeClasspathTask.class); + } +} \ No newline at end of file diff --git a/plugin/src/main/java/org/gauge/gradle/GaugeProperty.java b/plugin/src/main/java/org/gauge/gradle/GaugeProperty.java new file mode 100644 index 0000000..339c9e5 --- /dev/null +++ b/plugin/src/main/java/org/gauge/gradle/GaugeProperty.java @@ -0,0 +1,30 @@ +package org.gauge.gradle; + +enum GaugeProperty { + + ADDITIONAL_FLAGS("additionalFlags", ""), + ENV("env", "--env"), + GAUGE_ROOT("gaugeRoot", ""), + TAGS("tags", "--tags"), + SPECS_DIR("specsDir", ""), + IN_PARALLEL("inParallel", "--parallel"), + NODES("nodes", "--n"), + PROJECT_DIR("dir", "--dir"); + + private final String key; + private final String flag; + + GaugeProperty(final String key, final String flag) { + this.key = key; + this.flag = flag; + } + + String getKey() { + return this.key; + } + + String getFlag() { + return this.flag; + } + +} diff --git a/plugin/src/main/java/org/gauge/gradle/GaugeTask.java b/plugin/src/main/java/org/gauge/gradle/GaugeTask.java new file mode 100644 index 0000000..7f9864e --- /dev/null +++ b/plugin/src/main/java/org/gauge/gradle/GaugeTask.java @@ -0,0 +1,45 @@ +package org.gauge.gradle; + +import org.gradle.api.Project; +import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.testing.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class GaugeTask extends Test { + private static final Logger logger = LoggerFactory.getLogger("gauge"); + + public GaugeTask() { + this.setGroup(GaugeConstants.GAUGE_TASK_GROUP); + this.setDescription("Runs the Gauge test suite."); + // So that previous outputs of this task cannot be reused + this.getOutputs().upToDateWhen(task -> false); + } + + @TaskAction + public void execute() { + final Project project = getProject(); + final GaugeExtension extension = project.getExtensions().findByType(GaugeExtension.class); + final GaugeCommand command = new GaugeCommand(extension, project); + + project.exec(spec -> { + // Usage: + // gauge [flags] [args] + spec.executable(command.getExecutable()); + spec.args("run"); + spec.args(command.getProjectDir()); + spec.args(command.getFlags()); + if (command.isNotFailedOrRepeatFlagProvided()) { + spec.args(command.getEnvironment()); + spec.args(command.getTags()); + spec.args(command.getSpecsDir()); + } + spec.environment(GaugeConstants.GAUGE_CUSTOM_CLASSPATH, getClasspath().getAsPath()); + if (null != extension) { + extension.getEnvironmentVariables().get().forEach(spec::environment); + } + logger.info("Running {} {}", spec.getExecutable(), spec.getArgs()); + }); + } + +} diff --git a/plugin/src/main/java/org/gauge/gradle/GaugeValidateTask.java b/plugin/src/main/java/org/gauge/gradle/GaugeValidateTask.java new file mode 100644 index 0000000..dbac759 --- /dev/null +++ b/plugin/src/main/java/org/gauge/gradle/GaugeValidateTask.java @@ -0,0 +1,35 @@ +package org.gauge.gradle; + +import org.gradle.api.Project; +import org.gradle.api.tasks.TaskAction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class GaugeValidateTask extends GaugeTask { + private static final Logger logger = LoggerFactory.getLogger("gauge"); + + public GaugeValidateTask() { + this.setGroup(GaugeConstants.GAUGE_TASK_GROUP); + this.setDescription("Check for validation and parse errors."); + // So that previous outputs of this task cannot be reused + this.getOutputs().upToDateWhen(task -> false); + } + @TaskAction + public void execute() { + final Project project = getProject(); + final GaugeExtension extension = project.getExtensions().findByType(GaugeExtension.class); + final GaugeCommand command = new GaugeCommand(extension, project); + project.exec(spec -> { + spec.executable(command.getExecutable()); + spec.args("validate"); + spec.args(command.getProjectDir()); + spec.args(command.getSpecsDir()); + spec.environment(GaugeConstants.GAUGE_CUSTOM_CLASSPATH, getClasspath().getAsPath()); + if (null != extension) { + extension.getEnvironmentVariables().get().forEach(spec::environment); + } + logger.info("Running {} {}", spec.getExecutable(), spec.getArgs()); + }); + } + +} diff --git a/plugin/src/main/resources/META-INF/gradle-plugins/com.thoughtworks.gauge.properties b/plugin/src/main/resources/META-INF/gradle-plugins/com.thoughtworks.gauge.properties deleted file mode 100644 index 036f08d..0000000 --- a/plugin/src/main/resources/META-INF/gradle-plugins/com.thoughtworks.gauge.properties +++ /dev/null @@ -1 +0,0 @@ -implementation-class=com.thoughtworks.gauge.gradle.GaugePlugin \ No newline at end of file diff --git a/plugin/src/main/resources/META-INF/gradle-plugins/gauge.properties b/plugin/src/main/resources/META-INF/gradle-plugins/gauge.properties deleted file mode 100644 index 036f08d..0000000 --- a/plugin/src/main/resources/META-INF/gradle-plugins/gauge.properties +++ /dev/null @@ -1 +0,0 @@ -implementation-class=com.thoughtworks.gauge.gradle.GaugePlugin \ No newline at end of file diff --git a/plugin/src/test/java/com/thoughtworks/gauge/gradle/GaugeExtensionTest.java b/plugin/src/test/java/com/thoughtworks/gauge/gradle/GaugeExtensionTest.java deleted file mode 100644 index 9740f45..0000000 --- a/plugin/src/test/java/com/thoughtworks/gauge/gradle/GaugeExtensionTest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.thoughtworks.gauge.gradle; - -import org.gradle.api.Project; -import org.gradle.testfixtures.ProjectBuilder; -import org.junit.Test; - -import static org.junit.Assert.*; - -public class GaugeExtensionTest { - - private static final String GAUGE = "gauge"; - - @Test - public void shouldLoadDefaultProperties() { - Project project = ProjectBuilder.builder().build(); - GaugeExtension gauge = project.getExtensions().create(GAUGE, GaugeExtension.class); - - assertNotNull(gauge); - assertNull(gauge.getSpecsDir()); - assertFalse(gauge.isInParallel()); - assertNull(gauge.getNodes()); - assertNull(gauge.getEnv()); - assertNull(gauge.getTags()); - assertNull(gauge.getClasspath()); - assertNull(gauge.getAdditionalFlags()); - assertNull(gauge.getGaugeRoot()); - } -} diff --git a/plugin/src/test/java/com/thoughtworks/gauge/gradle/GaugePluginTest.java b/plugin/src/test/java/com/thoughtworks/gauge/gradle/GaugePluginTest.java deleted file mode 100644 index 47b4366..0000000 --- a/plugin/src/test/java/com/thoughtworks/gauge/gradle/GaugePluginTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.thoughtworks.gauge.gradle; - -import org.gradle.api.Project; -import org.gradle.api.Task; -import org.gradle.api.plugins.JavaPlugin; -import org.gradle.api.tasks.TaskContainer; -import org.gradle.testfixtures.ProjectBuilder; -import org.junit.Before; -import org.junit.Test; - -import java.util.SortedMap; - -import static org.junit.Assert.*; - -public class GaugePluginTest { - private static final String GAUGE = "gauge"; - private Project project; - - @Before - public void setUp() { - project = ProjectBuilder.builder().build(); - } - - @Test - public void pluginShouldBeAddedOnApply() { - project.getPluginManager().apply(GAUGE); - assertTrue(project.getPlugins().getPlugin(GAUGE) instanceof GaugePlugin); - assertFalse(project.getPlugins().getPlugin(GAUGE) instanceof JavaPlugin); - } - - @Test - public void taskShouldBeAddedOnApply() { - project.getPluginManager().apply(GAUGE); - TaskContainer tasks = project.getTasks(); - assertEquals(2, tasks.size()); - - SortedMap tasksMap = tasks.getAsMap(); - Task gauge = tasksMap.get(GAUGE); - Task classpath = tasksMap.get("classpath"); - - assertTrue(gauge instanceof GaugeTask); - assertTrue(classpath instanceof ClasspathTask); - } -} \ No newline at end of file diff --git a/plugin/src/test/java/com/thoughtworks/gauge/gradle/GaugeTaskTest.java b/plugin/src/test/java/com/thoughtworks/gauge/gradle/GaugeTaskTest.java deleted file mode 100644 index 27b2dbd..0000000 --- a/plugin/src/test/java/com/thoughtworks/gauge/gradle/GaugeTaskTest.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.thoughtworks.gauge.gradle; - -import com.thoughtworks.gauge.gradle.util.ProcessBuilderFactory; -import org.gradle.api.Project; -import org.gradle.testfixtures.ProjectBuilder; -import org.junit.Before; -import org.junit.Test; - -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; - -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class GaugeTaskTest { - private static final String GAUGE = "gauge"; - private static final String ENV_FLAG = "--env"; - private static final String TAGS_FLAG = "--tags"; - private static final String NODES_FLAG = "-n"; - private static final String VERBOSE_FLAG = "--verbose"; - private static final String SPECS_FOLDER = "specsFolder"; - private static final String PARALLEL_FLAG = "--parallel"; - private static final String GAUGE_ROOT = "/opt/gauge"; - - private GaugeTask task; - private Project project; - private ProcessBuilderFactory factory; - - @Before - public void setUp() { - GaugePlugin plugin = new GaugePlugin(); - project = ProjectBuilder.builder().build(); - plugin.apply(project); - task = (GaugeTask) project.getTasks().findByPath(GAUGE); - factory = mock(ProcessBuilderFactory.class); - } - - @Test - public void shouldLoadProperties() throws InterruptedException { - Process process = mock(Process.class); - setExpectations(process); - executeGaugeTask(process); - - List command = factory.create().command(); - assertTrue(command.contains(Paths.get(GAUGE_ROOT, "bin", GAUGE).toString())); - assertTrue(command.contains(PARALLEL_FLAG)); - assertTrue(command.contains(NODES_FLAG)); - assertTrue(command.contains("2")); - assertTrue(command.contains(ENV_FLAG)); - assertTrue(command.contains("dev")); - assertTrue(command.contains(TAGS_FLAG)); - assertTrue(command.contains("tag1")); - assertTrue(command.contains(VERBOSE_FLAG)); - assertTrue(command.contains(SPECS_FOLDER)); - } - - private void executeGaugeTask(Process process) { - GaugeExtension gauge = (GaugeExtension) project.getExtensions().findByName(GAUGE); - gauge.setInParallel(true); - gauge.setNodes(2); - gauge.setEnv("dev"); - gauge.setTags("tag1"); - gauge.setAdditionalFlags(VERBOSE_FLAG); - gauge.setSpecsDir(SPECS_FOLDER); - gauge.setGaugeRoot(GAUGE_ROOT); - task.executeGaugeSpecs(process); - } - - private void setExpectations(Process process) throws InterruptedException { - ArrayList expectedCommand = new ArrayList<>(); - expectedCommand.add(Paths.get(GAUGE_ROOT, "bin", GAUGE).toString()); - expectedCommand.add(PARALLEL_FLAG); - expectedCommand.add(NODES_FLAG); - expectedCommand.add("2"); - expectedCommand.add(ENV_FLAG); - expectedCommand.add("dev"); - expectedCommand.add(TAGS_FLAG); - expectedCommand.add("tag1"); - expectedCommand.add(VERBOSE_FLAG); - expectedCommand.add(SPECS_FOLDER); - - when(factory.create()).thenReturn(new ProcessBuilder(expectedCommand)); - when(process.waitFor()).thenReturn(0); - } -} diff --git a/plugin/src/test/java/com/thoughtworks/gauge/gradle/util/PropertyManagerTest.java b/plugin/src/test/java/com/thoughtworks/gauge/gradle/util/PropertyManagerTest.java deleted file mode 100644 index 98cf1a6..0000000 --- a/plugin/src/test/java/com/thoughtworks/gauge/gradle/util/PropertyManagerTest.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.thoughtworks.gauge.gradle.util; - -import com.thoughtworks.gauge.gradle.GaugeExtension; -import org.gradle.api.Project; -import org.gradle.api.tasks.SourceSet; -import org.gradle.api.tasks.SourceSetContainer; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; - -import java.io.File; -import java.util.HashMap; -import java.util.Map; - -import static org.hamcrest.CoreMatchers.containsString; -import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.*; - - -@RunWith(MockitoJUnitRunner.class) -public class PropertyManagerTest { - - - @Mock - private Project project; - - @Mock - private SourceSetContainer sourceSetContainer; - - private GaugeExtension extension; - - @Before - public void setUp() { - extension = new GaugeExtension(); - Map properties = new HashMap(); - properties.put("sourceSets",sourceSetContainer); - doReturn(properties).when(project).getProperties(); - } - - @Test - public void classpathShouldBeEmptyIfNoTesRuntimeDependencies() { - SourceSet config = mock(SourceSet.class, RETURNS_DEEP_STUBS); - when(config.getRuntimeClasspath().getAsPath()).thenReturn(""); - when(sourceSetContainer.getByName("test")).thenReturn(config); - PropertyManager manager = new PropertyManager(project, extension); - - manager.setProperties(); - - assertThat(extension.getClasspath(), containsString("")); - } - - @Test - public void classpathShouldIncludeTestRuntimeClasspathConfigurations() { - SourceSet config = mock(SourceSet.class, RETURNS_DEEP_STUBS); - when(config.getRuntimeClasspath().getAsPath()) - .thenReturn(String.join(File.pathSeparator, "blah.jar", "blah2.jar")); - when(sourceSetContainer.getByName("test")).thenReturn(config); - PropertyManager manager = new PropertyManager(project, extension); - - manager.setProperties(); - - assertThat(extension.getClasspath(), containsString(String.join(File.pathSeparator, "blah.jar", "blah2.jar"))); - } - - - -} \ No newline at end of file diff --git a/plugin/src/test/java/org/gauge/gradle/GaugeCommandTest.java b/plugin/src/test/java/org/gauge/gradle/GaugeCommandTest.java new file mode 100644 index 0000000..89b0264 --- /dev/null +++ b/plugin/src/test/java/org/gauge/gradle/GaugeCommandTest.java @@ -0,0 +1,132 @@ +package org.gauge.gradle; + +import org.gradle.api.Project; +import org.gradle.testfixtures.ProjectBuilder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class GaugeCommandTest { + + private Project project; + private GaugeExtension extension; + + @BeforeEach + void setUp() { + project = ProjectBuilder.builder().build(); + project.getPlugins().apply(GaugeConstants.GAUGE_PLUGIN_ID); + extension = project.getExtensions().findByType(GaugeExtension.class); + assertNotNull(extension, "extension not found"); + } + + @Test + void testSpecsDirCommand() { + assertEquals(List.of("specs"), new GaugeCommand(extension, project).getSpecsDir()); + extension.getSpecsDir().set("spec1 spec2"); + assertEquals(List.of("spec1", "spec2"), new GaugeCommand(extension, project).getSpecsDir()); + setProjectProperty(GaugeProperty.SPECS_DIR.getKey(), "project "); + assertEquals(List.of("project"), new GaugeCommand(extension, project).getSpecsDir()); + } + + @Test + void testEnvProperty() { + assertEquals(List.of(GaugeProperty.ENV.getFlag(), "default"), new GaugeCommand(extension, project).getEnvironment()); + extension.getEnv().set("env"); + assertEquals(List.of(GaugeProperty.ENV.getFlag(), "env"), new GaugeCommand(extension, project).getEnvironment()); + setProjectProperty(GaugeProperty.ENV.getKey(), "project "); + assertEquals(List.of(GaugeProperty.ENV.getFlag(), "project"), new GaugeCommand(extension, project).getEnvironment()); + } + + @Test + void testTagsProperty() { + assertEquals(Collections.emptyList(), new GaugeCommand(extension, project).getTags()); + extension.getTags().set("tag"); + assertEquals(List.of(GaugeProperty.TAGS.getFlag(), "tag"), new GaugeCommand(extension, project).getTags()); + setProjectProperty(GaugeProperty.TAGS.getKey(), "tag1 & tag2"); + assertEquals(List.of(GaugeProperty.TAGS.getFlag(), "tag1 & tag2"), new GaugeCommand(extension, project).getTags()); + } + + private String getProjectPath(final String projectDir) { + return Paths.get(projectDir).toAbsolutePath().toString(); + } + + @Test + void testProjectDir() { + assertEquals(List.of(GaugeProperty.PROJECT_DIR.getFlag(), project.getProjectDir().getAbsolutePath()), + new GaugeCommand(extension, project).getProjectDir()); + extension.getDir().set("/usr/ext"); + assertEquals(List.of(GaugeProperty.PROJECT_DIR.getFlag(), getProjectPath("/usr/ext")), + new GaugeCommand(extension, project).getProjectDir()); + extension.getDir().set("extDir"); + assertEquals(List.of(GaugeProperty.PROJECT_DIR.getFlag(), Path.of(project.getProjectDir().getPath(), "extDir").toString()), + new GaugeCommand(extension, project).getProjectDir()); + setProjectProperty(GaugeProperty.PROJECT_DIR.getKey(), "/project/dir"); + assertEquals(List.of(GaugeProperty.PROJECT_DIR.getFlag(), getProjectPath("/project/dir")), + new GaugeCommand(extension, project).getProjectDir()); + setProjectProperty(GaugeProperty.PROJECT_DIR.getKey(), "project/dir"); + assertEquals(List.of(GaugeProperty.PROJECT_DIR.getFlag(), Path.of(project.getProjectDir().getPath(), "project", "dir").toString()), + new GaugeCommand(extension, project).getProjectDir()); + } + + @Test + void testFlagsWithAdditionalFlagsProperty() { + assertEquals(Collections.emptyList(), new GaugeCommand(extension, project).getFlags()); + extension.getAdditionalFlags().set("--flag1"); + assertEquals(List.of("--flag1"), new GaugeCommand(extension, project).getFlags()); + setProjectProperty(GaugeProperty.ADDITIONAL_FLAGS.getKey(), "--simple-console -v "); + assertEquals(List.of("--simple-console", "-v"), new GaugeCommand(extension, project).getFlags()); + } + + @Test + void testFlagWithInParallelAndNodesProperty() { + assertEquals(Collections.emptyList(), new GaugeCommand(extension, project).getFlags()); + extension.getInParallel().set(true); + extension.getNodes().set(0); + assertEquals(List.of(GaugeProperty.IN_PARALLEL.getFlag()), new GaugeCommand(extension, project).getFlags()); + extension.getNodes().set(2); + assertEquals(List.of(GaugeProperty.IN_PARALLEL.getFlag(), GaugeProperty.NODES.getFlag(), 2), + new GaugeCommand(extension, project).getFlags()); + setProjectProperty(GaugeProperty.IN_PARALLEL.getKey(), false); + assertEquals(Collections.emptyList(), new GaugeCommand(extension, project).getFlags()); + setProjectProperty(GaugeProperty.IN_PARALLEL.getKey(), "true"); + setProjectProperty(GaugeProperty.NODES.getKey(), 3); + assertEquals(List.of(GaugeProperty.IN_PARALLEL.getFlag(), GaugeProperty.NODES.getFlag(), 3), + new GaugeCommand(extension, project).getFlags()); + } + + @Test + void testRepeatAndFailedFlagsWithAdditionalFlagsProperty() { + extension.getInParallel().set(true); + extension.getNodes().set(2); + final var flag = "--flag1"; + extension.getAdditionalFlags().set(flag); + assertEquals(List.of(flag, GaugeProperty.IN_PARALLEL.getFlag(), GaugeProperty.NODES.getFlag(), 2), + new GaugeCommand(extension, project).getFlags()); + // when --repeat or --failed flag is provided + extension.getAdditionalFlags().set("--failed " + flag); + // then it should exclude --parallel and --n flags + assertEquals(List.of("--failed", flag), new GaugeCommand(extension, project).getFlags()); + setProjectProperty(GaugeProperty.ADDITIONAL_FLAGS.getKey(), "-v --repeat"); + assertEquals(List.of("-v", "--repeat"), new GaugeCommand(extension, project).getFlags()); + } + + @Test + void testCanGetExecutablePath() { + assertEquals("gauge", new GaugeCommand(extension, project).getExecutable()); + extension.getGaugeRoot().set("/opt/gauge"); + assertEquals(Path.of("/opt/gauge/bin/gauge").toString(), + new GaugeCommand(extension, project).getExecutable()); + } + + private void setProjectProperty(final String key, final Object value) { + project.getExtensions().getExtraProperties().set(key, value); + } + +} diff --git a/plugin/src/test/java/org/gauge/gradle/GaugeExtensionTest.java b/plugin/src/test/java/org/gauge/gradle/GaugeExtensionTest.java new file mode 100644 index 0000000..f069349 --- /dev/null +++ b/plugin/src/test/java/org/gauge/gradle/GaugeExtensionTest.java @@ -0,0 +1,62 @@ +package org.gauge.gradle; + +import org.gradle.api.Project; +import org.gradle.testfixtures.ProjectBuilder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class GaugeExtensionTest { + + private GaugeExtension extension; + + @BeforeEach + void setUp() { + Project project = ProjectBuilder.builder().build(); + project.getPlugins().apply(GaugeConstants.GAUGE_PLUGIN_ID); + extension = project.getExtensions().findByType(GaugeExtension.class); + assertNotNull(extension, "extension should not be null"); + } + + @Test + public void testDefaultProperties() { + assertEquals("default", extension.getEnv().get()); + assertFalse(extension.getTags().isPresent()); + assertEquals("specs", extension.getSpecsDir().get()); + assertFalse(extension.getInParallel().get()); + assertFalse(extension.getNodes().isPresent()); + assertTrue(extension.getEnvironmentVariables().get().isEmpty()); + assertFalse(extension.getAdditionalFlags().isPresent()); + assertFalse(extension.getGaugeRoot().isPresent()); + } + + @Test + public void testExtensionProperties() { + setExtensionProperties(); + assertEquals("test", extension.getEnv().get()); + assertEquals("(tag1|tag2)&tag3", extension.getTags().get()); + assertEquals("extension specs", extension.getSpecsDir().get()); + assertTrue(extension.getInParallel().get()); + assertEquals(4, extension.getNodes().get()); + assertEquals(Map.of("key", "value"), extension.getEnvironmentVariables().get()); + assertEquals("--verbose --flag", extension.getAdditionalFlags().get()); + assertEquals("/usr/local/", extension.getGaugeRoot().get()); + } + + private void setExtensionProperties() { + extension.getEnv().set("test"); + extension.getTags().set("(tag1|tag2)&tag3"); + extension.getSpecsDir().set("extension specs"); + extension.getInParallel().set(true); + extension.getNodes().set(4); + extension.getEnvironmentVariables().set(Map.of("key", "value")); + extension.getAdditionalFlags().set("--verbose --flag"); + extension.getGaugeRoot().set("/usr/local/"); + } +} diff --git a/plugin/src/test/java/org/gauge/gradle/GaugePluginTest.java b/plugin/src/test/java/org/gauge/gradle/GaugePluginTest.java new file mode 100644 index 0000000..2af6b95 --- /dev/null +++ b/plugin/src/test/java/org/gauge/gradle/GaugePluginTest.java @@ -0,0 +1,52 @@ +package org.gauge.gradle; + +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.tasks.TaskContainer; +import org.gradle.testfixtures.ProjectBuilder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.gauge.gradle.GaugeConstants.GAUGE_CLASSPATH_TASK; +import static org.gauge.gradle.GaugeConstants.GAUGE_PLUGIN_ID; +import static org.gauge.gradle.GaugeConstants.GAUGE_TASK; +import static org.gauge.gradle.GaugeConstants.GAUGE_TASK_GROUP; +import static org.gauge.gradle.GaugeConstants.GAUGE_VALIDATE_TASK; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class GaugePluginTest { + private Project project; + + @BeforeEach + public void setUp() { + project = ProjectBuilder.builder().build(); + } + + @Test + public void pluginShouldBeAddedOnApply() { + project.getPluginManager().apply(GAUGE_PLUGIN_ID); + assertTrue(project.getPluginManager().hasPlugin("java")); + assertTrue(project.getPluginManager().hasPlugin(GAUGE_PLUGIN_ID)); + assertTrue(project.getPlugins().getPlugin(GAUGE_PLUGIN_ID) instanceof GaugePlugin); + assertFalse(project.getPlugins().getPlugin(GAUGE_PLUGIN_ID) instanceof JavaPlugin); + } + + @Test + public void taskShouldBeAddedOnApply() { + project.getPluginManager().apply(GAUGE_PLUGIN_ID); + TaskContainer tasks = project.getTasks(); + var tasksMap = tasks.getAsMap(); + Task gauge = tasksMap.get(GAUGE_TASK); + assertEquals(GAUGE_TASK_GROUP, gauge.getGroup()); + assertTrue(gauge instanceof GaugeTask); + Task classpath = tasksMap.get(GAUGE_CLASSPATH_TASK); + assertEquals(GAUGE_TASK_GROUP, classpath.getGroup()); + assertTrue(classpath instanceof GaugeClasspathTask); + Task validate = tasksMap.get(GAUGE_VALIDATE_TASK); + assertEquals(GAUGE_TASK_GROUP, validate.getGroup()); + assertTrue(validate instanceof GaugeValidateTask); + } +} \ No newline at end of file diff --git a/sample/build.gradle b/sample/build.gradle index f9d4c9f..69e017c 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,48 +1,57 @@ -import com.thoughtworks.gauge.gradle.GaugeTask +import org.gauge.gradle.GaugeTask plugins { id 'java' - id 'org.gauge' version '1.8.1' + id 'jvm-test-suite' + id 'org.gauge' + id 'pmd' } -group = "my-gauge-tests" -version = "1.1.0" - -description = "My Gauge Tests" - -sourceCompatibility = 1.8 -targetCompatibility = 1.8 - repositories { + mavenLocal() mavenCentral() } +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +pmd { + consoleOutput = true +} + sourceSets { spec { - java.srcDir 'src/spec/java' + java.srcDirs('src/spec/') } test { compileClasspath += sourceSets.spec.runtimeClasspath + runtimeClasspath += sourceSets.spec.runtimeClasspath } } dependencies { - implementation 'org.seleniumhq.selenium:selenium-firefox-driver:3.141.59' - specImplementation 'com.thoughtworks.gauge:gauge-java:+' testImplementation 'com.thoughtworks.gauge:gauge-java:+' - testImplementation 'net.sourceforge.htmlunit:webdriver:2.6' - testImplementation 'junit:junit:4.13.2' + testImplementation("org.junit.jupiter:junit-jupiter:5.+") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") } +tasks.register('cleanBuildDir', Delete) { + delete "${projectDir}/logs" + delete "${projectDir}/out" + delete "${projectDir}/reports" +} +tasks.clean.dependsOn("cleanBuildDir") + gauge { specsDir = 'specs' inParallel = true nodes = 4 - env = 'ci' additionalFlags = '--verbose' } -task gaugeDev(type: GaugeTask) { +tasks.register('gaugeDev', GaugeTask) { doFirst { gauge { specsDir = 'specs' @@ -54,26 +63,23 @@ task gaugeDev(type: GaugeTask) { } } -task gaugeTest(type: GaugeTask) { +tasks.register('gaugeDevRepeat', GaugeTask) { + dependsOn("gaugeDev") doFirst { gauge { - specsDir = 'specs' - inParallel = true - nodes = 4 - env = 'test' - additionalFlags = '--verbose' + additionalFlags = '--repeat --simple-console' } } } -task gaugeCi(type: GaugeTask) { - doFirst { - gauge { - specsDir = 'specs' - inParallel = true - nodes = 3 - env = 'ci' - additionalFlags = '--verbose' - } - } +tasks.withType(Test).configureEach { + // https://docs.gradle.org/8.2/userguide/upgrading_version_8.html#test_framework_implementation_dependencies + // Using JUnitPlatform for running tests + useJUnitPlatform() } + +tasks.withType(GaugeTask).configureEach { + // https://docs.gradle.org/8.2/userguide/upgrading_version_8.html#test_task_default_classpath + testClassesDirs = sourceSets.test.output + sourceSets.spec.output + classpath = sourceSets.test.runtimeClasspath + sourceSets.spec.runtimeClasspath +} \ No newline at end of file diff --git a/sample/env/ci/user.properties b/sample/env/ci/user.properties deleted file mode 100644 index 7b914a3..0000000 --- a/sample/env/ci/user.properties +++ /dev/null @@ -1,16 +0,0 @@ -# default.properties -# properties set here will be available to the test execution as environment variables - -# sample_key = sample_value - -#The path to the gauge reports directory. Should be either relative to the project directory or an absolute path -gauge_reports_dir = reportsCI - -#Set as false if gauge reports should not be overwritten on each execution. A new time-stamped directory will be created on each execution. -overwrite_reports = true - -# Set to false to disable screenshots on failure in reports. -screenshot_on_failure = true - -# The path to the gauge logs directory. Should be either relative to the project directory or an absolute path -logs_directory = logsCI \ No newline at end of file diff --git a/sample/env/default/default.properties b/sample/env/default/default.properties index e1920d3..9a75d7b 100644 --- a/sample/env/default/default.properties +++ b/sample/env/default/default.properties @@ -10,7 +10,7 @@ gauge_reports_dir = reports overwrite_reports = true # Set to false to disable screenshots on failure in reports. -screenshot_on_failure = true +screenshot_on_failure = false # The path to the gauge logs directory. Should be either relative to the project directory or an absolute path logs_directory = logs diff --git a/sample/env/dev/user.properties b/sample/env/dev/user.properties index 624402a..5595fca 100644 --- a/sample/env/dev/user.properties +++ b/sample/env/dev/user.properties @@ -4,13 +4,13 @@ # sample_key = sample_value #The path to the gauge reports directory. Should be either relative to the project directory or an absolute path -gauge_reports_dir = reportsDev +gauge_reports_dir = reports/dev #Set as false if gauge reports should not be overwritten on each execution. A new time-stamped directory will be created on each execution. overwrite_reports = true # Set to false to disable screenshots on failure in reports. -screenshot_on_failure = true +screenshot_on_failure = false # The path to the gauge logs directory. Should be either relative to the project directory or an absolute path -logs_directory = logsDev \ No newline at end of file +logs_directory = logs/dev \ No newline at end of file diff --git a/sample/env/test/user.properties b/sample/env/test/user.properties deleted file mode 100644 index 1e747fa..0000000 --- a/sample/env/test/user.properties +++ /dev/null @@ -1,16 +0,0 @@ -# default.properties -# properties set here will be available to the test execution as environment variables - -# sample_key = sample_value - -#The path to the gauge reports directory. Should be either relative to the project directory or an absolute path -gauge_reports_dir = reportsTest - -#Set as false if gauge reports should not be overwritten on each execution. A new time-stamped directory will be created on each execution. -overwrite_reports = true - -# Set to false to disable screenshots on failure in reports. -screenshot_on_failure = true - -# The path to the gauge logs directory. Should be either relative to the project directory or an absolute path -logs_directory = logsTest \ No newline at end of file diff --git a/sample/settings.gradle b/sample/settings.gradle new file mode 100644 index 0000000..67bf655 --- /dev/null +++ b/sample/settings.gradle @@ -0,0 +1,7 @@ +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + } + includeBuild "../plugin" +} \ No newline at end of file diff --git a/sample/specs/hello_world1.spec b/sample/specs/hello_world1.spec index e719d7c..cbf5ff3 100644 --- a/sample/specs/hello_world1.spec +++ b/sample/specs/hello_world1.spec @@ -1,33 +1,30 @@ -Specification Heading -===================== +# Specification Heading This is an executable specification file. This file follows markdown syntax. Every heading in this file denotes a scenario. Every bulleted point denotes a step. To execute this specification, use - gauge specs/hello_world.spec + gauge run specs/hello_world.spec * A context step which gets executed before every scenario -First scenario --------------- +## First scenario tags: hello world, first test * Say "hello" to "gauge" - -Second scenario for the specification -------------------------------------- +## Second scenario for the specification This is the second scenario in this specification * Say "hello again" to "gauge" * Step that takes a table - |Product| Description | - |-------|-----------------------------| - |Gauge |Test automation with ease | - |Mingle |Agile project management | - |Snap |Hosted continuous integration| - |Gocd |Continuous delivery platform | + + |Product|Description | + |-------|-----------------------------| + |Gauge |Test automation with ease | + |Mingle |Agile project management | + |Snap |Hosted continuous integration| + |Gocd |Continuous delivery platform | diff --git a/sample/specs/hello_world2.spec b/sample/specs/hello_world2.spec index e719d7c..cbf5ff3 100644 --- a/sample/specs/hello_world2.spec +++ b/sample/specs/hello_world2.spec @@ -1,33 +1,30 @@ -Specification Heading -===================== +# Specification Heading This is an executable specification file. This file follows markdown syntax. Every heading in this file denotes a scenario. Every bulleted point denotes a step. To execute this specification, use - gauge specs/hello_world.spec + gauge run specs/hello_world.spec * A context step which gets executed before every scenario -First scenario --------------- +## First scenario tags: hello world, first test * Say "hello" to "gauge" - -Second scenario for the specification -------------------------------------- +## Second scenario for the specification This is the second scenario in this specification * Say "hello again" to "gauge" * Step that takes a table - |Product| Description | - |-------|-----------------------------| - |Gauge |Test automation with ease | - |Mingle |Agile project management | - |Snap |Hosted continuous integration| - |Gocd |Continuous delivery platform | + + |Product|Description | + |-------|-----------------------------| + |Gauge |Test automation with ease | + |Mingle |Agile project management | + |Snap |Hosted continuous integration| + |Gocd |Continuous delivery platform | diff --git a/sample/specs/hello_world3.spec b/sample/specs/hello_world3.spec index e719d7c..cbf5ff3 100644 --- a/sample/specs/hello_world3.spec +++ b/sample/specs/hello_world3.spec @@ -1,33 +1,30 @@ -Specification Heading -===================== +# Specification Heading This is an executable specification file. This file follows markdown syntax. Every heading in this file denotes a scenario. Every bulleted point denotes a step. To execute this specification, use - gauge specs/hello_world.spec + gauge run specs/hello_world.spec * A context step which gets executed before every scenario -First scenario --------------- +## First scenario tags: hello world, first test * Say "hello" to "gauge" - -Second scenario for the specification -------------------------------------- +## Second scenario for the specification This is the second scenario in this specification * Say "hello again" to "gauge" * Step that takes a table - |Product| Description | - |-------|-----------------------------| - |Gauge |Test automation with ease | - |Mingle |Agile project management | - |Snap |Hosted continuous integration| - |Gocd |Continuous delivery platform | + + |Product|Description | + |-------|-----------------------------| + |Gauge |Test automation with ease | + |Mingle |Agile project management | + |Snap |Hosted continuous integration| + |Gocd |Continuous delivery platform | diff --git a/sample/specs/hello_world4.spec b/sample/specs/hello_world4.spec index e719d7c..cbf5ff3 100644 --- a/sample/specs/hello_world4.spec +++ b/sample/specs/hello_world4.spec @@ -1,33 +1,30 @@ -Specification Heading -===================== +# Specification Heading This is an executable specification file. This file follows markdown syntax. Every heading in this file denotes a scenario. Every bulleted point denotes a step. To execute this specification, use - gauge specs/hello_world.spec + gauge run specs/hello_world.spec * A context step which gets executed before every scenario -First scenario --------------- +## First scenario tags: hello world, first test * Say "hello" to "gauge" - -Second scenario for the specification -------------------------------------- +## Second scenario for the specification This is the second scenario in this specification * Say "hello again" to "gauge" * Step that takes a table - |Product| Description | - |-------|-----------------------------| - |Gauge |Test automation with ease | - |Mingle |Agile project management | - |Snap |Hosted continuous integration| - |Gocd |Continuous delivery platform | + + |Product|Description | + |-------|-----------------------------| + |Gauge |Test automation with ease | + |Mingle |Agile project management | + |Snap |Hosted continuous integration| + |Gocd |Continuous delivery platform | diff --git a/sample/specs/specific/hello_world.spec b/sample/specs/specific/hello_world.spec index e719d7c..5b3f80c 100644 --- a/sample/specs/specific/hello_world.spec +++ b/sample/specs/specific/hello_world.spec @@ -1,33 +1,30 @@ -Specification Heading -===================== +# Specification Heading This is an executable specification file. This file follows markdown syntax. Every heading in this file denotes a scenario. Every bulleted point denotes a step. To execute this specification, use - gauge specs/hello_world.spec + gauge run specs/hello_world.spec * A context step which gets executed before every scenario -First scenario --------------- +## First scenario -tags: hello world, first test +tags: hello world, first test, haroon * Say "hello" to "gauge" - -Second scenario for the specification -------------------------------------- +## Second scenario for the specification This is the second scenario in this specification * Say "hello again" to "gauge" * Step that takes a table - |Product| Description | - |-------|-----------------------------| - |Gauge |Test automation with ease | - |Mingle |Agile project management | - |Snap |Hosted continuous integration| - |Gocd |Continuous delivery platform | + + |Product|Description | + |-------|-----------------------------| + |Gauge |Test automation with ease | + |Mingle |Agile project management | + |Snap |Hosted continuous integration| + |Gocd |Continuous delivery platform | diff --git a/sample/src/spec/java/com/thoughtworks/gauge/gradle/sample/Main.java b/sample/src/spec/java/com/thoughtworks/gauge/gradle/sample/Main.java deleted file mode 100644 index f826841..0000000 --- a/sample/src/spec/java/com/thoughtworks/gauge/gradle/sample/Main.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.thoughtworks.gauge.gradle.sample; - -public class Main { - public static void main(String[] args) { - Test test = new Test("test_value"); - System.out.println("com.thoughtworks.gauge.gradle.sample.Test Value is : " + test.getValue()); - } -} diff --git a/sample/src/spec/java/com/thoughtworks/gauge/gradle/sample/Test.java b/sample/src/spec/java/com/thoughtworks/gauge/gradle/sample/Test.java deleted file mode 100644 index 4f34d21..0000000 --- a/sample/src/spec/java/com/thoughtworks/gauge/gradle/sample/Test.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.thoughtworks.gauge.gradle.sample; - -public class Test { - private String value; - - public Test(String value) { - this.value = value; - } - - public String getValue() { - return value; - } -} diff --git a/sample/src/spec/java/org/gauge/gradle/sample/Sample.java b/sample/src/spec/java/org/gauge/gradle/sample/Sample.java new file mode 100644 index 0000000..95f605c --- /dev/null +++ b/sample/src/spec/java/org/gauge/gradle/sample/Sample.java @@ -0,0 +1,13 @@ +package org.gauge.gradle.sample; + +public class Sample { + private final String value; + + public Sample(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/sample/src/test/java/com/thoughtworks/gauge/gradle/sample/StepImplementation.java b/sample/src/test/java/org/gauge/gradle/sample/SampleSteps.java similarity index 65% rename from sample/src/test/java/com/thoughtworks/gauge/gradle/sample/StepImplementation.java rename to sample/src/test/java/org/gauge/gradle/sample/SampleSteps.java index 6ca4caa..f6acde0 100644 --- a/sample/src/test/java/com/thoughtworks/gauge/gradle/sample/StepImplementation.java +++ b/sample/src/test/java/org/gauge/gradle/sample/SampleSteps.java @@ -1,17 +1,13 @@ -package com.thoughtworks.gauge.gradle.sample; +package org.gauge.gradle.sample; import com.thoughtworks.gauge.Step; import com.thoughtworks.gauge.Table; import com.thoughtworks.gauge.TableRow; -import org.junit.Assert; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.htmlunit.HtmlUnitDriver; +import org.junit.jupiter.api.Assertions; -public class StepImplementation { +public class SampleSteps { @Step("Say to ") public void helloWorld(String greeting, String name) { - WebDriver driver = new HtmlUnitDriver(); - driver.close(); System.out.println(greeting + ", " + name); } @@ -26,7 +22,7 @@ public void stepWithTable(Table table) { @Step("A context step which gets executed before every scenario") public void contextStep() { - Test test = new Test("test"); - Assert.assertTrue(test.getValue().equals("test")); + Sample sample = new Sample("test"); + Assertions.assertEquals("test", sample.getValue()); } } diff --git a/scripts/pre_build.sh b/scripts/pre_build.sh deleted file mode 100644 index e9ef10f..0000000 --- a/scripts/pre_build.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -sudo apt-get update -sudo apt-get install gauge -gauge install java \ No newline at end of file diff --git a/scripts/test.sh b/scripts/test.sh deleted file mode 100644 index ea65799..0000000 --- a/scripts/test.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash -cd plugin -../gradlew clean test diff --git a/settings.gradle b/settings.gradle index 7937daf..0ed6904 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,9 @@ -rootProject.name = 'gauge-gradle-plugin' -include 'plugin' +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + } + includeBuild "plugin" +} include 'sample' +rootProject.name = "gauge-gradle-plugin"