diff --git a/.gitignore b/.gitignore index 8de3985..bf650a4 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ target results.json !/bin/ + +.idea diff --git a/Dockerfile b/Dockerfile index 967557d..33d0ae4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # === Build builder image === -FROM gradle:7.3-jdk17 AS build +FROM gradle:8.2-jdk17 AS build WORKDIR /home/builder @@ -13,26 +13,13 @@ RUN gradle -i clean build RUN gradle -i shadowJar \ && cp build/libs/autotest-runner.jar . -FROM maven:3.8-eclipse-temurin-17 AS cache - -# Ensure exercise dependencies are downloaded -WORKDIR /opt/exercise -COPY exercise . -RUN mvn test dependency:go-offline -DexcludeReactor=false - # === Build runtime image === -FROM maven:3.8.6-eclipse-temurin-17-focal +FROM eclipse-temurin:17-focal WORKDIR /opt/test-runner # Copy binary and launcher script COPY bin/ ./bin/ COPY --from=build /home/builder/autotest-runner.jar ./ -# Copy cached dependencies -COPY --from=cache /root/.m2 /root/.m2 - -# Copy Maven pom.xml -COPY --from=cache /opt/exercise/pom.xml /root/pom.xml - ENTRYPOINT ["sh", "/opt/test-runner/bin/run.sh"] diff --git a/README.md b/README.md index f6ba7ac..240e242 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,12 @@ To run the tests to verify the behavior of the test runner, do the following: 1. Open a terminal in the project's root 2. Run `./bin/run-tests.sh` -These are [golden tests][golden] that compare the `results.json` generated by running the current state of the code against the "known good" `tests//results.json`. All files created during the test run itself are discarded. +These are [golden tests][golden] that compare the `results.json` generated by running the current state of the code +against the "known good" `tests//expected_results.json`. +All files created during the test run itself are discarded. -When you've made modifications to the code that will result in a new "golden" state, you'll need to generate and commit a new `tests//results.json` file. +When you've made modifications to the code that will result in a new "golden" state, +you'll need to generate and commit a new `tests//expected_results.json` file. ## Run the tests using Docker @@ -44,9 +47,12 @@ To run the tests to verify the behavior of the test runner using the Docker imag 1. Open a terminal in the project's root 2. Run `./bin/run-tests-in-docker.sh` -These are [golden tests][golden] that compare the `results.json` generated by running the current state of the code against the "known good" `tests//results.json`. All files created during the test run itself are discarded. +These are [golden tests][golden] that compare the `results.json` generated by running the current state of the code +against the "known good" `tests//expected_results.json`. +All files created during the test run itself are discarded. -When you've made modifications to the code that will result in a new "golden" state, you'll need to generate and commit a new `tests//results.json` file. +When you've made modifications to the code that will result in a new "golden" state, +you'll need to generate and commit a new `tests//expected_results.json` file. [test-runners]: https://github.com/exercism/docs/tree/main/building/tooling/test-runners [golden]: https://ro-che.info/articles/2017-12-04-golden-tests diff --git a/bin/run.sh b/bin/run.sh index 28f49eb..db88720 100755 --- a/bin/run.sh +++ b/bin/run.sh @@ -36,9 +36,6 @@ mkdir -p $tmp_folder cd $tmp_folder cp -R $input_folder/* . -find . -mindepth 1 -type f | grep 'Test.java' | xargs -I file sed -i "s/@Ignore(.*)//g;s/@Ignore//g;" file +find . -mindepth 1 -type f | grep 'Test.java' | xargs -I file sed -i "s/@Ignore(.*)//g;s/@Ignore//g;s/@Disabled(.*)//g;s/@Disabled//g;" file -cp /root/pom.xml . - -java -jar /opt/test-runner/autotest-runner.jar $problem_slug -mv results.json $output_folder +java -jar /opt/test-runner/autotest-runner.jar $problem_slug . $output_folder diff --git a/exercise/build.gradle b/exercise/build.gradle deleted file mode 100644 index 8bd005d..0000000 --- a/exercise/build.gradle +++ /dev/null @@ -1,24 +0,0 @@ -apply plugin: "java" -apply plugin: "eclipse" -apply plugin: "idea" - -// set default encoding to UTF-8 -compileJava.options.encoding = "UTF-8" -compileTestJava.options.encoding = "UTF-8" - -repositories { - mavenCentral() -} - -dependencies { - testImplementation "junit:junit:4.13" - testImplementation "org.assertj:assertj-core:3.15.0" -} - -test { - testLogging { - exceptionFormat = 'full' - showStandardStreams = true - events = ["passed", "failed", "skipped"] - } -} diff --git a/exercise/pom.xml b/exercise/pom.xml deleted file mode 100644 index ccd8901..0000000 --- a/exercise/pom.xml +++ /dev/null @@ -1,102 +0,0 @@ - - - - 4.0.0 - - com.exercism - exercise - 1.0-SNAPSHOT - - - UTF-8 - 17 - 17 - - - - - junit - junit - 4.13 - test - - - org.assertj - assertj-core - 3.15.0 - test - - - org.json - json - 20190722 - - - io.reactivex.rxjava2 - rxjava - 2.2.12 - - - - - - - - - maven-clean-plugin - 3.1.0 - - - - maven-resources-plugin - 3.0.2 - - - maven-compiler-plugin - 3.8.0 - - 17 - 17 - - - - maven-surefire-plugin - 2.22.1 - - - maven-jar-plugin - 3.0.2 - - - maven-install-plugin - 2.5.2 - - - maven-deploy-plugin - 2.8.2 - - - - maven-site-plugin - 3.7.1 - - - maven-project-info-reports-plugin - 3.0.0 - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - org.codehaus.mojo - exec-maven-plugin - 3.0.0 - - - - diff --git a/exercise/src/main/java/Anagram.java b/exercise/src/main/java/Anagram.java deleted file mode 100644 index f0a9718..0000000 --- a/exercise/src/main/java/Anagram.java +++ /dev/null @@ -1,43 +0,0 @@ -import java.util.*; -import java.util.stream.Collectors; - -class Anagram { - - private final AnagramSubject anagramSubject; - - Anagram(String word) { - anagramSubject = new AnagramSubject(word); - } - - List match(List candidates) { - return candidates.stream() - .filter(anagramSubject::anagramOf) - .collect(Collectors.toList()); - } - - static final class AnagramSubject { - - private final String word; - private final char[] fingerprint; - - AnagramSubject(String other) { - this.word = other; - this.fingerprint = canonicalize(other); - } - - boolean anagramOf(String other) { - return !duplicate(other) && Arrays.equals(fingerprint, canonicalize(other)); - } - - private boolean duplicate(String other) { - return word.equalsIgnoreCase(other); - } - - private char[] canonicalize(String other) { - char[] chars = other.toLowerCase().toCharArray(); - Arrays.sort(chars); - return chars; - } - } -} - diff --git a/exercise/src/test/java/AnagramTest.java b/exercise/src/test/java/AnagramTest.java deleted file mode 100644 index 8d47c45..0000000 --- a/exercise/src/test/java/AnagramTest.java +++ /dev/null @@ -1,152 +0,0 @@ -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.Ignore; -import org.junit.Test; - -import java.util.Arrays; -import java.util.Collections; - -public class AnagramTest { - - @Test - public void testNoMatches() { - Anagram detector = new Anagram("diaper"); - - assertThat( - detector.match( - Arrays.asList("hello", "world", "zombies", "pants"))) - .isEmpty(); - } - - @Ignore("Remove to run test") - @Test - public void testDetectMultipleAnagrams() { - Anagram detector = new Anagram("master"); - - assertThat(detector.match(Arrays.asList("stream", "pigeon", "maters"))) - .containsExactlyInAnyOrder("maters", "stream"); - } - - @Ignore("Remove to run test") - @Test - public void testEliminateAnagramSubsets() { - Anagram detector = new Anagram("good"); - - assertThat(detector.match(Arrays.asList("dog", "goody"))).isEmpty(); - } - - @Ignore("Remove to run test") - @Test - public void testDetectLongerAnagram() { - Anagram detector = new Anagram("listen"); - - assertThat( - detector.match( - Arrays.asList("enlists", "google", "inlets", "banana"))) - .containsExactlyInAnyOrder("inlets"); - } - - @Ignore("Remove to run test") - @Test - public void testDetectMultipleAnagramsForLongerWord() { - Anagram detector = new Anagram("allergy"); - assertThat( - detector.match( - Arrays.asList( - "gallery", - "ballerina", - "regally", - "clergy", - "largely", - "leading"))) - .containsExactlyInAnyOrder("gallery", "regally", "largely"); - } - - @Ignore("Remove to run test") - @Test - public void testDetectsMultipleAnagramsWithDifferentCase() { - Anagram detector = new Anagram("nose"); - - assertThat(detector.match(Arrays.asList("Eons", "ONES"))) - .containsExactlyInAnyOrder("Eons", "ONES"); - } - - @Ignore("Remove to run test") - @Test - public void testEliminateAnagramsWithSameChecksum() { - Anagram detector = new Anagram("mass"); - - assertThat(detector.match(Collections.singletonList("last"))) - .isEmpty(); - } - - @Ignore("Remove to run test") - @Test - public void testCaseInsensitiveWhenBothAnagramAndSubjectStartWithUpperCaseLetter() { - Anagram detector = new Anagram("Orchestra"); - - assertThat( - detector.match( - Arrays.asList("cashregister", "Carthorse", "radishes"))) - .containsExactlyInAnyOrder("Carthorse"); - } - - @Ignore("Remove to run test") - @Test - public void testCaseInsensitiveWhenSubjectStartsWithUpperCaseLetter() { - Anagram detector = new Anagram("Orchestra"); - - assertThat( - detector.match( - Arrays.asList("cashregister", "carthorse", "radishes"))) - .containsExactlyInAnyOrder("carthorse"); - } - - @Ignore("Remove to run test") - @Test - public void testCaseInsensitiveWhenAnagramStartsWithUpperCaseLetter() { - Anagram detector = new Anagram("orchestra"); - - assertThat( - detector.match( - Arrays.asList("cashregister", "Carthorse", "radishes"))) - .containsExactlyInAnyOrder("Carthorse"); - } - - @Ignore("Remove to run test") - @Test - public void testIdenticalWordRepeatedIsNotAnagram() { - Anagram detector = new Anagram("go"); - - assertThat(detector.match(Collections.singletonList("go Go GO"))) - .isEmpty(); - } - - @Ignore("Remove to run test") - @Test - public void testAnagramMustUseAllLettersExactlyOnce() { - Anagram detector = new Anagram("tapper"); - - assertThat(detector.match(Collections.singletonList("patter"))) - .isEmpty(); - } - - @Ignore("Remove to run test") - @Test - public void testWordsAreNotAnagramsOfThemselvesCaseInsensitive() { - Anagram detector = new Anagram("BANANA"); - - assertThat(detector.match(Arrays.asList("BANANA", "Banana", "banana"))) - .isEmpty(); - } - - @Ignore("Remove to run test") - @Test - public void testWordsOtherThanThemselvesCanBeAnagrams() { - Anagram detector = new Anagram("LISTEN"); - - assertThat(detector.match(Arrays.asList("Listen", "Silent", "LISTEN"))) - .containsExactlyInAnyOrder("Silent"); - } - -} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..033e24c 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 0000000..62f495d --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +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 new file mode 100755 index 0000000..fcb6fca --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# 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. +# 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 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 +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 + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +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 ;; #( + 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 + 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 + 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" && ! "$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 + +# 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" || "$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 + 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 + # 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 +fi + + +# 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 new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@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=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@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% equ 0 goto execute + +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 execute + +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 + +: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 %* + +:end +@rem End local scope for the variables with windows NT shell +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! +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 + +:omega diff --git a/lib/build.gradle b/lib/build.gradle index 89a63da..e0038cd 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -1,29 +1,44 @@ plugins { - id 'com.github.johnrengelman.shadow' version '6.1.0' - id 'java' - id 'application' - id 'eclipse' - id 'idea' + id 'com.github.johnrengelman.shadow' version '8.1.1' + id 'java' + id 'application' } -mainClassName = 'com.exercism.runner.TestRunner' +group = "org.exercism" +version = "1.0-SNAPSHOT" +mainClassName = 'com.exercism.TestRunner' repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation 'com.google.auto.value:auto-value-annotations:1.8.2' - annotationProcessor 'com.google.auto.value:auto-value:1.8.2' - implementation 'com.google.guava:guava:30.1.1-jre' - implementation 'com.fasterxml.jackson.core:jackson-core:2.12.5' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.5' - implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.12.5' - implementation 'com.github.javaparser:javaparser-core:3.23.0' - implementation 'junit:junit:4.13' - implementation 'org.assertj:assertj-core:3.15.0' + implementation 'com.google.auto.value:auto-value-annotations:1.8.2' + annotationProcessor 'com.google.auto.value:auto-value:1.8.2' + implementation 'com.google.guava:guava:30.1.1-jre' + implementation 'com.fasterxml.jackson.core:jackson-core:2.12.5' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.7.1' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.12.5' + implementation 'com.github.javaparser:javaparser-core:3.23.0' + + implementation 'junit:junit:4.13.1' + implementation 'org.assertj:assertj-core:3.15.0' + implementation platform('org.junit:junit-bom:5.10.0') + implementation 'org.junit.jupiter:junit-jupiter' + implementation 'org.junit.platform:junit-platform-launcher' + runtimeOnly 'org.junit.vintage:junit-vintage-engine' + runtimeOnly 'org.junit.jupiter:junit-jupiter-engine' + + // Add exercise dependencies to runtime classpath + runtimeOnly 'org.json:json:20190722' // used in practice:rest-api + runtimeOnly 'io.reactivex.rxjava2:rxjava:2.2.12' // used in practice:hangman +} + +shadowJar { + mergeServiceFiles() + archiveFileName.set("autotest-runner.jar") } -tasks.named("shadowJar") { - archiveFileName.set("autotest-runner.jar") +artifacts { + archives shadowJar } diff --git a/lib/src/main/java/com/exercism/TestDetails.java b/lib/src/main/java/com/exercism/TestDetails.java new file mode 100644 index 0000000..e190f37 --- /dev/null +++ b/lib/src/main/java/com/exercism/TestDetails.java @@ -0,0 +1,4 @@ +package com.exercism; + +public record TestDetails(TestSource source, TestMetadata metadata, TestResult result, String output) { +} diff --git a/lib/src/main/java/com/exercism/TestMetadata.java b/lib/src/main/java/com/exercism/TestMetadata.java new file mode 100644 index 0000000..c6405e0 --- /dev/null +++ b/lib/src/main/java/com/exercism/TestMetadata.java @@ -0,0 +1,6 @@ +package com.exercism; + +import java.util.Optional; + +public record TestMetadata(String name, Optional taskId) { +} diff --git a/lib/src/main/java/com/exercism/TestResult.java b/lib/src/main/java/com/exercism/TestResult.java new file mode 100644 index 0000000..639a6c7 --- /dev/null +++ b/lib/src/main/java/com/exercism/TestResult.java @@ -0,0 +1,6 @@ +package com.exercism; + +import java.util.Optional; + +public record TestResult(TestStatus status, Optional failure) { +} diff --git a/lib/src/main/java/com/exercism/TestRunner.java b/lib/src/main/java/com/exercism/TestRunner.java new file mode 100644 index 0000000..06c144c --- /dev/null +++ b/lib/src/main/java/com/exercism/TestRunner.java @@ -0,0 +1,83 @@ +package com.exercism; + +import com.exercism.compiler.ExerciseCompilationException; +import com.exercism.compiler.ExerciseCompiler; +import com.exercism.junit.JUnitTestParser; +import com.exercism.junit.JUnitTestRunner; +import com.exercism.report.Report; +import com.exercism.report.ReportGenerator; +import com.exercism.report.ReportWriter; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collection; +import java.util.List; +import java.util.stream.Stream; + +public final class TestRunner { + + private final JUnitTestParser testParser; + private final JUnitTestRunner testRunner; + private final ReportWriter reportWriter; + private final String slug; + private final String inputDirectory; + + public TestRunner(String slug, String inputDirectory, String outputDirectory) { + this.testParser = new JUnitTestParser(); + this.reportWriter = new ReportWriter(Paths.get(outputDirectory)); + this.testRunner = new JUnitTestRunner(); + this.slug = slug; + this.inputDirectory = inputDirectory; + } + + public static void main(String[] args) throws IOException { + if (args.length < 3) { + throw new IllegalArgumentException("Not enough arguments, need "); + } + new TestRunner(args[0], args[1], args[2]).run(); + } + + private void run() throws IOException { + var sourceFiles = resolveSourceFiles(); + var testFiles = resolveTestFiles(); + + for (File testFile : testFiles) { + testParser.parse(testFile); + } + + var filesToCompile = Stream.concat(sourceFiles.stream(), testFiles.stream()).toList(); + + Report report; + try (var compiler = new ExerciseCompiler(slug)) { + compiler.compile(filesToCompile); + testRunner.test(compiler.getClassLoader(), compiler.getClasspathRoots()); + report = ReportGenerator.generate(testRunner.getTestDetails(), testParser.buildTestCodeMap()); + } catch (ExerciseCompilationException e) { + report = Report.builder() + .setStatus("error") + .setMessage(e.getMessage()) + .build(); + } + + reportWriter.report(report); + } + + private Collection resolveSourceFiles() throws IOException { + var sourcePath = Paths.get(inputDirectory, "src", "main", "java"); + return resolveJavaFiles(sourcePath); + } + + private Collection resolveTestFiles() throws IOException { + var testPath = Paths.get(inputDirectory, "src", "test", "java"); + return resolveJavaFiles(testPath); + } + + private static List resolveJavaFiles(Path path) throws IOException { + try (var files = Files.find(path, 10, (file, attrs) -> attrs.isRegularFile() && file.toString().endsWith(".java"))) { + return files.map(Path::toFile).toList(); + } + } +} diff --git a/lib/src/main/java/com/exercism/TestSource.java b/lib/src/main/java/com/exercism/TestSource.java new file mode 100644 index 0000000..5a0de2f --- /dev/null +++ b/lib/src/main/java/com/exercism/TestSource.java @@ -0,0 +1,4 @@ +package com.exercism; + +public record TestSource(String packageName, String className, String methodName) { +} diff --git a/lib/src/main/java/com/exercism/TestStatus.java b/lib/src/main/java/com/exercism/TestStatus.java new file mode 100644 index 0000000..99f332f --- /dev/null +++ b/lib/src/main/java/com/exercism/TestStatus.java @@ -0,0 +1,5 @@ +package com.exercism; + +public enum TestStatus { + PASS, FAIL, ERROR +} diff --git a/lib/src/main/java/com/exercism/compiler/ExerciseCompilationException.java b/lib/src/main/java/com/exercism/compiler/ExerciseCompilationException.java new file mode 100644 index 0000000..61ac2fe --- /dev/null +++ b/lib/src/main/java/com/exercism/compiler/ExerciseCompilationException.java @@ -0,0 +1,7 @@ +package com.exercism.compiler; + +public class ExerciseCompilationException extends Exception { + public ExerciseCompilationException(String message) { + super(message); + } +} diff --git a/lib/src/main/java/com/exercism/compiler/ExerciseCompiler.java b/lib/src/main/java/com/exercism/compiler/ExerciseCompiler.java new file mode 100644 index 0000000..0e81553 --- /dev/null +++ b/lib/src/main/java/com/exercism/compiler/ExerciseCompiler.java @@ -0,0 +1,61 @@ +package com.exercism.compiler; + +import com.google.common.collect.Sets; + +import javax.tools.*; +import java.io.Closeable; +import java.io.File; +import java.io.Flushable; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +public class ExerciseCompiler implements Closeable, Flushable { + private final JavaCompiler compiler; + private final StandardJavaFileManager fileManager; + private final DiagnosticCollector diagnosticCollector; + + public ExerciseCompiler(String exerciseSlug) throws IOException { + this.compiler = ToolProvider.getSystemJavaCompiler(); + this.diagnosticCollector = new DiagnosticCollector<>(); + this.fileManager = compiler.getStandardFileManager(this.diagnosticCollector, null, null); + + var outputDir = Files.createTempDirectory(String.format("java-test-runner-%s-", exerciseSlug)); + this.fileManager.setLocation(StandardLocation.CLASS_OUTPUT, List.of(outputDir.toFile())); + } + + public void compile(final Collection filesToCompile) throws ExerciseCompilationException { + var sources = fileManager.getJavaFileObjectsFromFiles(filesToCompile); + var task = compiler.getTask(null, fileManager, diagnosticCollector, null, null, sources); + if (task.call()) { + return; + } + + var messageBuilder = new StringBuilder(); + for (var diagnostics : diagnosticCollector.getDiagnostics()) { + messageBuilder.append(diagnostics.toString()); + } + throw new ExerciseCompilationException(messageBuilder.toString()); + } + + public ClassLoader getClassLoader() { + return this.fileManager.getClassLoader(StandardLocation.CLASS_OUTPUT); + } + + public Set getClasspathRoots() { + return Sets.newHashSet(this.fileManager.getLocationAsPaths(StandardLocation.CLASS_OUTPUT)); + } + + @Override + public void close() throws IOException { + this.fileManager.close(); + } + + @Override + public void flush() throws IOException { + this.fileManager.flush(); + } +} diff --git a/lib/src/main/java/com/exercism/junit/JUnitTestParser.java b/lib/src/main/java/com/exercism/junit/JUnitTestParser.java index b5605c3..300838a 100644 --- a/lib/src/main/java/com/exercism/junit/JUnitTestParser.java +++ b/lib/src/main/java/com/exercism/junit/JUnitTestParser.java @@ -1,66 +1,53 @@ package com.exercism.junit; -import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; -import com.github.javaparser.ast.body.MethodDeclaration; +import com.exercism.TestSource; +import com.github.javaparser.JavaParser; import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.PackageDeclaration; -import com.github.javaparser.JavaParser; -import com.github.javaparser.ParseProblemException; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.MethodDeclaration; import com.google.common.collect.ImmutableMap; -import com.google.common.io.Files; +import org.junit.Test; import java.io.File; -import java.io.FileNotFoundException; import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Paths; -import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import org.junit.Test; public final class JUnitTestParser { - private final ImmutableMap.Builder testCodeByTestName = - ImmutableMap.builder(); - - public JUnitTestParser parse(File file) { + private final ImmutableMap.Builder testCodeByTestName = ImmutableMap.builder(); + + public void parse(File file) { try { - new JavaParser().parse(file) - .ifSuccessful(compilationUnit -> parse(file, compilationUnit)); + new JavaParser().parse(file).ifSuccessful(this::parse); } catch (IOException e) { throw new IllegalStateException("Could not read test file: " + file.getAbsolutePath(), e); } - return this; } - private void parse(File file, CompilationUnit compilationUnit) { - String methodPrefix = - compilationUnit.getPackageDeclaration() - .map(PackageDeclaration::getNameAsString) - .orElse(""); + private void parse(CompilationUnit compilationUnit) { + var packageName = + compilationUnit.getPackageDeclaration() + .map(PackageDeclaration::getNameAsString) + .orElse(""); - String className = ""; + var className = ""; for (ClassOrInterfaceDeclaration classDeclaration - : compilationUnit.findAll(ClassOrInterfaceDeclaration.class)) { + : compilationUnit.findAll(ClassOrInterfaceDeclaration.class)) { className = classDeclaration.getNameAsString(); break; } - if (methodPrefix.isEmpty()) { - methodPrefix = className; - } else { - methodPrefix = methodPrefix + "." + className; - } for (MethodDeclaration methodDeclaration : compilationUnit.findAll(MethodDeclaration.class)) { - if (!methodDeclaration.isAnnotationPresent​(Test.class)) { + if (!methodDeclaration.isAnnotationPresent(Test.class) && + !methodDeclaration.isAnnotationPresent(org.junit.jupiter.api.Test.class)) { continue; } - String fullMethodName = methodPrefix + "." + methodDeclaration.getNameAsString(); - testCodeByTestName.put(fullMethodName, methodDeclaration.toString()); + var methodName = methodDeclaration.getNameAsString(); + var testSource = new TestSource(packageName, className, methodName); + testCodeByTestName.put(testSource, methodDeclaration.toString()); } } - public ImmutableMap buildTestCodeMap() { + public ImmutableMap buildTestCodeMap() { return testCodeByTestName.build(); } } diff --git a/lib/src/main/java/com/exercism/junit/JUnitTestRunner.java b/lib/src/main/java/com/exercism/junit/JUnitTestRunner.java new file mode 100644 index 0000000..1a3ed78 --- /dev/null +++ b/lib/src/main/java/com/exercism/junit/JUnitTestRunner.java @@ -0,0 +1,116 @@ +package com.exercism.junit; + +import com.exercism.*; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.TestTag; +import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.engine.reporting.ReportEntry; +import org.junit.platform.engine.support.descriptor.MethodSource; +import org.junit.platform.launcher.TestExecutionListener; +import org.junit.platform.launcher.TestIdentifier; +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; +import org.junit.platform.launcher.core.LauncherFactory; + +import java.nio.file.Path; +import java.util.*; + +public class JUnitTestRunner implements TestExecutionListener { + + public static final String TASK_ID_TAG_PREFIX = "task:"; + private final Map outputPerTest; + private final List testDetails; + + public JUnitTestRunner() { + this.outputPerTest = new HashMap<>(); + this.testDetails = new ArrayList<>(); + } + + public void test(ClassLoader classLoader, Set classpathRoots) { + var originalClassLoader = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(classLoader); + var request = LauncherDiscoveryRequestBuilder.request() + .selectors(DiscoverySelectors.selectClasspathRoots(classpathRoots)) + .configurationParameter("junit.platform.output.capture.stdout", "true") + .build(); + var launcher = LauncherFactory.create(); + var testPlan = launcher.discover(request); + launcher.execute(testPlan, this); + Thread.currentThread().setContextClassLoader(originalClassLoader); + } + + @Override + public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) { + if (!testIdentifier.isTest()) { + return; + } + + var id = testIdentifier.getUniqueId(); + String output = null; + if (outputPerTest.containsKey(id)) { + output = outputPerTest.get(id); + } + + var result = new TestResult(mapStatus(testExecutionResult.getStatus()), testExecutionResult.getThrowable()); + var metadata = new TestMetadata(testIdentifier.getDisplayName(), getTaskId(testIdentifier)); + var details = new TestDetails(getTestSource(testIdentifier), metadata, result, output); + testDetails.add(details); + } + + @Override + public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry entry) { + for (Map.Entry pair : entry.getKeyValuePairs().entrySet()) { + if (pair.getKey().equals("stdout")) { + var output = pair.getValue(); + outputPerTest.merge(testIdentifier.getUniqueId(), output, (s1, s2) -> s1 + s2); + } + } + } + + public List getTestDetails() { + return testDetails; + } + + private static TestStatus mapStatus(TestExecutionResult.Status status) { + return switch (status) { + case SUCCESSFUL -> TestStatus.PASS; + case FAILED -> TestStatus.FAIL; + case ABORTED -> TestStatus.ERROR; + }; + } + + private static TestSource getTestSource(TestIdentifier identifier) { + return identifier.getSource() + .flatMap(ts -> ts instanceof MethodSource ms ? Optional.of(ms) : Optional.empty()) + .map(JUnitTestRunner::mapTestSource) + .orElseGet(() -> + new TestSource( + null, + identifier.getUniqueIdObject().removeLastSegment().getLastSegment().getValue(), + identifier.getUniqueIdObject().getLastSegment().getValue() + ) + ); + } + + private static TestSource mapTestSource(MethodSource methodSource) { + return new TestSource( + methodSource.getJavaClass().getPackageName(), + methodSource.getClassName(), + methodSource.getMethodName() + ); + } + + private static Optional getTaskId(TestIdentifier identifier) { + for (TestTag tag : identifier.getTags()) { + if (!tag.getName().startsWith(TASK_ID_TAG_PREFIX)) { + continue; + } + + var taskId = tag.getName().replaceFirst(TASK_ID_TAG_PREFIX, ""); + try { + return Optional.of(Integer.parseInt(taskId)); + } catch (NumberFormatException ignored) { + } + } + return Optional.empty(); + } +} diff --git a/lib/src/main/java/com/exercism/data/Report.java b/lib/src/main/java/com/exercism/report/Report.java similarity index 97% rename from lib/src/main/java/com/exercism/data/Report.java rename to lib/src/main/java/com/exercism/report/Report.java index 201758c..405abf8 100644 --- a/lib/src/main/java/com/exercism/data/Report.java +++ b/lib/src/main/java/com/exercism/report/Report.java @@ -1,4 +1,4 @@ -package com.exercism.data; +package com.exercism.report; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; diff --git a/lib/src/main/java/com/exercism/report/ReportGenerator.java b/lib/src/main/java/com/exercism/report/ReportGenerator.java index 33f6d86..2ced013 100644 --- a/lib/src/main/java/com/exercism/report/ReportGenerator.java +++ b/lib/src/main/java/com/exercism/report/ReportGenerator.java @@ -1,19 +1,62 @@ package com.exercism.report; -import java.io.File; +import com.exercism.TestDetails; +import com.exercism.TestSource; +import com.exercism.TestStatus; +import com.google.common.base.Throwables; -import com.exercism.data.Report; -import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Collection; +import java.util.Map; public class ReportGenerator { - - public static void report(Report report) { - ObjectMapper mapper = new ObjectMapper(); - - try { - mapper.writerWithDefaultPrettyPrinter().writeValue(new File("results.json"), report); - } catch (Exception e) { - e.printStackTrace(); - } - } + public static Report generate(Collection testDetails, Map testCodeMap) { + var reportDetails = testDetails.stream().map(item -> buildTestDetails(item, testCodeMap)).toList(); + var reportStatus = collapseStatuses(testDetails.stream().map(details -> details.result().status()).toList()); + var reportVersion = testDetails.stream().anyMatch(details -> details.metadata().taskId().isPresent()) ? 3 : 2; + + return Report.builder() + .setTests(reportDetails) + .setStatus(mapStatus(reportStatus)) + .setVersion(reportVersion) + .build(); + } + + private static com.exercism.report.TestDetails buildTestDetails(TestDetails testDetails, Map testCodeMap) { + var detailBuilder = com.exercism.report.TestDetails.builder() + .setStatus(mapStatus(testDetails.result().status())) + .setTestCode(testCodeMap.get(testDetails.source())) + .setName(testDetails.metadata().name()) + .setOutput(testDetails.output()); + + testDetails.result().failure().ifPresent(t -> { + var message = String.format("Message: %s%nException: %s", t.getMessage(), Throwables.getStackTraceAsString(t)); + detailBuilder.setMessage(message); + }); + + testDetails.metadata().taskId().ifPresent(detailBuilder::setTaskId); + + return detailBuilder.build(); + } + + private static String mapStatus(TestStatus status) { + return switch (status) { + case PASS -> "pass"; + case FAIL -> "fail"; + case ERROR -> "error"; + }; + } + + private static TestStatus collapseStatuses(Collection statuses) { + for (TestStatus status : statuses) { + if (status == TestStatus.ERROR) { + return TestStatus.ERROR; + } + + if (status == TestStatus.FAIL) { + return TestStatus.FAIL; + } + } + + return TestStatus.PASS; + } } diff --git a/lib/src/main/java/com/exercism/report/ReportWriter.java b/lib/src/main/java/com/exercism/report/ReportWriter.java new file mode 100644 index 0000000..9aea856 --- /dev/null +++ b/lib/src/main/java/com/exercism/report/ReportWriter.java @@ -0,0 +1,29 @@ +package com.exercism.report; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; + +import java.nio.file.Path; + +public class ReportWriter { + + private final Path outputDirectory; + + public ReportWriter(Path outputDirectory) { + this.outputDirectory = outputDirectory; + } + + public void report(Report report) { + var mapper = new ObjectMapper(); + mapper.registerModule(new Jdk8Module()); + mapper.setSerializationInclusion(JsonInclude.Include.NON_ABSENT); + var filePath = this.outputDirectory.resolve("results.json"); + + try { + mapper.writerWithDefaultPrettyPrinter().writeValue(filePath.toFile(), report); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/lib/src/main/java/com/exercism/data/TestDetails.java b/lib/src/main/java/com/exercism/report/TestDetails.java similarity index 85% rename from lib/src/main/java/com/exercism/data/TestDetails.java rename to lib/src/main/java/com/exercism/report/TestDetails.java index d284ce5..5b165a9 100644 --- a/lib/src/main/java/com/exercism/data/TestDetails.java +++ b/lib/src/main/java/com/exercism/report/TestDetails.java @@ -1,4 +1,4 @@ -package com.exercism.data; +package com.exercism.report; import javax.annotation.Nullable; @@ -6,6 +6,8 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.google.auto.value.AutoValue; +import java.util.Optional; + @AutoValue @JsonDeserialize(builder = AutoValue_Report.Builder.class) public abstract class TestDetails { @@ -26,6 +28,9 @@ public abstract class TestDetails { @Nullable public abstract String output(); + @JsonProperty("task_id") + public abstract Optional taskId(); + public static Builder builder() { return new AutoValue_TestDetails.Builder(); } @@ -47,6 +52,9 @@ public abstract static class Builder { @JsonProperty("output") public abstract Builder setOutput(String output); + @JsonProperty("task_id") + public abstract Builder setTaskId(int taskId); + public abstract TestDetails build(); } } diff --git a/lib/src/main/java/com/exercism/runner/TestRunner.java b/lib/src/main/java/com/exercism/runner/TestRunner.java deleted file mode 100644 index 082b5b4..0000000 --- a/lib/src/main/java/com/exercism/runner/TestRunner.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.exercism.runner; - -import static java.util.concurrent.TimeUnit.SECONDS; - -import com.exercism.data.Report; -import com.exercism.junit.JUnitTestParser; -import com.exercism.report.ReportGenerator; -import com.exercism.xml.JUnitXmlParser; -import com.google.common.collect.ImmutableMap; -import com.google.common.io.Files; -import com.google.common.io.MoreFiles; -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.nio.file.Paths; - -public final class TestRunner { - private static final String MAVEN_TEST_OUTPUT = "maven-test.out"; - - public static void main(String[] args) throws InterruptedException, IOException { - run(args[0]); - } - - private static void run(String slug) throws InterruptedException, IOException { - Process mavenTest = new ProcessBuilder( - "mvn", - "test", - "--offline", - "--legacy-local-repository", - "--batch-mode", - "--non-recursive", - "--quiet") - .redirectOutput(new File(MAVEN_TEST_OUTPUT)) - .start(); - if (!mavenTest.waitFor(20, SECONDS)) { - throw new IllegalStateException("test did not complete within 20 seconds"); - } - - if (mavenTest.exitValue() != 0) { - String mavenOutput = Files.asCharSource( - Paths.get(MAVEN_TEST_OUTPUT).toFile(), StandardCharsets.UTF_8) - .read(); - if (mavenOutput.contains("COMPILATION ERROR")) { - ReportGenerator.report( - Report.builder() - .setStatus("error") - .setMessage(mavenOutput) - .build()); - return; - } - } - - JUnitTestParser testParser = new JUnitTestParser(); - for (Path filePath : MoreFiles.listFiles(Paths.get("src", "test", "java"))) { - if (MoreFiles.getFileExtension(filePath).equals("java")) { - testParser.parse(filePath.toFile()); - } - } - ImmutableMap testCodeByTestName = testParser.buildTestCodeMap(); - JUnitXmlParser xmlParser = new JUnitXmlParser(mavenTest.exitValue(), testCodeByTestName); - for (Path filePath : MoreFiles.listFiles(Paths.get("target", "surefire-reports"))) { - if (MoreFiles.getFileExtension(filePath).equals("xml")) { - xmlParser.parse(filePath.toFile()); - } - } - Report report = xmlParser.buildReport(); - ReportGenerator.report(report); - } -} diff --git a/lib/src/main/java/com/exercism/xml/JUnitXmlParser.java b/lib/src/main/java/com/exercism/xml/JUnitXmlParser.java deleted file mode 100644 index 590cd10..0000000 --- a/lib/src/main/java/com/exercism/xml/JUnitXmlParser.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.exercism.xml; - -import com.exercism.data.Report; -import com.exercism.data.TestDetails; -import com.exercism.xml.data.TestCase; -import com.exercism.xml.data.TestSuite; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.dataformat.xml.XmlMapper; -import com.google.common.collect.ImmutableMap; -import com.google.common.io.Files; -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; - -public final class JUnitXmlParser { - private int exitCode = 1; - private final ImmutableMap testCodeByTestName; - private final Report.Builder report = Report.builder(); - - public JUnitXmlParser(int exitCode, ImmutableMap testCodeByTestName) { - this.exitCode = exitCode; - this.testCodeByTestName = testCodeByTestName; - } - - public JUnitXmlParser parse(File file) { - String xml; - try { - xml = Files.asCharSource(file, StandardCharsets.UTF_8).read(); - } catch (IOException e) { - throw new IllegalStateException("Count not read file " + file.getAbsolutePath()); - } - TestSuite testSuite; - try { - testSuite = XmlMapper.builder() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .build() - .readValue(xml, TestSuite.class); - } catch (JsonProcessingException e) { - throw new IllegalStateException( - "Could not process XML for path " + file.getAbsolutePath(), e); - } - - if (this.exitCode == 0 && testSuite.failures == 0 && testSuite.errors == 0) { - report.setStatus("pass"); - } else { - report.setStatus("fail"); - } - for (TestCase tc : testSuite.testcase) { - String fullMethodName = tc.classname + "." + tc.name; - TestDetails.Builder testDetails = TestDetails.builder() - .setName(fullMethodName) - .setTestCode( - testCodeByTestName.getOrDefault( - fullMethodName, "Could not determine test code for " + fullMethodName)) - .setStatus("pass"); - if (tc.failure != null) { - testDetails - .setStatus("fail") - .setMessage( - "Message: " + tc.failure.message + "\n" - + "Exception: " + tc.failure.value); - } - if (tc.error != null) { - testDetails - .setStatus("fail") - .setMessage( - "Message: " + tc.error.message + "\n" - + "Exception: " + tc.error.value); - } - report.addTest(testDetails.build()); - } - return this; - } - - public Report buildReport() { - return report.build(); - } -} diff --git a/lib/src/main/java/com/exercism/xml/data/Error.java b/lib/src/main/java/com/exercism/xml/data/Error.java deleted file mode 100644 index a9ec277..0000000 --- a/lib/src/main/java/com/exercism/xml/data/Error.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.exercism.xml.data; - -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText; -import com.google.common.base.MoreObjects; - -public class Error { - @JacksonXmlProperty(isAttribute = true) - public String message; - @JacksonXmlProperty(isAttribute = true) - public String type; - @JacksonXmlText - public String value; - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("message", message) - .add("type", type) - .add("value", value) - .toString(); - } -} diff --git a/lib/src/main/java/com/exercism/xml/data/Failure.java b/lib/src/main/java/com/exercism/xml/data/Failure.java deleted file mode 100644 index 8a99b59..0000000 --- a/lib/src/main/java/com/exercism/xml/data/Failure.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.exercism.xml.data; - -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText; -import com.google.common.base.MoreObjects; - -public class Failure { - @JacksonXmlProperty(isAttribute = true) - public String message; - @JacksonXmlProperty(isAttribute = true) - public String type; - @JacksonXmlText - public String value; - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("message", message) - .add("type", type) - .add("value", value) - .toString(); - } -} diff --git a/lib/src/main/java/com/exercism/xml/data/Property.java b/lib/src/main/java/com/exercism/xml/data/Property.java deleted file mode 100644 index 9dc84bc..0000000 --- a/lib/src/main/java/com/exercism/xml/data/Property.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.exercism.xml.data; - -public class Property { - // No elements that we care about -} diff --git a/lib/src/main/java/com/exercism/xml/data/SystemErr.java b/lib/src/main/java/com/exercism/xml/data/SystemErr.java deleted file mode 100644 index 702d587..0000000 --- a/lib/src/main/java/com/exercism/xml/data/SystemErr.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.exercism.xml.data; - -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText; -import com.google.common.base.MoreObjects; - -public class SystemErr { - @JacksonXmlText - public String value; - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("value", value) - .toString(); - } -} diff --git a/lib/src/main/java/com/exercism/xml/data/SystemOut.java b/lib/src/main/java/com/exercism/xml/data/SystemOut.java deleted file mode 100644 index 01db98f..0000000 --- a/lib/src/main/java/com/exercism/xml/data/SystemOut.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.exercism.xml.data; - -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText; -import com.google.common.base.MoreObjects; - -public class SystemOut { - @JacksonXmlText - public String value; - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("value", value) - .toString(); - } -} diff --git a/lib/src/main/java/com/exercism/xml/data/TestCase.java b/lib/src/main/java/com/exercism/xml/data/TestCase.java deleted file mode 100644 index 3199297..0000000 --- a/lib/src/main/java/com/exercism/xml/data/TestCase.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.exercism.xml.data; - -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import com.google.common.base.MoreObjects; - -public class TestCase { - @JacksonXmlProperty(isAttribute = true) - public String name; - @JacksonXmlProperty(isAttribute = true) - public String classname; - @JacksonXmlProperty(isAttribute = true) - public double time; - public Failure failure; - public Error error; - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("name", name) - .add("classname", classname) - .add("time", time) - .add("failure", failure) - .add("error", error) - .toString(); - } -} diff --git a/lib/src/main/java/com/exercism/xml/data/TestSuite.java b/lib/src/main/java/com/exercism/xml/data/TestSuite.java deleted file mode 100644 index 3d9bbcb..0000000 --- a/lib/src/main/java/com/exercism/xml/data/TestSuite.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.exercism.xml.data; - -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import com.google.common.base.MoreObjects; - -import java.util.List; - -public class TestSuite { - @JacksonXmlProperty(isAttribute = true) - public String name; - @JacksonXmlProperty(isAttribute = true) - public int tests; - @JacksonXmlProperty(isAttribute = true) - public int skipped; - @JacksonXmlProperty(isAttribute = true) - public int failures; - @JacksonXmlProperty(isAttribute = true) - public int errors; - @JacksonXmlProperty(isAttribute = true) - public String timestamp; - @JacksonXmlProperty(isAttribute = true) - public String hostname; - @JacksonXmlProperty(isAttribute = true) - public double time; - @JacksonXmlElementWrapper - public List properties; - @JacksonXmlElementWrapper(useWrapping = false) - public List testcase; - @JacksonXmlProperty(localName = "system-out") - public SystemOut systemOut; - @JacksonXmlProperty(localName = "system-err") - public SystemErr systemErr; - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("name", name) - .add("tests", tests) - .add("skipped", skipped) - .add("failures", failures) - .add("errors", errors) - .add("hostname", hostname) - .add("time", time) - .add("testcase", testcase) - .add("system-out", systemOut) - .add("system-err", systemErr) - .toString(); - } -} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..4fccafd --- /dev/null +++ b/settings.gradle @@ -0,0 +1,11 @@ +rootProject.name = "java-test-runner" + +include 'lib' +include 'tests:example-all-fail' +include 'tests:example-empty-file' +include 'tests:example-partial-fail' +include 'tests:example-success' +include 'tests:example-success-java17' +include 'tests:example-syntax-error' +include 'tests:example-junit5' +include 'tests:example-v3' diff --git a/tests/example-all-fail/expected_results.json b/tests/example-all-fail/expected_results.json index c42d237..bcbb3e3 100644 --- a/tests/example-all-fail/expected_results.json +++ b/tests/example-all-fail/expected_results.json @@ -1,60 +1,50 @@ { "status" : "fail", - "message" : null, "tests" : [ { - "name" : "LeapTest.testYearDivBy100NotDivBy3IsNotLeapYear", + "name" : "testYearDivBy100NotDivBy3IsNotLeapYear", "test_code" : "@Test\npublic void testYearDivBy100NotDivBy3IsNotLeapYear() {\n assertFalse(leap.isLeapYear(1900));\n}", "status" : "fail", - "message" : "Message: null\nException: java.lang.AssertionError\n\tat LeapTest.testYearDivBy100NotDivBy3IsNotLeapYear(LeapTest.java:49)\n", - "output" : null + "message" : "Message: null\nException: java.lang.AssertionError\n\tat org.junit.Assert.fail(Assert.java:87)\n\tat org.junit.Assert.assertTrue(Assert.java:42)\n\tat org.junit.Assert.assertFalse(Assert.java:65)\n\tat org.junit.Assert.assertFalse(Assert.java:75)\n\tat LeapTest.testYearDivBy100NotDivBy3IsNotLeapYear(LeapTest.java:49)\n\tat java.base/java.lang.reflect.Method.invoke(Method.java:568)\n" }, { - "name" : "LeapTest.testYearDivBy2NotDivBy4InCommonYear", + "name" : "testYearDivBy2NotDivBy4InCommonYear", "test_code" : "@Test\npublic void testYearDivBy2NotDivBy4InCommonYear() {\n assertFalse(leap.isLeapYear(1970));\n}", "status" : "fail", - "message" : "Message: null\nException: java.lang.AssertionError\n\tat LeapTest.testYearDivBy2NotDivBy4InCommonYear(LeapTest.java:25)\n", - "output" : null + "message" : "Message: null\nException: java.lang.AssertionError\n\tat org.junit.Assert.fail(Assert.java:87)\n\tat org.junit.Assert.assertTrue(Assert.java:42)\n\tat org.junit.Assert.assertFalse(Assert.java:65)\n\tat org.junit.Assert.assertFalse(Assert.java:75)\n\tat LeapTest.testYearDivBy2NotDivBy4InCommonYear(LeapTest.java:25)\n\tat java.base/java.lang.reflect.Method.invoke(Method.java:568)\n" }, { - "name" : "LeapTest.testYearDivBy400InLeapYear", + "name" : "testYearDivBy400InLeapYear", "test_code" : "@Test\npublic void testYearDivBy400InLeapYear() {\n assertTrue(leap.isLeapYear(2000));\n}", "status" : "fail", - "message" : "Message: null\nException: java.lang.AssertionError\n\tat LeapTest.testYearDivBy400InLeapYear(LeapTest.java:55)\n", - "output" : null + "message" : "Message: null\nException: java.lang.AssertionError\n\tat org.junit.Assert.fail(Assert.java:87)\n\tat org.junit.Assert.assertTrue(Assert.java:42)\n\tat org.junit.Assert.assertTrue(Assert.java:53)\n\tat LeapTest.testYearDivBy400InLeapYear(LeapTest.java:55)\n\tat java.base/java.lang.reflect.Method.invoke(Method.java:568)\n" }, { - "name" : "LeapTest.testYearNotDivBy4InCommonYear", + "name" : "testYearNotDivBy4InCommonYear", "test_code" : "@Test\npublic void testYearNotDivBy4InCommonYear() {\n assertFalse(leap.isLeapYear(2015));\n}", "status" : "fail", - "message" : "Message: null\nException: java.lang.AssertionError\n\tat LeapTest.testYearNotDivBy4InCommonYear(LeapTest.java:19)\n", - "output" : null + "message" : "Message: null\nException: java.lang.AssertionError\n\tat org.junit.Assert.fail(Assert.java:87)\n\tat org.junit.Assert.assertTrue(Assert.java:42)\n\tat org.junit.Assert.assertFalse(Assert.java:65)\n\tat org.junit.Assert.assertFalse(Assert.java:75)\n\tat LeapTest.testYearNotDivBy4InCommonYear(LeapTest.java:19)\n\tat java.base/java.lang.reflect.Method.invoke(Method.java:568)\n" }, { - "name" : "LeapTest.testYearDivBy4NotDivBy100InLeapYear", + "name" : "testYearDivBy4NotDivBy100InLeapYear", "test_code" : "@Test\npublic void testYearDivBy4NotDivBy100InLeapYear() {\n assertTrue(leap.isLeapYear(1996));\n}", "status" : "fail", - "message" : "Message: null\nException: java.lang.AssertionError\n\tat LeapTest.testYearDivBy4NotDivBy100InLeapYear(LeapTest.java:31)\n", - "output" : null + "message" : "Message: null\nException: java.lang.AssertionError\n\tat org.junit.Assert.fail(Assert.java:87)\n\tat org.junit.Assert.assertTrue(Assert.java:42)\n\tat org.junit.Assert.assertTrue(Assert.java:53)\n\tat LeapTest.testYearDivBy4NotDivBy100InLeapYear(LeapTest.java:31)\n\tat java.base/java.lang.reflect.Method.invoke(Method.java:568)\n" }, { - "name" : "LeapTest.testYearDivBy400NotDivBy125IsLeapYear", + "name" : "testYearDivBy400NotDivBy125IsLeapYear", "test_code" : "@Test\npublic void testYearDivBy400NotDivBy125IsLeapYear() {\n assertTrue(leap.isLeapYear(2400));\n}", "status" : "fail", - "message" : "Message: null\nException: java.lang.AssertionError\n\tat LeapTest.testYearDivBy400NotDivBy125IsLeapYear(LeapTest.java:61)\n", - "output" : null + "message" : "Message: null\nException: java.lang.AssertionError\n\tat org.junit.Assert.fail(Assert.java:87)\n\tat org.junit.Assert.assertTrue(Assert.java:42)\n\tat org.junit.Assert.assertTrue(Assert.java:53)\n\tat LeapTest.testYearDivBy400NotDivBy125IsLeapYear(LeapTest.java:61)\n\tat java.base/java.lang.reflect.Method.invoke(Method.java:568)\n" }, { - "name" : "LeapTest.testYearDivBy100NotDivBy400InCommonYear", + "name" : "testYearDivBy100NotDivBy400InCommonYear", "test_code" : "@Test\npublic void testYearDivBy100NotDivBy400InCommonYear() {\n assertFalse(leap.isLeapYear(2100));\n}", "status" : "fail", - "message" : "Message: null\nException: java.lang.AssertionError\n\tat LeapTest.testYearDivBy100NotDivBy400InCommonYear(LeapTest.java:43)\n", - "output" : null + "message" : "Message: null\nException: java.lang.AssertionError\n\tat org.junit.Assert.fail(Assert.java:87)\n\tat org.junit.Assert.assertTrue(Assert.java:42)\n\tat org.junit.Assert.assertFalse(Assert.java:65)\n\tat org.junit.Assert.assertFalse(Assert.java:75)\n\tat LeapTest.testYearDivBy100NotDivBy400InCommonYear(LeapTest.java:43)\n\tat java.base/java.lang.reflect.Method.invoke(Method.java:568)\n" }, { - "name" : "LeapTest.testYearDivBy200NotDivBy400InCommonYear", + "name" : "testYearDivBy200NotDivBy400InCommonYear", "test_code" : "@Test\npublic void testYearDivBy200NotDivBy400InCommonYear() {\n assertFalse(leap.isLeapYear(1800));\n}", "status" : "fail", - "message" : "Message: null\nException: java.lang.AssertionError\n\tat LeapTest.testYearDivBy200NotDivBy400InCommonYear(LeapTest.java:67)\n", - "output" : null + "message" : "Message: null\nException: java.lang.AssertionError\n\tat org.junit.Assert.fail(Assert.java:87)\n\tat org.junit.Assert.assertTrue(Assert.java:42)\n\tat org.junit.Assert.assertFalse(Assert.java:65)\n\tat org.junit.Assert.assertFalse(Assert.java:75)\n\tat LeapTest.testYearDivBy200NotDivBy400InCommonYear(LeapTest.java:67)\n\tat java.base/java.lang.reflect.Method.invoke(Method.java:568)\n" }, { - "name" : "LeapTest.testYearDivBy4And5InLeapYear", + "name" : "testYearDivBy4And5InLeapYear", "test_code" : "@Test\npublic void testYearDivBy4And5InLeapYear() {\n assertTrue(leap.isLeapYear(1960));\n}", "status" : "fail", - "message" : "Message: null\nException: java.lang.AssertionError\n\tat LeapTest.testYearDivBy4And5InLeapYear(LeapTest.java:37)\n", - "output" : null + "message" : "Message: null\nException: java.lang.AssertionError\n\tat org.junit.Assert.fail(Assert.java:87)\n\tat org.junit.Assert.assertTrue(Assert.java:42)\n\tat org.junit.Assert.assertTrue(Assert.java:53)\n\tat LeapTest.testYearDivBy4And5InLeapYear(LeapTest.java:37)\n\tat java.base/java.lang.reflect.Method.invoke(Method.java:568)\n" } ], "version" : 2 } \ No newline at end of file diff --git a/tests/example-empty-file/expected_results.json b/tests/example-empty-file/expected_results.json index 7cb92f3..95b4e32 100644 --- a/tests/example-empty-file/expected_results.json +++ b/tests/example-empty-file/expected_results.json @@ -1,6 +1,6 @@ { "status" : "error", - "message" : "[ERROR] COMPILATION ERROR : \n[ERROR] /tmp/solution/src/test/java/LeapTest.java:[10,13] cannot find symbol\n symbol: class Leap\n location: class LeapTest\n[ERROR] /tmp/solution/src/test/java/LeapTest.java:[14,20] cannot find symbol\n symbol: class Leap\n location: class LeapTest\n[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.0:testCompile (default-testCompile) on project exercise: Compilation failure: Compilation failure: \n[ERROR] /tmp/solution/src/test/java/LeapTest.java:[10,13] cannot find symbol\n[ERROR] symbol: class Leap\n[ERROR] location: class LeapTest\n[ERROR] /tmp/solution/src/test/java/LeapTest.java:[14,20] cannot find symbol\n[ERROR] symbol: class Leap\n[ERROR] location: class LeapTest\n[ERROR] -> [Help 1]\n[ERROR] \n[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.\n[ERROR] Re-run Maven using the -X switch to enable full debug logging.\n[ERROR] \n[ERROR] For more information about the errors and possible solutions, please read the following articles:\n[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException\n", + "message" : "./src/test/java/LeapTest.java:10: error: cannot find symbol\n private Leap leap;\n ^\n symbol: class Leap\n location: class LeapTest./src/test/java/LeapTest.java:14: error: cannot find symbol\n leap = new Leap();\n ^\n symbol: class Leap\n location: class LeapTest", "tests" : [ ], "version" : 2 } \ No newline at end of file diff --git a/tests/example-junit5/.meta/config.json b/tests/example-junit5/.meta/config.json new file mode 100644 index 0000000..56ff318 --- /dev/null +++ b/tests/example-junit5/.meta/config.json @@ -0,0 +1,26 @@ +{ + "blurb": "Given a year, report if it is a leap year.", + "authors": [ + "sonapraneeth-a" + ], + "contributors": [ + "jmrunkle", + "lemoncurry", + "msomji", + "muzimuzhi", + "sshine" + ], + "files": { + "solution": [ + "src/main/java/Leap.java" + ], + "test": [ + "src/test/java/LeapTest.java" + ], + "example": [ + ".meta/src/reference/java/Leap.java" + ] + }, + "source": "JavaRanch Cattle Drive, exercise 3", + "source_url": "http://www.javaranch.com/leap.jsp" +} diff --git a/tests/example-junit5/build.gradle b/tests/example-junit5/build.gradle new file mode 100644 index 0000000..66b0f37 --- /dev/null +++ b/tests/example-junit5/build.gradle @@ -0,0 +1,23 @@ +plugins { + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation platform('org.junit:junit-bom:5.10.0') + testImplementation 'org.junit.jupiter:junit-jupiter' + testImplementation 'org.assertj:assertj-core:3.15.0' +} + +test { + useJUnitPlatform() + + testLogging { + exceptionFormat = 'full' + showStandardStreams = true + events = ["passed", "failed", "skipped"] + } +} diff --git a/tests/example-junit5/expected_results.json b/tests/example-junit5/expected_results.json new file mode 100644 index 0000000..3fa17ef --- /dev/null +++ b/tests/example-junit5/expected_results.json @@ -0,0 +1,43 @@ +{ + "status" : "fail", + "tests" : [ { + "name" : "testYearDivBy100NotDivBy3IsNotLeapYear()", + "test_code" : "@Test\npublic void testYearDivBy100NotDivBy3IsNotLeapYear() {\n assertThat(leap.isLeapYear(1900)).isFalse();\n}", + "status" : "pass" + }, { + "name" : "testYearDivBy2NotDivBy4InCommonYear()", + "test_code" : "@Test\npublic void testYearDivBy2NotDivBy4InCommonYear() {\n assertThat(leap.isLeapYear(1970)).isFalse();\n}", + "status" : "pass" + }, { + "name" : "testYearDivBy400InLeapYear()", + "test_code" : "@Test\npublic void testYearDivBy400InLeapYear() {\n assertThat(leap.isLeapYear(2000)).isTrue();\n}", + "status" : "fail", + "message" : "Message: \nExpecting:\n \nto be equal to:\n \nbut was not.\nException: org.opentest4j.AssertionFailedError: \nExpecting:\n \nto be equal to:\n \nbut was not.\n\tat java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)\n\tat java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:77)\n\tat java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)\n\tat java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499)\n\tat LeapTest.testYearDivBy400InLeapYear(LeapTest.java:54)\n\tat java.base/java.lang.reflect.Method.invoke(Method.java:568)\n\tat java.base/java.util.ArrayList.forEach(ArrayList.java:1511)\n\tat java.base/java.util.ArrayList.forEach(ArrayList.java:1511)\n" + }, { + "name" : "testYearNotDivBy4InCommonYear()", + "test_code" : "@Test\npublic void testYearNotDivBy4InCommonYear() {\n assertThat(leap.isLeapYear(2015)).isFalse();\n}", + "status" : "pass" + }, { + "name" : "testYearDivBy4NotDivBy100InLeapYear()", + "test_code" : "@Test\npublic void testYearDivBy4NotDivBy100InLeapYear() {\n assertThat(leap.isLeapYear(1996)).isTrue();\n}", + "status" : "pass" + }, { + "name" : "testYearDivBy400NotDivBy125IsLeapYear()", + "test_code" : "@Test\npublic void testYearDivBy400NotDivBy125IsLeapYear() {\n assertThat(leap.isLeapYear(2400)).isTrue();\n}", + "status" : "fail", + "message" : "Message: \nExpecting:\n \nto be equal to:\n \nbut was not.\nException: org.opentest4j.AssertionFailedError: \nExpecting:\n \nto be equal to:\n \nbut was not.\n\tat java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)\n\tat java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:77)\n\tat java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)\n\tat java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499)\n\tat LeapTest.testYearDivBy400NotDivBy125IsLeapYear(LeapTest.java:60)\n\tat java.base/java.lang.reflect.Method.invoke(Method.java:568)\n\tat java.base/java.util.ArrayList.forEach(ArrayList.java:1511)\n\tat java.base/java.util.ArrayList.forEach(ArrayList.java:1511)\n" + }, { + "name" : "testYearDivBy100NotDivBy400InCommonYear()", + "test_code" : "@Test\npublic void testYearDivBy100NotDivBy400InCommonYear() {\n assertThat(leap.isLeapYear(2100)).isFalse();\n}", + "status" : "pass" + }, { + "name" : "testYearDivBy200NotDivBy400InCommonYear()", + "test_code" : "@Test\npublic void testYearDivBy200NotDivBy400InCommonYear() {\n assertThat(leap.isLeapYear(1800)).isFalse();\n}", + "status" : "pass" + }, { + "name" : "testYearDivBy4And5InLeapYear()", + "test_code" : "@Test\npublic void testYearDivBy4And5InLeapYear() {\n assertThat(leap.isLeapYear(1960)).isTrue();\n}", + "status" : "pass" + } ], + "version" : 2 +} \ No newline at end of file diff --git a/tests/example-junit5/src/main/java/Leap.java b/tests/example-junit5/src/main/java/Leap.java new file mode 100644 index 0000000..2cd1c49 --- /dev/null +++ b/tests/example-junit5/src/main/java/Leap.java @@ -0,0 +1,13 @@ +class Leap { + + boolean isLeapYear(int year) { + return ( + ( + (year % 4 == 0) && + (year % 100 != 0) + ) || + (year % 401 == 0) + ); + } + +} diff --git a/tests/example-junit5/src/test/java/LeapTest.java b/tests/example-junit5/src/test/java/LeapTest.java new file mode 100644 index 0000000..4f0b226 --- /dev/null +++ b/tests/example-junit5/src/test/java/LeapTest.java @@ -0,0 +1,68 @@ +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class LeapTest { + + private Leap leap; + + @BeforeEach + public void setup() { + leap = new Leap(); + } + + @Test + public void testYearNotDivBy4InCommonYear() { + assertThat(leap.isLeapYear(2015)).isFalse(); + } + + @Disabled("Remove to run test") + @Test + public void testYearDivBy2NotDivBy4InCommonYear() { + assertThat(leap.isLeapYear(1970)).isFalse(); + } + + @Disabled("Remove to run test") + @Test + public void testYearDivBy4NotDivBy100InLeapYear() { + assertThat(leap.isLeapYear(1996)).isTrue(); + } + + @Disabled("Remove to run test") + @Test + public void testYearDivBy4And5InLeapYear() { + assertThat(leap.isLeapYear(1960)).isTrue(); + } + + @Disabled("Remove to run test") + @Test + public void testYearDivBy100NotDivBy400InCommonYear() { + assertThat(leap.isLeapYear(2100)).isFalse(); + } + + @Disabled("Remove to run test") + @Test + public void testYearDivBy100NotDivBy3IsNotLeapYear() { + assertThat(leap.isLeapYear(1900)).isFalse(); + } + + @Disabled("Remove to run test") + @Test + public void testYearDivBy400InLeapYear() { + assertThat(leap.isLeapYear(2000)).isTrue(); + } + + @Disabled("Remove to run test") + @Test + public void testYearDivBy400NotDivBy125IsLeapYear() { + assertThat(leap.isLeapYear(2400)).isTrue(); + } + + @Disabled("Remove to run test") + @Test + public void testYearDivBy200NotDivBy400InCommonYear() { + assertThat(leap.isLeapYear(1800)).isFalse(); + } +} diff --git a/tests/example-partial-fail/expected_results.json b/tests/example-partial-fail/expected_results.json index 36b4118..22581f4 100644 --- a/tests/example-partial-fail/expected_results.json +++ b/tests/example-partial-fail/expected_results.json @@ -1,60 +1,43 @@ { "status" : "fail", - "message" : null, "tests" : [ { - "name" : "LeapTest.testYearDivBy100NotDivBy3IsNotLeapYear", + "name" : "testYearDivBy100NotDivBy3IsNotLeapYear", "test_code" : "@Test\npublic void testYearDivBy100NotDivBy3IsNotLeapYear() {\n assertFalse(leap.isLeapYear(1900));\n}", - "status" : "pass", - "message" : null, - "output" : null + "status" : "pass" }, { - "name" : "LeapTest.testYearDivBy2NotDivBy4InCommonYear", + "name" : "testYearDivBy2NotDivBy4InCommonYear", "test_code" : "@Test\npublic void testYearDivBy2NotDivBy4InCommonYear() {\n assertFalse(leap.isLeapYear(1970));\n}", - "status" : "pass", - "message" : null, - "output" : null + "status" : "pass" }, { - "name" : "LeapTest.testYearDivBy400InLeapYear", + "name" : "testYearDivBy400InLeapYear", "test_code" : "@Test\npublic void testYearDivBy400InLeapYear() {\n assertTrue(leap.isLeapYear(2000));\n}", "status" : "fail", - "message" : "Message: null\nException: java.lang.AssertionError\n\tat LeapTest.testYearDivBy400InLeapYear(LeapTest.java:55)\n", - "output" : null + "message" : "Message: null\nException: java.lang.AssertionError\n\tat org.junit.Assert.fail(Assert.java:87)\n\tat org.junit.Assert.assertTrue(Assert.java:42)\n\tat org.junit.Assert.assertTrue(Assert.java:53)\n\tat LeapTest.testYearDivBy400InLeapYear(LeapTest.java:55)\n\tat java.base/java.lang.reflect.Method.invoke(Method.java:568)\n" }, { - "name" : "LeapTest.testYearNotDivBy4InCommonYear", + "name" : "testYearNotDivBy4InCommonYear", "test_code" : "@Test\npublic void testYearNotDivBy4InCommonYear() {\n assertFalse(leap.isLeapYear(2015));\n}", - "status" : "pass", - "message" : null, - "output" : null + "status" : "pass" }, { - "name" : "LeapTest.testYearDivBy4NotDivBy100InLeapYear", + "name" : "testYearDivBy4NotDivBy100InLeapYear", "test_code" : "@Test\npublic void testYearDivBy4NotDivBy100InLeapYear() {\n assertTrue(leap.isLeapYear(1996));\n}", - "status" : "pass", - "message" : null, - "output" : null + "status" : "pass" }, { - "name" : "LeapTest.testYearDivBy400NotDivBy125IsLeapYear", + "name" : "testYearDivBy400NotDivBy125IsLeapYear", "test_code" : "@Test\npublic void testYearDivBy400NotDivBy125IsLeapYear() {\n assertTrue(leap.isLeapYear(2400));\n}", "status" : "fail", - "message" : "Message: null\nException: java.lang.AssertionError\n\tat LeapTest.testYearDivBy400NotDivBy125IsLeapYear(LeapTest.java:61)\n", - "output" : null + "message" : "Message: null\nException: java.lang.AssertionError\n\tat org.junit.Assert.fail(Assert.java:87)\n\tat org.junit.Assert.assertTrue(Assert.java:42)\n\tat org.junit.Assert.assertTrue(Assert.java:53)\n\tat LeapTest.testYearDivBy400NotDivBy125IsLeapYear(LeapTest.java:61)\n\tat java.base/java.lang.reflect.Method.invoke(Method.java:568)\n" }, { - "name" : "LeapTest.testYearDivBy100NotDivBy400InCommonYear", + "name" : "testYearDivBy100NotDivBy400InCommonYear", "test_code" : "@Test\npublic void testYearDivBy100NotDivBy400InCommonYear() {\n assertFalse(leap.isLeapYear(2100));\n}", - "status" : "pass", - "message" : null, - "output" : null + "status" : "pass" }, { - "name" : "LeapTest.testYearDivBy200NotDivBy400InCommonYear", + "name" : "testYearDivBy200NotDivBy400InCommonYear", "test_code" : "@Test\npublic void testYearDivBy200NotDivBy400InCommonYear() {\n assertFalse(leap.isLeapYear(1800));\n}", - "status" : "pass", - "message" : null, - "output" : null + "status" : "pass" }, { - "name" : "LeapTest.testYearDivBy4And5InLeapYear", + "name" : "testYearDivBy4And5InLeapYear", "test_code" : "@Test\npublic void testYearDivBy4And5InLeapYear() {\n assertTrue(leap.isLeapYear(1960));\n}", - "status" : "pass", - "message" : null, - "output" : null + "status" : "pass" } ], "version" : 2 } \ No newline at end of file diff --git a/tests/example-success-java17/expected_results.json b/tests/example-success-java17/expected_results.json index 5162616..639d588 100644 --- a/tests/example-success-java17/expected_results.json +++ b/tests/example-success-java17/expected_results.json @@ -1,60 +1,41 @@ { "status" : "pass", - "message" : null, "tests" : [ { - "name" : "LeapTest.testYearDivBy100NotDivBy3IsNotLeapYear", + "name" : "testYearDivBy100NotDivBy3IsNotLeapYear", "test_code" : "@Test\npublic void testYearDivBy100NotDivBy3IsNotLeapYear() {\n assertFalse(leap.isLeapYear(1900));\n}", - "status" : "pass", - "message" : null, - "output" : null + "status" : "pass" }, { - "name" : "LeapTest.testYearDivBy2NotDivBy4InCommonYear", + "name" : "testYearDivBy2NotDivBy4InCommonYear", "test_code" : "@Test\npublic void testYearDivBy2NotDivBy4InCommonYear() {\n assertFalse(leap.isLeapYear(1970));\n}", - "status" : "pass", - "message" : null, - "output" : null + "status" : "pass" }, { - "name" : "LeapTest.testYearDivBy400InLeapYear", + "name" : "testYearDivBy400InLeapYear", "test_code" : "@Test\npublic void testYearDivBy400InLeapYear() {\n assertTrue(leap.isLeapYear(2000));\n}", - "status" : "pass", - "message" : null, - "output" : null + "status" : "pass" }, { - "name" : "LeapTest.testYearNotDivBy4InCommonYear", + "name" : "testYearNotDivBy4InCommonYear", "test_code" : "@Test\npublic void testYearNotDivBy4InCommonYear() {\n assertFalse(leap.isLeapYear(2015));\n}", - "status" : "pass", - "message" : null, - "output" : null + "status" : "pass" }, { - "name" : "LeapTest.testYearDivBy4NotDivBy100InLeapYear", + "name" : "testYearDivBy4NotDivBy100InLeapYear", "test_code" : "@Test\npublic void testYearDivBy4NotDivBy100InLeapYear() {\n assertTrue(leap.isLeapYear(1996));\n}", - "status" : "pass", - "message" : null, - "output" : null + "status" : "pass" }, { - "name" : "LeapTest.testYearDivBy400NotDivBy125IsLeapYear", + "name" : "testYearDivBy400NotDivBy125IsLeapYear", "test_code" : "@Test\npublic void testYearDivBy400NotDivBy125IsLeapYear() {\n assertTrue(leap.isLeapYear(2400));\n}", - "status" : "pass", - "message" : null, - "output" : null + "status" : "pass" }, { - "name" : "LeapTest.testYearDivBy100NotDivBy400InCommonYear", + "name" : "testYearDivBy100NotDivBy400InCommonYear", "test_code" : "@Test\npublic void testYearDivBy100NotDivBy400InCommonYear() {\n assertFalse(leap.isLeapYear(2100));\n}", - "status" : "pass", - "message" : null, - "output" : null + "status" : "pass" }, { - "name" : "LeapTest.testYearDivBy200NotDivBy400InCommonYear", + "name" : "testYearDivBy200NotDivBy400InCommonYear", "test_code" : "@Test\npublic void testYearDivBy200NotDivBy400InCommonYear() {\n assertFalse(leap.isLeapYear(1800));\n}", - "status" : "pass", - "message" : null, - "output" : null + "status" : "pass" }, { - "name" : "LeapTest.testYearDivBy4And5InLeapYear", + "name" : "testYearDivBy4And5InLeapYear", "test_code" : "@Test\npublic void testYearDivBy4And5InLeapYear() {\n assertTrue(leap.isLeapYear(1960));\n}", - "status" : "pass", - "message" : null, - "output" : null + "status" : "pass" } ], "version" : 2 } \ No newline at end of file diff --git a/tests/example-success/expected_results.json b/tests/example-success/expected_results.json index 5162616..639d588 100644 --- a/tests/example-success/expected_results.json +++ b/tests/example-success/expected_results.json @@ -1,60 +1,41 @@ { "status" : "pass", - "message" : null, "tests" : [ { - "name" : "LeapTest.testYearDivBy100NotDivBy3IsNotLeapYear", + "name" : "testYearDivBy100NotDivBy3IsNotLeapYear", "test_code" : "@Test\npublic void testYearDivBy100NotDivBy3IsNotLeapYear() {\n assertFalse(leap.isLeapYear(1900));\n}", - "status" : "pass", - "message" : null, - "output" : null + "status" : "pass" }, { - "name" : "LeapTest.testYearDivBy2NotDivBy4InCommonYear", + "name" : "testYearDivBy2NotDivBy4InCommonYear", "test_code" : "@Test\npublic void testYearDivBy2NotDivBy4InCommonYear() {\n assertFalse(leap.isLeapYear(1970));\n}", - "status" : "pass", - "message" : null, - "output" : null + "status" : "pass" }, { - "name" : "LeapTest.testYearDivBy400InLeapYear", + "name" : "testYearDivBy400InLeapYear", "test_code" : "@Test\npublic void testYearDivBy400InLeapYear() {\n assertTrue(leap.isLeapYear(2000));\n}", - "status" : "pass", - "message" : null, - "output" : null + "status" : "pass" }, { - "name" : "LeapTest.testYearNotDivBy4InCommonYear", + "name" : "testYearNotDivBy4InCommonYear", "test_code" : "@Test\npublic void testYearNotDivBy4InCommonYear() {\n assertFalse(leap.isLeapYear(2015));\n}", - "status" : "pass", - "message" : null, - "output" : null + "status" : "pass" }, { - "name" : "LeapTest.testYearDivBy4NotDivBy100InLeapYear", + "name" : "testYearDivBy4NotDivBy100InLeapYear", "test_code" : "@Test\npublic void testYearDivBy4NotDivBy100InLeapYear() {\n assertTrue(leap.isLeapYear(1996));\n}", - "status" : "pass", - "message" : null, - "output" : null + "status" : "pass" }, { - "name" : "LeapTest.testYearDivBy400NotDivBy125IsLeapYear", + "name" : "testYearDivBy400NotDivBy125IsLeapYear", "test_code" : "@Test\npublic void testYearDivBy400NotDivBy125IsLeapYear() {\n assertTrue(leap.isLeapYear(2400));\n}", - "status" : "pass", - "message" : null, - "output" : null + "status" : "pass" }, { - "name" : "LeapTest.testYearDivBy100NotDivBy400InCommonYear", + "name" : "testYearDivBy100NotDivBy400InCommonYear", "test_code" : "@Test\npublic void testYearDivBy100NotDivBy400InCommonYear() {\n assertFalse(leap.isLeapYear(2100));\n}", - "status" : "pass", - "message" : null, - "output" : null + "status" : "pass" }, { - "name" : "LeapTest.testYearDivBy200NotDivBy400InCommonYear", + "name" : "testYearDivBy200NotDivBy400InCommonYear", "test_code" : "@Test\npublic void testYearDivBy200NotDivBy400InCommonYear() {\n assertFalse(leap.isLeapYear(1800));\n}", - "status" : "pass", - "message" : null, - "output" : null + "status" : "pass" }, { - "name" : "LeapTest.testYearDivBy4And5InLeapYear", + "name" : "testYearDivBy4And5InLeapYear", "test_code" : "@Test\npublic void testYearDivBy4And5InLeapYear() {\n assertTrue(leap.isLeapYear(1960));\n}", - "status" : "pass", - "message" : null, - "output" : null + "status" : "pass" } ], "version" : 2 } \ No newline at end of file diff --git a/tests/example-syntax-error/expected_results.json b/tests/example-syntax-error/expected_results.json index b0acabf..aa3d2a5 100644 --- a/tests/example-syntax-error/expected_results.json +++ b/tests/example-syntax-error/expected_results.json @@ -1,6 +1,6 @@ { "status" : "error", - "message" : "[ERROR] COMPILATION ERROR : \n[ERROR] /tmp/solution/src/main/java/Leap.java:[1,1] class, interface, enum, or record expected\n[ERROR] /tmp/solution/src/main/java/Leap.java:[1,17] reached end of file while parsing\n[ERROR] /tmp/solution/src/main/java/Leap.java:[2,1] reached end of file while parsing\n[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.0:compile (default-compile) on project exercise: Compilation failure: Compilation failure: \n[ERROR] /tmp/solution/src/main/java/Leap.java:[1,1] class, interface, enum, or record expected\n[ERROR] /tmp/solution/src/main/java/Leap.java:[1,17] reached end of file while parsing\n[ERROR] /tmp/solution/src/main/java/Leap.java:[2,1] reached end of file while parsing\n[ERROR] -> [Help 1]\n[ERROR] \n[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.\n[ERROR] Re-run Maven using the -X switch to enable full debug logging.\n[ERROR] \n[ERROR] For more information about the errors and possible solutions, please read the following articles:\n[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException\n", + "message" : "./src/main/java/Leap.java:1: error: class, interface, enum, or record expected\nclassYY Leapy {@\n^./src/main/java/Leap.java:1: error: reached end of file while parsing\nclassYY Leapy {@\n ^./src/main/java/Leap.java:2: error: reached end of file while parsing\n", "tests" : [ ], "version" : 2 } \ No newline at end of file diff --git a/tests/example-v3/build.gradle b/tests/example-v3/build.gradle new file mode 100644 index 0000000..66b0f37 --- /dev/null +++ b/tests/example-v3/build.gradle @@ -0,0 +1,23 @@ +plugins { + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation platform('org.junit:junit-bom:5.10.0') + testImplementation 'org.junit.jupiter:junit-jupiter' + testImplementation 'org.assertj:assertj-core:3.15.0' +} + +test { + useJUnitPlatform() + + testLogging { + exceptionFormat = 'full' + showStandardStreams = true + events = ["passed", "failed", "skipped"] + } +} diff --git a/tests/example-v3/expected_results.json b/tests/example-v3/expected_results.json new file mode 100644 index 0000000..2d98aea --- /dev/null +++ b/tests/example-v3/expected_results.json @@ -0,0 +1,57 @@ +{ + "status" : "pass", + "tests" : [ { + "name" : "totalTimeInMinutes method calculates the correct value for multiple layers", + "test_code" : "@Test\n@Tag(\"task:4\")\n@DisplayName(\"totalTimeInMinutes method calculates the correct value for multiple layers\")\npublic void total_time_in_minutes_for_multiple_layers() {\n assertThat(new Lasagna().totalTimeInMinutes(4, 8)).isEqualTo(16);\n}", + "status" : "pass", + "output" : "Preparation time: 8\n", + "task_id" : 4 + }, { + "name" : "totalTimeInMinutes method calculates the correct value for single layer", + "test_code" : "@Test\n@Tag(\"task:4\")\n@DisplayName(\"totalTimeInMinutes method calculates the correct value for single layer\")\npublic void total_time_in_minutes_for_one_layer() {\n assertThat(new Lasagna().totalTimeInMinutes(1, 30)).isEqualTo(32);\n}", + "status" : "pass", + "output" : "Preparation time: 2\n", + "task_id" : 4 + }, { + "name" : "remainingMinutesInOven method calculates and returns the correct value", + "test_code" : "@Test\n@Tag(\"task:2\")\n@DisplayName(\"remainingMinutesInOven method calculates and returns the correct value\")\npublic void remaining_minutes_in_oven() {\n assertThat(new Lasagna().remainingMinutesInOven(25)).isEqualTo(15);\n}", + "status" : "pass", + "task_id" : 2 + }, { + "name" : "preparationTimeInMinutes method calculates the correct value for single layer", + "test_code" : "@Test\n@Tag(\"task:3\")\n@DisplayName(\"preparationTimeInMinutes method calculates the correct value for single layer\")\npublic void preparation_time_in_minutes_for_one_layer() {\n assertThat(new Lasagna().preparationTimeInMinutes(1)).isEqualTo(2);\n}", + "status" : "pass", + "task_id" : 3 + }, { + "name" : "expectedMinutesInOven method returns the correct value", + "test_code" : "@Test\n@Tag(\"task:1\")\n@DisplayName(\"expectedMinutesInOven method returns the correct value\")\npublic void expected_minutes_in_oven() {\n assertThat(new Lasagna().expectedMinutesInOven()).isEqualTo(40);\n}", + "status" : "pass", + "task_id" : 1 + }, { + "name" : "Implemented the totalTimeInMinutes method", + "test_code" : "@Test\n@Tag(\"task:4\")\n@DisplayName(\"Implemented the totalTimeInMinutes method\")\npublic void implemented_total_time_in_minutes() {\n assertThat(new Lasagna().hasMethod(\"totalTimeInMinutes\", int.class, int.class)).withFailMessage(\"Method totalTimeInMinutes must be created\").isTrue();\n assertThat(new Lasagna().isMethodPublic(\"totalTimeInMinutes\", int.class, int.class)).withFailMessage(\"Method totalTimeInMinutes must be public\").isTrue();\n assertThat(new Lasagna().isMethodReturnType(int.class, \"totalTimeInMinutes\", int.class, int.class)).withFailMessage(\"Method totalTimeInMinutes must return an int\").isTrue();\n}", + "status" : "pass", + "task_id" : 4 + }, { + "name" : "Implemented the remainingMinutesInOven method", + "test_code" : "@Test\n@Tag(\"task:2\")\n@DisplayName(\"Implemented the remainingMinutesInOven method\")\npublic void implemented_remaining_minutes_in_oven() {\n assertThat(new Lasagna().hasMethod(\"remainingMinutesInOven\", int.class)).withFailMessage(\"Method remainingMinutesInOven must be created\").isTrue();\n assertThat(new Lasagna().isMethodPublic(\"remainingMinutesInOven\", int.class)).withFailMessage(\"Method remainingMinutesInOven must be public\").isTrue();\n assertThat(new Lasagna().isMethodReturnType(int.class, \"remainingMinutesInOven\", int.class)).withFailMessage(\"Method remainingMinutesInOven must return an int\").isTrue();\n}", + "status" : "pass", + "task_id" : 2 + }, { + "name" : "preparationTimeInMinutes method calculates the correct value for multiple layers", + "test_code" : "@Test\n@Tag(\"task:3\")\n@DisplayName(\"preparationTimeInMinutes method calculates the correct value for multiple layers\")\npublic void preparation_time_in_minutes_for_multiple_layers() {\n assertThat(new Lasagna().preparationTimeInMinutes(4)).isEqualTo(8);\n}", + "status" : "pass", + "task_id" : 3 + }, { + "name" : "Implemented the expectedMinutesInOven method", + "test_code" : "@Test\n@Tag(\"task:1\")\n@DisplayName(\"Implemented the expectedMinutesInOven method\")\npublic void implemented_expected_minutes_in_oven() {\n assertThat(new Lasagna().hasMethod(\"expectedMinutesInOven\")).withFailMessage(\"Method expectedMinutesInOven must be created\").isTrue();\n assertThat(new Lasagna().isMethodPublic(\"expectedMinutesInOven\")).withFailMessage(\"Method expectedMinutesInOven must be public\").isTrue();\n assertThat(new Lasagna().isMethodReturnType(int.class, \"expectedMinutesInOven\")).withFailMessage(\"Method expectedMinutesInOven must return an int\").isTrue();\n}", + "status" : "pass", + "task_id" : 1 + }, { + "name" : "Implemented the preparationTimeInMinutes method", + "test_code" : "@Test\n@Tag(\"task:3\")\n@DisplayName(\"Implemented the preparationTimeInMinutes method\")\npublic void implemented_preparation_time_in_minutes() {\n assertThat(new Lasagna().hasMethod(\"preparationTimeInMinutes\", int.class)).withFailMessage(\"Method preparationTimeInMinutes must be created\").isTrue();\n assertThat(new Lasagna().isMethodPublic(\"preparationTimeInMinutes\", int.class)).withFailMessage(\"Method preparationTimeInMinutes must be public\").isTrue();\n assertThat(new Lasagna().isMethodReturnType(int.class, \"preparationTimeInMinutes\", int.class)).withFailMessage(\"Method preparationTimeInMinutes must return an int\").isTrue();\n}", + "status" : "pass", + "task_id" : 3 + } ], + "version" : 3 +} \ No newline at end of file diff --git a/tests/example-v3/src/main/java/Lasagna.java b/tests/example-v3/src/main/java/Lasagna.java new file mode 100644 index 0000000..fb6ba0d --- /dev/null +++ b/tests/example-v3/src/main/java/Lasagna.java @@ -0,0 +1,19 @@ +public class Lasagna { + public int expectedMinutesInOven() { + return 40; + } + + public int remainingMinutesInOven(int actualMinutesInOven) { + return expectedMinutesInOven() - actualMinutesInOven; + } + + public int preparationTimeInMinutes(int numberOfLayers) { + return numberOfLayers * 2; + } + + public int totalTimeInMinutes(int numberOfLayers, int actualMinutesInOven) { + var preparationTimeInMinutes = preparationTimeInMinutes(numberOfLayers); + System.out.println("Preparation time: " + preparationTimeInMinutes); + return preparationTimeInMinutes + actualMinutesInOven; + } +} \ No newline at end of file diff --git a/tests/example-v3/src/test/java/LasagnaTest.java b/tests/example-v3/src/test/java/LasagnaTest.java new file mode 100644 index 0000000..ea700ee --- /dev/null +++ b/tests/example-v3/src/test/java/LasagnaTest.java @@ -0,0 +1,112 @@ + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import utils.Lasagna; + +import static org.assertj.core.api.Assertions.*; + +public class LasagnaTest { + + @Test + @Tag("task:1") + @DisplayName("Implemented the expectedMinutesInOven method") + public void implemented_expected_minutes_in_oven() { + assertThat(new Lasagna().hasMethod("expectedMinutesInOven")) + .withFailMessage("Method expectedMinutesInOven must be created") + .isTrue(); + assertThat(new Lasagna().isMethodPublic("expectedMinutesInOven")) + .withFailMessage("Method expectedMinutesInOven must be public") + .isTrue(); + assertThat(new Lasagna().isMethodReturnType(int.class, "expectedMinutesInOven")) + .withFailMessage("Method expectedMinutesInOven must return an int") + .isTrue(); + } + + @Test + @Tag("task:1") + @DisplayName("expectedMinutesInOven method returns the correct value") + public void expected_minutes_in_oven() { + assertThat(new Lasagna().expectedMinutesInOven()).isEqualTo(40); + } + + @Test + @Tag("task:2") + @DisplayName("Implemented the remainingMinutesInOven method") + public void implemented_remaining_minutes_in_oven() { + assertThat(new Lasagna().hasMethod("remainingMinutesInOven", int.class)) + .withFailMessage("Method remainingMinutesInOven must be created") + .isTrue(); + assertThat(new Lasagna().isMethodPublic("remainingMinutesInOven", int.class)) + .withFailMessage("Method remainingMinutesInOven must be public") + .isTrue(); + assertThat(new Lasagna().isMethodReturnType(int.class, "remainingMinutesInOven", int.class)) + .withFailMessage("Method remainingMinutesInOven must return an int") + .isTrue(); + } + + @Test + @Tag("task:2") + @DisplayName("remainingMinutesInOven method calculates and returns the correct value") + public void remaining_minutes_in_oven() { + assertThat(new Lasagna().remainingMinutesInOven(25)).isEqualTo(15); + } + + @Test + @Tag("task:3") + @DisplayName("Implemented the preparationTimeInMinutes method") + public void implemented_preparation_time_in_minutes() { + assertThat(new Lasagna().hasMethod("preparationTimeInMinutes", int.class)) + .withFailMessage("Method preparationTimeInMinutes must be created") + .isTrue(); + assertThat(new Lasagna().isMethodPublic("preparationTimeInMinutes", int.class)) + .withFailMessage("Method preparationTimeInMinutes must be public") + .isTrue(); + assertThat(new Lasagna().isMethodReturnType(int.class, "preparationTimeInMinutes", int.class)) + .withFailMessage("Method preparationTimeInMinutes must return an int") + .isTrue(); + } + + @Test + @Tag("task:3") + @DisplayName("preparationTimeInMinutes method calculates the correct value for single layer") + public void preparation_time_in_minutes_for_one_layer() { + assertThat(new Lasagna().preparationTimeInMinutes(1)).isEqualTo(2); + } + + @Test + @Tag("task:3") + @DisplayName("preparationTimeInMinutes method calculates the correct value for multiple layers") + public void preparation_time_in_minutes_for_multiple_layers() { + assertThat(new Lasagna().preparationTimeInMinutes(4)).isEqualTo(8); + } + + @Test + @Tag("task:4") + @DisplayName("Implemented the totalTimeInMinutes method") + public void implemented_total_time_in_minutes() { + assertThat(new Lasagna().hasMethod("totalTimeInMinutes", int.class, int.class)) + .withFailMessage("Method totalTimeInMinutes must be created") + .isTrue(); + assertThat(new Lasagna().isMethodPublic("totalTimeInMinutes", int.class, int.class)) + .withFailMessage("Method totalTimeInMinutes must be public") + .isTrue(); + assertThat(new Lasagna().isMethodReturnType(int.class, "totalTimeInMinutes", int.class, int.class)) + .withFailMessage("Method totalTimeInMinutes must return an int") + .isTrue(); + } + + @Test + @Tag("task:4") + @DisplayName("totalTimeInMinutes method calculates the correct value for single layer") + public void total_time_in_minutes_for_one_layer() { + assertThat(new Lasagna().totalTimeInMinutes(1, 30)).isEqualTo(32); + } + + @Test + @Tag("task:4") + @DisplayName("totalTimeInMinutes method calculates the correct value for multiple layers") + public void total_time_in_minutes_for_multiple_layers() { + assertThat(new Lasagna().totalTimeInMinutes(4, 8)).isEqualTo(16); + } +} \ No newline at end of file diff --git a/tests/example-v3/src/test/java/utils/Lasagna.java b/tests/example-v3/src/test/java/utils/Lasagna.java new file mode 100644 index 0000000..dd64121 --- /dev/null +++ b/tests/example-v3/src/test/java/utils/Lasagna.java @@ -0,0 +1,41 @@ +package utils; + +public class Lasagna extends ReflectionProxy { + + @Override + public String getTargetClassName() { + return "Lasagna"; + } + + public int expectedMinutesInOven() { + try { + return invokeMethod("expectedMinutesInOven", new Class[]{}); + } catch (Exception e) { + throw new UnsupportedOperationException("Please implement the expectedMinutesInOven() method"); + } + } + + public int remainingMinutesInOven(int actualMinutes) { + try { + return invokeMethod("remainingMinutesInOven", new Class[]{int.class}, actualMinutes); + } catch (Exception e) { + throw new UnsupportedOperationException("Please implement the remainingMinutesInOven(int) method"); + } + } + + public int preparationTimeInMinutes(int amountLayers) { + try { + return invokeMethod("preparationTimeInMinutes", new Class[]{int.class}, amountLayers); + } catch (Exception e) { + throw new UnsupportedOperationException("Please implement the preparationTimeInMinutes(int) method"); + } + } + + public int totalTimeInMinutes(int amountLayers, int actualMinutes) { + try { + return invokeMethod("totalTimeInMinutes", new Class[]{int.class, int.class}, amountLayers, actualMinutes); + } catch (Exception e) { + throw new UnsupportedOperationException("Please implement the totalTimeInMinutes(int, int) method"); + } + } +} \ No newline at end of file diff --git a/tests/example-v3/src/test/java/utils/ReflectionProxy.java b/tests/example-v3/src/test/java/utils/ReflectionProxy.java new file mode 100644 index 0000000..9766dac --- /dev/null +++ b/tests/example-v3/src/test/java/utils/ReflectionProxy.java @@ -0,0 +1,487 @@ +package utils; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; + +import static java.lang.Class.forName; + +public abstract class ReflectionProxy { + + /** + * An instance of the target class (if found) + */ + private final Object target; + + /** + * A constructor to instantiate the target class with parameters + * @param args An array of parameters matching the constructor from the target class + */ + protected ReflectionProxy(Object... args) { + this.target = instantiateTarget(args); + } + + /** + * Abstract method that defines the fully qualified name of the target class + * @return The fully qualified name of the target class + */ + public abstract String getTargetClassName(); + + /** + * Getter for the target instance + * @return The target instance + */ + public Object getTarget() { + return target; + } + + /** + * Gets the target class + * @return The target class if it exists, null otherwise + */ + public Class getTargetClass() { + try { + return forName(this.getTargetClassName()); + } catch (ClassNotFoundException e) { + return null; + } + } + + /** + * Checks if the target class has a specific method + * @param name The name of the method to find + * @param parameterTypes The list of parameter types + * @return True if the method is found, false otherwise + */ + public boolean hasMethod(String name, Class... parameterTypes) { + Class targetClass = getTargetClass(); + if (targetClass == null || name == null) { + return false; + } + try { + Method m = targetClass.getDeclaredMethod(name, parameterTypes); + return m != null; + } catch (NoSuchMethodException e) { + return false; + } + } + + /** + * Checks if a method from the target class is public + * @param name The name of the method + * @param parameterTypes A list of method parameters + * @return True if the method exists and is public, false otherwise + */ + public boolean isMethodPublic(String name, Class... parameterTypes) { + Class targetClass = getTargetClass(); + if (targetClass == null || name == null) { + return false; + } + try { + Method m = targetClass.getDeclaredMethod(name, parameterTypes); + return Modifier.isPublic(m.getModifiers()); + } catch (NoSuchMethodException e) { + return false; + } + } + + /** + * Checks if a method from the target class returns the correct type + * @param returnType The type of return value + * @param name The name of the method + * @param parameterTypes The list of method parameters + * @return + */ + public boolean isMethodReturnType(Class returnType, String name, Class... parameterTypes) { + return isMethodReturnType(returnType, null, name, parameterTypes); + } + + /** + * Invokes a method from the target instance + * @param methodName The name of the method + * @param parameterTypes The list of parameter types + * @param parameterValues The list with values for the method parameters + * @param The result type we expect the method to be + * @return The value returned by the method + */ + protected T invokeMethod(String methodName, Class[] parameterTypes, Object... parameterValues) { + if (target == null) { + return null; + } + try { + // getDeclaredMethod is used to get protected/private methods + Method method = target.getClass().getDeclaredMethod(methodName, parameterTypes); + method.setAccessible(true); + return (T) method.invoke(target, parameterValues); + } catch (NoSuchMethodException e) { + try { + // try getting it from parent class, but only public methods will work + Method method = target.getClass().getMethod(methodName, parameterTypes); + method.setAccessible(true); + return (T) method.invoke(target, parameterValues); + } catch (Exception ex) { + return null; + } + } catch (Exception e) { + return null; + } + } + + /** + * Creates an instance of the target class + * @param args The list of constructor parameters + * @return An instance of the target class, if found, or null otherwise + */ + private Object instantiateTarget(Object... args) { + Class targetClass = getTargetClass(); + if (targetClass == null) { + return null; + } + Constructor[] constructors = getAllConstructors(); + for (Constructor c : constructors) { + if (c.getParameterCount() == args.length) { + try { + return c.newInstance(args); + } catch (Exception e) { + // do nothing; + } + } + } + return null; + } + + /** + * Gets a list with all the constructors defined by the target class + * @return A list with all constructor definitions + */ + private Constructor[] getAllConstructors() { + Class targetClass = getTargetClass(); + if (targetClass == null) { + return new Constructor[]{}; + } + return targetClass.getConstructors(); + } + + + //region Unused + + /** + * The default constructor, for when you have already an instance of the target class + * @param target An instance of the target class + */ + protected ReflectionProxy(Object target) { + this.target = target; + } + + /** + * Checks if the target class exists + * @return True if the class exists, false otherwise + */ + public boolean existsClass() { + return getTargetClass() != null; + } + + /** + * Checks if the class implements a specific interface + * @param anInterface The interface to check + * @return True if the class implements the referred interface, false otherwise + */ + public boolean implementsInterface(Class anInterface) { + Class targetClass = getTargetClass(); + if (targetClass == null || anInterface == null) { + return false; + } + return anInterface.isAssignableFrom(targetClass); + } + + /** + * Checks if the target class has a specific property + * @param name The name of the property to find + * @return True if the property is found, false otherwise + */ + public boolean hasProperty(String name) { + Class targetClass = getTargetClass(); + if (targetClass == null || name == null) { + return false; + } + try { + Field f = targetClass.getDeclaredField(name); + return f != null; + } catch (NoSuchFieldException e) { + return false; + } + } + + /** + * Checks if an existing property has the type we expect + * @param name The name of the property to check + * @param type The type you are expecting the property to be + * @return True if the property is found and has the specified type, false otherwise + */ + public boolean isPropertyOfType(String name, Class type) { + return isPropertyOfType(name, type, null); + } + + /** + * Checks if an existing Collection type has the parameterized type (Generics) as expected (eg. List) + * @param name The name of the property + * @param type The type of the property (eg. List) + * @param parameterizedType The parameterized property (eg. String) + * @return True if the parameterized type matches the desired type, false otherwise + */ + public boolean isPropertyOfType(String name, Class type, Class parameterizedType) { + Class targetClass = getTargetClass(); + if (targetClass == null || name == null || type == null) { + return false; + } + try { + Field f = targetClass.getDeclaredField(name); + if (!f.getType().equals(type)) { + return false; + } + if (parameterizedType == null) { + return true; + } + if (!(f.getGenericType() instanceof ParameterizedType)) { + return false; + } + ParameterizedType pType = (ParameterizedType) f.getGenericType(); + return pType.getActualTypeArguments()[0].equals(parameterizedType); + + } catch (NoSuchFieldException e) { + return false; + } + } + + /** + * Checks if a property is private + * @param name The name of the property + * @return True if the property exists and is private, false otherwise + */ + public boolean isPropertyPrivate(String name) { + Class targetClass = getTargetClass(); + if (targetClass == null || name == null) { + return false; + } + try { + Field f = targetClass.getDeclaredField(name); + return Modifier.isPrivate(f.getModifiers()); + } catch (NoSuchFieldException e) { + return false; + } + } + + /** + * Checks if a method from the target class returns a correct parameterized collection (Generics) + * @param returnType The return type we expect (eg. List) + * @param parameterizedType The parameterized type we expect (eg. String) + * @param name The name of the method + * @param parameterTypes A list of method parameter types + * @return True if the method returns the correct parameterized collection, false otherwise + */ + public boolean isMethodReturnType(Class returnType, Class parameterizedType, + String name, Class... parameterTypes) { + Class targetClass = getTargetClass(); + if (targetClass == null || name == null) { + return false; + } + try { + Method m = targetClass.getDeclaredMethod(name, parameterTypes); + if (!m.getReturnType().equals(returnType)) { + return false; + } + if (parameterizedType == null) { + return true; + } + if (!(m.getGenericReturnType() instanceof ParameterizedType)) { + return false; + } + ParameterizedType pType = (ParameterizedType) m.getGenericReturnType(); + return pType.getActualTypeArguments()[0].equals(parameterizedType); + } catch (NoSuchMethodException e) { + return false; + } + } + + /** + * Checks if a target class has a specific constructor + * @param parameterTypes The list of desired parameter types + * @return True if the constructor exists, false otherwise + */ + public boolean hasConstructor(Class... parameterTypes) { + Class targetClass = getTargetClass(); + if (targetClass == null) { + return false; + } + try { + Constructor c = targetClass.getDeclaredConstructor(parameterTypes); + return c != null; + } catch (NoSuchMethodException e) { + return false; + } + } + + /** + * Checks if a specific constructor from the target class is public + * @param parameterTypes A list of parameter types + * @return True if the constructor is found and is public, false otherwise + */ + public boolean isConstructorPublic(Class... parameterTypes) { + Class targetClass = getTargetClass(); + if (targetClass == null) { + return false; + } + try { + Constructor c = targetClass.getDeclaredConstructor(parameterTypes); + return Modifier.isPublic(c.getModifiers()); + } catch (NoSuchMethodException e) { + return false; + } + } + + /** + * Proxy for the 'equals' method + * @param obj The ReflexionProxy object you want to compare against + * @return True if both targets are equal, false otherwise + */ + public boolean equals(Object obj) { + if (target == null || !(obj instanceof ReflectionProxy)) { + return false; + } + try { + Method method = target.getClass().getMethod("equals", Object.class); + method.setAccessible(true); + return (boolean) method.invoke(target, ((ReflectionProxy) obj).getTarget()); + } catch (Exception e) { + return false; + } + } + + /** + * Proxy for the 'hashCode' method + * @return The hashCode from the target class + */ + public int hashCode() { + if (target == null) { + return 0; + } + try { + Method method = target.getClass().getMethod("hashCode"); + method.setAccessible(true); + return (int) method.invoke(target); + } catch (Exception e) { + return 0; + } + } + + /** + * Proxy for the 'toString' method from the target class + * @return The result of 'toString' from the target instance + */ + public String toString() { + return invokeMethod("toString", new Class[]{ }); + } + + /** + * Gets a property value from the target instance (if it exists) + * @param propertyName The name of the property + * @param The type we are expecting it to be + * @return The value of the property (if it exists) + */ + protected T getPropertyValue(String propertyName) { + if (target == null || !hasProperty(propertyName)) { + return null; + } + try { + Field field = target.getClass().getDeclaredField(propertyName); + field.setAccessible(true); + return (T) field.get(target); + } catch (Exception e) { + return null; + } + } + + /** + * Checks if the target class is abstract + * @return True if the target class exists and is abstract, false otherwise + */ + public boolean isAbstract() { + Class targetClass = getTargetClass(); + if (targetClass == null) { + return false; + } + return Modifier.isAbstract(targetClass.getModifiers()); + } + + /** + * Checks if the target class extends another + * @param className The fully qualified name of the class it should extend + * @return True if the target class extends the specified one, false otherwise + */ + public boolean extendsClass(String className) { + Class targetClass = getTargetClass(); + if (targetClass == null) { + return false; + } + try { + Class parentClass = Class.forName(className); + return parentClass.isAssignableFrom(targetClass); + } catch (ClassNotFoundException e) { + return false; + } + } + + /** + * Checks if the target class is an interface + * @return True if the target class exists and is an interface, false otherwise + */ + public boolean isInterface() { + Class targetClass = getTargetClass(); + if (targetClass == null) { + return false; + } + return targetClass.isInterface(); + } + + /** + * Checks if a method is abstract + * @param name The name of the method + * @param parameterTypes The list of method parameter types + * @return True if the method exists and is abstract, false otherwise + */ + public boolean isMethodAbstract(String name, Class... parameterTypes) { + Class targetClass = getTargetClass(); + if (targetClass == null || name == null) { + return false; + } + try { + Method m = targetClass.getDeclaredMethod(name, parameterTypes); + return Modifier.isAbstract(m.getModifiers()); + } catch (NoSuchMethodException e) { + return false; + } + } + + /** + * Checks if a method is protected + * @param name The name of the method + * @param parameterTypes The list of method parameter types + * @return True if the method exists and is protected, false otherwise + */ + public boolean isMethodProtected(String name, Class... parameterTypes) { + Class targetClass = getTargetClass(); + if (targetClass == null || name == null) { + return false; + } + try { + Method m = targetClass.getDeclaredMethod(name, parameterTypes); + return Modifier.isProtected(m.getModifiers()); + } catch (NoSuchMethodException e) { + return false; + } + } + + //endregion +} \ No newline at end of file