diff --git a/.idea/DD2480-Big-Brain-CI.iml b/.idea/DD2480-Big-Brain-CI.iml new file mode 100644 index 00000000..40981982 --- /dev/null +++ b/.idea/DD2480-Big-Brain-CI.iml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 00000000..75e314af --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 00000000..99a3e779 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 00000000..2739aa50 --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..f4bb4251 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 00000000..e96534fb --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..9661ac71 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 00000000..e916faf9 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,335 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1580655170109 + + + 1580739487717 + + + 1580831211583 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ContinuousIntegrationServer.java b/ContinuousIntegrationServer.java deleted file mode 100644 index 9adb2ff0..00000000 --- a/ContinuousIntegrationServer.java +++ /dev/null @@ -1,45 +0,0 @@ -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.ServletException; - -import java.io.IOException; - -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.handler.AbstractHandler; - -/** - Skeleton of a ContinuousIntegrationServer which acts as webhook - See the Jetty documentation for API documentation of those classes. -*/ -public class ContinuousIntegrationServer extends AbstractHandler -{ - public void handle(String target, - Request baseRequest, - HttpServletRequest request, - HttpServletResponse response) - throws IOException, ServletException - { - response.setContentType("text/html;charset=utf-8"); - response.setStatus(HttpServletResponse.SC_OK); - baseRequest.setHandled(true); - - System.out.println(target); - - // here you do all the continuous integration tasks - // for example - // 1st clone your repository - // 2nd compile the code - - response.getWriter().println("CI job done"); - } - - // used to start the CI server in command line - public static void main(String[] args) throws Exception - { - Server server = new Server(8080); - server.setHandler(new ContinuousIntegrationServer()); - server.start(); - server.join(); - } -} diff --git a/README.md b/README.md index 10f7f2d7..d3e5f045 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,46 @@ -The smallest Java Continuous Integration server for Github -=========================================================== +# DD2480-CI +CI is the second assignment in the course DD2480 Software Engineering Fundamentals. The goal of the assignment is to implement a continuous integration server that first clones a repository on GitHub when there are any changes on it, builds the project and notifies the user. -Here is a tiny CI server skeleton implemented in Java for educational purposes. It is meant to be called as webhook by Github. The HTTP part of it is based on Jetty. +## Getting Started +This implementation of a CI-server is made with Java SDK 11 and IntelliJ; therefore make sure you have these installed. The project that uses the CI-server needs to have `Gradle` installed, due to it being used by the CI-server. There must exist a `build.gradle` file in the project. This file must contain the dependencies needed for the project to run. -We assume here that you have a standard Linux machine (eg with Ubuntu), with Java installed. +## Testing +Tests are written using the `JUnit` library. -We first checkout this repository: -``` -git clone https://github.com/monperrus/smallest-java-ci -cd smallest-java-ci -``` +## Using the CI-server +1. Go to the assessment branch +1. Make a change somewhere +1. Push changes to `GitHub` +1. The CI-server will do its part and notify about the results -We then download the required dependencies: -``` -JETTY_VERSION=7.0.2.v20100331 -wget -U none http://repo1.maven.org/maven2/org/eclipse/jetty/aggregate/jetty-all/$JETTY_VERSION/jetty-all-$JETTY_VERSION.jar -wget -U none http://repo1.maven.org/maven2/javax/servlet/servlet-api/2.5/servlet-api-2.5.jar -#For linux users: -curl -LO --tlsv1 https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip -unzip ngrok-stable-linux-amd64.zip -#For Mac user: -curl -LO --tlsv1 https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-darwin-386.zip -unzip ngrok-stable-darwin-386.zip -``` +## What the server is doing +1. Clone the project using `JGit`, this happens when webhook sees a change (such as a push) in the repository +1. Compile and run the test using `Gradle` and the Gradle tooling API `GradleConnector` (can be done using the command `gradlew build` in the terminal) +1. Notify the user using the `XML-file` generated by `Gradle` -We compile the skeleton the continuous integration server: -``` -javac -cp servlet-api-2.5.jar:jetty-all-$JETTY_VERSION.jar ContinuousIntegrationServer.java -``` +## Contributing +### Branches +Each issue should be worked on separately in its own branch; issue #420 should only be worked on in the branch `issue-420`. Solutions to issues can only be merged to `master` after a successful review of a pull request. -We run the server on the machine, and we may make it visible on the Internet thanks to [Ngrok](https://ngrok.com/): -``` -# open a first terminal window -JETTY_VERSION=7.0.2.v20100331 -java -cp .:servlet-api-2.5.jar:jetty-all-$JETTY_VERSION.jar ContinuousIntegrationServer +There are two branches. One `master` and one called `assessment`, the `assessment` branch is simulating a project with a main class and a simple test for it. This is used to show the functionality of the CI-server. The `master` branch is the server part. -# open a second terminal window -# this gives you the public URL of your CI server to set in Github -# copy-paste the forwarding URL "Forwarding http://8929b010.ngrok.io -> localhost:8080" -# note that this url is short-lived, and is reset everytime you run ngrok -./ngrok http 8080 +### Documentation +* Every method should have a `JavaDoc` comment explaining what it does, the parameters and the return value. -``` +### Statement of Contributions +_Axel Kennedal_ -We configure our Github repository: + +_My Helmisaari_ + * this README -* go to `Settings >> Webhooks`, click on `Add webhook`. -* paste the forwarding URL (eg `http://8929b010.ngrok.io`) in field `Payload URL`) and send click on `Add webhook`. In the simplest setting, nothing more is required. + +_Henrik Mellin_ -We test that everything works: - -* go to tp check that the CI server is running locally -* go to your Ngrok forwarding URL (eg ) to check that the CI server is visible from the internet, hence visible from Github -* make a commit in your repository -* observe the result, in two ways: - * locally: in the console of your first terminal window, observe the requested URL printed on the console - * on github: go to `Settings >> Webhooks` in your repo, click on your newly created webhook, scroll down to "Recent Deliveries", click on the last delivery and the on the `Response tab`, you'll see the output of your server `CI job done` - * on ngrok: raise the terminal window with Ngrok, and you'll also the see URLs requested by Github - -We shutdown everything: - -* `Ctrl-C` in the ngrok terminal window -* `Ctrl-C` in the ngrok java window -* delete the webhook in the webhook configuration page. - -Notes: -* by default, Github delivers a `push` JSON payloard, documented here: , this information can be used to get interesting information about the commit that has just been pushed. + +_Mathieu Desponds_ + * this README + +## Troubleshooting +### IntelliJ +If the source and test folders are not showing up in the project view, try following [these instructions](https://stackoverflow.com/questions/5816419/intellij-does-not-show-project-folders). diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..fa5322d0 --- /dev/null +++ b/build.gradle @@ -0,0 +1,39 @@ +plugins { + id 'java' +} + +group 'org.example' +version '1.0-SNAPSHOT' + +sourceCompatibility = 11 + +repositories { + mavenCentral() + jcenter() +} + +dependencies { + implementation group: 'javax.servlet', name: 'javax.servlet-api', version: '3.0.1' + implementation group: 'org.eclipse.jetty', name: 'jetty-server', version: '9.4.26.v20200117' + implementation group: 'org.json', name: 'json', version: '20190722' + implementation group: 'org.gradle', name: 'gradle-tooling-api', version: '4.3' + testImplementation( + 'junit:junit:4.12', + 'org.junit.jupiter:junit-jupiter-api:5.4.2' + ) + testRuntime( + 'org.junit.jupiter:junit-jupiter-engine:5.4.2', + 'org.junit.vintage:junit-vintage-engine:5.4.2' + ) + + compile group: 'org.eclipse.jgit', name: 'org.eclipse.jgit', version: '5.6.0.201912101111-r' + compile group: 'commons-io', name: 'commons-io', version: '2.6' + + testRuntime( + 'org.junit.jupiter:junit-jupiter-engine:5.4.2', + 'org.junit.vintage:junit-vintage-engine:5.4.2' + ) +} +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..f3d88b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..1b16c34a --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 00000000..2fe81a7d --- /dev/null +++ b/gradlew @@ -0,0 +1,183 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# 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 +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"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# 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 + ;; +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" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +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. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +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 +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 + +# 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 + # 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\"" + fi + i=`expr $i + 1` + 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" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..24467a14 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,100 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +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% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="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 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..b5ec8bfd --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'DD2480-Big-Brain-CI' + diff --git a/src/main/java/ContinuousIntegrationServer.java b/src/main/java/ContinuousIntegrationServer.java new file mode 100644 index 00000000..dbdef915 --- /dev/null +++ b/src/main/java/ContinuousIntegrationServer.java @@ -0,0 +1,106 @@ +import org.apache.commons.io.FileUtils; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.gradle.tooling.BuildLauncher; +import org.gradle.tooling.GradleConnector; +import org.gradle.tooling.ProjectConnection; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.util.List; + +import static java.util.Collections.singleton; + +/** + Skeleton of a ContinuousIntegrationServer which acts as webhook + See the Jetty documentation for API documentation of those classes. +*/ +public class ContinuousIntegrationServer extends AbstractHandler +{ + private static ProjectConnection connection; + public void handle(String target, + Request baseRequest, + HttpServletRequest request, + HttpServletResponse response) + throws IOException + { + response.setContentType("text/html;charset=utf-8"); + response.setStatus(HttpServletResponse.SC_OK); + baseRequest.setHandled(true); + + // here you do all the continuous integration tasks + // for example + // 1st clone your repository + File f = cloneTheProject("https://github.com/myhelmisaari/DD2480-Big-Brain-CI.git"); + // 2nd compile the code + build(f); + notifyUser(); + } + + // used to start the CI server in command line + public static void main(String[] args) throws Exception + { + Server server = new Server(8083); + server.setHandler(new ContinuousIntegrationServer()); + server.start(); + server.join(); + } + + /** + * Clones the assessment branch from the GitHub repository given as argument. + * @param gitHubHTTPS the https of the repository we want to clone + * @return the file that contains the file of the GitHub project + * @throws IOException + */ + private static File cloneTheProject(String gitHubHTTPS) throws IOException{ + File localPath = new File("assessment/"); + FileUtils.deleteDirectory(localPath); + localPath = new File("assessment/"); + try { + Git.cloneRepository() + .setURI(gitHubHTTPS) + .setDirectory(localPath)// #1 + .setBranchesToClone(singleton("refs/heads/assessment")) + .setBranch("refs/heads/assessment") + .call(); + } catch (GitAPIException ex) { + System.out.println("Exception with the Git API"); + } + return localPath; + } + + /** + * This method will build (compile an test) the project contained in the file given + * as argument + * @param file The file that contains the project we want to build + */ + private static void build(File file){ + connection = GradleConnector.newConnector() + .forProjectDirectory(file).connect(); + BuildLauncher build = connection.newBuild(); + try { + build.run(); + }finally { + connection.close(); + } + } + + /** + * This functions notify the user of what append when building. + * It will notify the user sending him a message on Slack with information on + * testSuiteName on the hostName, on the number of tests, on skippedCount + * on the number of errors, on the number of failure, on the time taken to run the tests + * and on the timestamp + */ + private static void notifyUser() { + List testResults = TestResultsParser.getResults(); + + String message = testResults.toString(); + SlackIntegration.sendMessage(message); + } +} diff --git a/src/main/java/SlackIntegration.java b/src/main/java/SlackIntegration.java new file mode 100644 index 00000000..b7e0addd --- /dev/null +++ b/src/main/java/SlackIntegration.java @@ -0,0 +1,63 @@ +import org.json.JSONObject; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +/** + * This class provides functionality for sending messages to a Slack channel, through a Slack app + * configured at https://api.slack.com/apps/ATFSMNS4Q. + */ +public class SlackIntegration { + // needs to be base64 since Slack will find it as publicly accessible otherwise and disable it + private static final String bigBrainSlackHookURLBase64 = "aHR0cHM6Ly9ob29rcy5zbGFjay5jb20vc2VydmljZXMvVFNaOE41TkJZL0JUSjNRRjg3UC92TzlwTnIwa3lNZHJXbTVNbmprQjNLeWo="; + + /** + * Send a message in the Slack channel that this class is configured for. + * @param message the string you want to send. May contain emojis like ":clap:" etc. + * @return true if the message was sent successfully, false otherwise. + */ + public static boolean sendMessage(String message) { + return sendPOST(bigBrainSlackHookURLBase64, new JSONObject().put("text", message)); + } + + /** + * Send an HTTP POST request. + * @param URL the destination of the request, base64 encoded. + * @param requestBody what JSON you want to send. + * @return true if successful, false otherwise. + */ + private static boolean sendPOST(String URL, JSONObject requestBody) { + HttpClient client = HttpClient.newHttpClient(); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(decodeBase64(URL))) + .POST(HttpRequest.BodyPublishers.ofString(requestBody.toString())) + .header("Content-type", "application/json") + .build(); + + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + System.out.println(response); + return response.statusCode() == 200; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + private static String decodeBase64(String encoded) { + byte[] decodedBytes = java.util.Base64.getDecoder().decode(encoded); + return new String(decodedBytes); + } + + /** + * Send a hello message in the Slack channel, for testing. + * @return true if successful, false otherwise. + */ + protected static boolean sendHello() { + JSONObject requestBody = new JSONObject().put("text", ":clap: Hello! This is a test :)"); + return sendPOST(bigBrainSlackHookURLBase64, requestBody); + } +} diff --git a/src/main/java/TestResultsParser.java b/src/main/java/TestResultsParser.java new file mode 100644 index 00000000..fd29b4ae --- /dev/null +++ b/src/main/java/TestResultsParser.java @@ -0,0 +1,109 @@ +import org.w3c.dom.Document; +import org.w3c.dom.Node; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.File; +import java.io.FilenameFilter; +import java.util.LinkedList; +import java.util.List; + +/** + * This class can parse the gradle test results generated in build/test-results/test/. + */ +public class TestResultsParser { + private static final File resultsFolder = new File("build/test-results/test/"); + private static List parsedTestResults; + + /** + * @return the results of the tests. + */ + public static List getResults() { + if (parsedTestResults != null) return parsedTestResults; + parsedTestResults = new LinkedList<>(); + + // find the right xml file + //File resultsFile = new File("build/test-results/test/TEST-SlackIntegrationTests.xml"); + for (File file : resultsFolder.listFiles((dir, name) -> name.endsWith(".xml") && name.startsWith("TEST-"))) { + parseResultFile(file); + } + + return parsedTestResults; + } + + public static File getResultsFolder() { return resultsFolder; } + + /** + * Parse a single test results XML file. + * @param resultsFile the file to parse + */ + private static void parseResultFile(File resultsFile) { + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + + try { + DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + Document document = documentBuilder.parse(resultsFile); + Node testSuiteNode = document.getFirstChild(); + + parsedTestResults.add(new TestResult( + getAttributeStringValue(testSuiteNode, "name"), + getAttributeStringValue(testSuiteNode, "hostname"), + getAttributeIntValue(testSuiteNode, "tests"), + getAttributeIntValue(testSuiteNode, "skipped"), + getAttributeIntValue(testSuiteNode, "errors"), + getAttributeIntValue(testSuiteNode, "failures"), + getAttributeStringValue(testSuiteNode, "time"), + getAttributeStringValue(testSuiteNode, "timestamp") + )); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static String getAttributeStringValue(Node node, String attribute) { + return node.getAttributes().getNamedItem(attribute).getTextContent(); + } + + private static int getAttributeIntValue(Node node, String attribute) { + return Integer.parseInt(getAttributeStringValue(node, attribute)); + } + + /** + * A TestResult is a model for the data generated after running tests in gradle. + */ + static class TestResult { + private final String testSuiteName; + private final String hostName; + private final int testsCount; + private final int skippedCount; + private final int errorCount; + private final int failureCount; + private final String time; + private final String timestamp; + + public TestResult(String testSuiteName, String hostName, int testsCount, int skippedCount, int errorCount, int failureCount, String time, String timestamp) { + this.testSuiteName = testSuiteName; + this.hostName = hostName; + this.testsCount = testsCount; + this.skippedCount = skippedCount; + this.errorCount = errorCount; + this.failureCount = failureCount; + this.time = time; + this.timestamp = timestamp; + } + + @Override + public String toString() { + return "Results{" + + "testSuiteName='" + testSuiteName + '\'' + + ", hostName='" + hostName + '\'' + + ", testsCount=" + testsCount + + ", skippedCount=" + skippedCount + + ", errorCount=" + errorCount + + ", failureCount=" + failureCount + + ", time='" + time + '\'' + + ", timestamp='" + timestamp + '\'' + + '}'; + } + } +} diff --git a/src/test/java/SlackIntegrationTests.java b/src/test/java/SlackIntegrationTests.java new file mode 100644 index 00000000..bb9f0af2 --- /dev/null +++ b/src/test/java/SlackIntegrationTests.java @@ -0,0 +1,11 @@ +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class SlackIntegrationTests { + + @Test + void canSendHello() { + boolean success = SlackIntegration.sendHello(); + assertTrue(success); + } +} diff --git a/src/test/java/TestResultsParserTests.java b/src/test/java/TestResultsParserTests.java new file mode 100644 index 00000000..6541f3f6 --- /dev/null +++ b/src/test/java/TestResultsParserTests.java @@ -0,0 +1,33 @@ +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.FileWriter; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TestResultsParserTests { + @Test + void getSomeResult() { + createDummyTestResults(); + List testResults = TestResultsParser.getResults(); + assertNotNull(testResults); + assertTrue(testResults.size() > 0); + } + + private void createDummyTestResults() { + String dummyXML = "\n" + + "\n" + + "\n"; + try { + File dummyFile = new File(TestResultsParser.getResultsFolder() + "/TEST-dummy.xml"); + dummyFile.createNewFile(); + FileWriter writer = new FileWriter(dummyFile); + writer.write(dummyXML); + writer.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } +}