commit 3826b5af0cc2a83ba19922a30cf814d43959dcdc Author: Oshgnacknak Date: Sat Jan 11 16:41:08 2025 +0100 Squashed 'solution/H03/' content from commit ea0c1f2 git-subtree-dir: solution/H03 git-subtree-split: ea0c1f269eb8d625ec4dee6f9888b6e41db30c81 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..38866d3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# Editor configuration, see https://editorconfig.org + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true + +[{*.yml,*.json}] +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..528b8e0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,88 @@ +### Intellij ### +.idea/ +*.iws +/out/ +*.iml +.idea_modules/ +atlassian-ide-plugin.xml + +### VS-Code ### +.vscode/ +.VSCodeCounter/ + +### Eclipse ### +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders +.externalToolBuilders/ +*.launch +.factorypath +.recommenders/ +.apt_generated/ +.project +.classpath + +### Linux ### +*~ +.fuse_hidden* +.directory +.Trash-* +.nfs* + +### macOS ### +.DS_Store +.AppleDouble +.LSOverride +Icon +._* +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### NetBeans ### +nbproject/private/ +build/ +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db +*.stackdump +[Dd]esktop.ini +$RECYCLE.BIN/ +*.lnk + +### Gradle ### +.gradle +/build/ +out/ +gradle-app.setting +!gradle-wrapper.jar +.gradletasknamecache + +*.hprof +screenshots/ + +jagr.conf diff --git a/README.md b/README.md new file mode 100644 index 0000000..6d8aef4 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# Musterlösung zu Hausübung 03 + +Beachten Sie die Hinweise zum Herunterladen, Importieren, Bearbeitern, Exportieren und Hochladen in unserem +[Studierenden-Guide](https://wiki.tudalgo.org/) diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..1f8acc6 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,46 @@ +plugins { + alias(libs.plugins.algomate) + alias(libs.plugins.style) +} + +version = file("version").readLines().first() + +exercise { + assignmentId.set("h03") +} + +submission { + // ACHTUNG! + // Setzen Sie im folgenden Bereich Ihre TU-ID (NICHT Ihre Matrikelnummer!), Ihren Nachnamen und Ihren Vornamen + // in Anführungszeichen (z.B. "ab12cdef" für Ihre TU-ID) ein! + // BEISPIEL: + // studentId = "ab12cdef" + // firstName = "sol_first" + // lastName = "sol_last" + studentId = "ab12cdef" + firstName = "sol_first" + lastName = "sol_last" + + // Optionally require own tests for mainBuildSubmission task. Default is false + requireTests = false +} + +dependencies { + implementation(libs.algoutils.student) + implementation(libs.fopbot) +} + +jagr { + graders { + val graderPublic by getting { + configureDependencies { + implementation(libs.algoutils.tutor) + } + } + val graderPrivate by creating { + parent(graderPublic) + graderName.set("FOP-2425-H03-Private") + rubricProviderName.set("h03.H03_RubricProvider") + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..fc61595 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,11 @@ +[versions] +algoutils = "0.9.1-SNAPSHOT" + +[plugins] +algomate = { id = "org.tudalgo.algomate", version = "0.7.1" } +style = { id = "org.sourcegrade.style", version = "3.0.0" } + +[libraries] +algoutils-student = { module = "org.tudalgo:algoutils-student", version.ref = "algoutils" } +algoutils-tutor = { module = "org.tudalgo:algoutils-tutor", version.ref = "algoutils" } +fopbot = { module = "org.tudalgo:fopbot", version = "0.8.1" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..c1962a7 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..36074ad --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..aeb74cb --- /dev/null +++ b/gradlew @@ -0,0 +1,245 @@ +#!/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 + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$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/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..7f0802c --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,11 @@ +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + mavenLocal() + maven("https://s01.oss.sonatype.org/content/repositories/snapshots") + maven("https://jitpack.io") + mavenCentral() + } +} + +rootProject.name = "H03-Root" diff --git a/src/graderPrivate/java/h03/H03_RubricProvider.java b/src/graderPrivate/java/h03/H03_RubricProvider.java new file mode 100644 index 0000000..6a0bedd --- /dev/null +++ b/src/graderPrivate/java/h03/H03_RubricProvider.java @@ -0,0 +1,295 @@ +package h03; + +import h03.h3_1.HackingRobotTest; +import h03.h3_1.MovementTypeTest; +import h03.h3_2.DoublePowerRobotTest; +import h03.h3_2.VersatileRobotTest; +import h03.h3_3.RobotsChallengeTest; +import org.sourcegrade.jagr.api.rubric.*; +import org.sourcegrade.jagr.api.testing.RubricConfiguration; +import org.tudalgo.algoutils.transform.SolutionMergingClassTransformer; +import org.tudalgo.algoutils.transform.util.headers.MethodHeader; +import org.tudalgo.algoutils.tutor.general.json.JsonParameterSet; + +import static org.tudalgo.algoutils.tutor.general.jagr.RubricUtils.criterion; +import static org.tudalgo.algoutils.tutor.general.jagr.RubricUtils.manualGrader; + +public class H03_RubricProvider implements RubricProvider { + + private static final Criterion H3_1_1 = Criterion.builder() + .shortDescription("H3.1.1 | Movement types") + .maxPoints(1) + .addChildCriteria( + criterion( + "Die Enumeration MovementType ist korrekt deklariert und umfasst DIAGONAL, OVERSTEP, TELEPORT.", + JUnitTestRef.ofMethod(() -> MovementTypeTest.class.getDeclaredMethod("testEnum")), + JUnitTestRef.ofMethod(() -> MovementTypeTest.class.getDeclaredMethod("testEnumConstants")) + ) + ) + .build(); + + private static final Criterion H3_1_2 = Criterion.builder() + .shortDescription("H3.1.2 | First class") + .maxPoints(1) + .addChildCriteria( + criterion( + "Die Klasse HackingRobot ist korrekt deklariert mit den Attributen type und robotTypes.", + JUnitTestRef.ofMethod(() -> HackingRobotTest.class.getDeclaredMethod("testClassHeader")), + JUnitTestRef.ofMethod(() -> HackingRobotTest.class.getDeclaredMethod("testFields")) + ) + ) + .build(); + + private static final Criterion H3_1_3 = Criterion.builder() + .shortDescription("H3.1.3 | Robot under construction") + .maxPoints(3) + .addChildCriteria( + criterion( + "Der Konstruktor von HackingRobot ist korrekt deklariert.", + JUnitTestRef.ofMethod(() -> HackingRobotTest.class.getDeclaredMethod("testConstructorHeader")) + ), + criterion( + "Der Konstruktor ruft den Konstruktor der Basisklasse Robot korrekt auf.", + JUnitTestRef.ofMethod(() -> HackingRobotTest.class.getDeclaredMethod("testConstructorSuperCall")) + ), + criterion( + "Das Attribut robotTypes ist korrekt initialisiert und die Elemente korrekt nach order verschoben.", + JUnitTestRef.ofMethod(() -> HackingRobotTest.class.getDeclaredMethod("testConstructorSetsFields", boolean.class)) + ) + ) + .build(); + + private static final Criterion H3_1_4 = Criterion.builder() + .shortDescription("H3.1.4 | Access to robot types") + .maxPoints(3) + .addChildCriteria( + criterion( + "Die Methode getType gibt den aktuellen Robotertyp korrekt zurück.", + JUnitTestRef.ofMethod(() -> HackingRobotTest.class.getDeclaredMethod("testGetType")) + ), + criterion( + "Die Methode getNextType gibt den nächsten Typ korrekt zurück, wenn nicht zum Index 0 zurückgesprungen wird.", + JUnitTestRef.ofMethod(() -> HackingRobotTest.class.getDeclaredMethod("testGetNextTypeNoMod", int.class)) + ), + criterion( + "Die Methode getNextType gibt den nächsten Typ korrekt zurück, wenn zum Index 0 zurückgesprungen werden muss.", + JUnitTestRef.ofMethod(() -> HackingRobotTest.class.getDeclaredMethod("testGetNextTypeMod", int.class)) + ) + ) + .build(); + + private static final Criterion H3_1_5 = Criterion.builder() + .shortDescription("H3.1.5 | Swap type") + .addChildCriteria( + criterion( + "Die Methode shuffle(int itNr) funktioniert korrekt und ändert den Robotertyp zufällig.", + JUnitTestRef.ofMethod(() -> HackingRobotTest.class.getDeclaredMethod("testShuffleWithParams_SetField", int.class)) + ), + criterion( + "Die Methode gibt true zurück, wenn der Typ geändert wurde, sonst false.", + JUnitTestRef.ofMethod(() -> HackingRobotTest.class.getDeclaredMethod("testShuffleWithParams_ReturnValue", int.class)) + ) + ) + .build(); + + private static final Criterion H3_1_6 = Criterion.builder() + .shortDescription("H3.1.6 | Are you sure of the swap?") + .addChildCriteria( + criterion( + "Die Methode shuffle() ist korrekt überladen und garantiert, dass der Typ des Roboters geändert wird.", + 2, + JUnitTestRef.ofMethod(() -> HackingRobotTest.class.getDeclaredMethod("testShuffleNoParams")) + ) + ) + .build(); + + private static final Criterion H3_1 = Criterion.builder() + .shortDescription("H3.1 | HackingRobot") + .maxPoints(12).addChildCriteria( + H3_1_1, + H3_1_2, + H3_1_3, + H3_1_4, + H3_1_5, + H3_1_6 + ) + .build(); + + private static final Criterion H3_2_1 = Criterion.builder() + .shortDescription("H3.2.1 | DoublePowerRobot") + .maxPoints(4).addChildCriteria( + criterion( + "Die Klasse DoublePowerRobot ist korrekt deklariert mit den Attributen und Methoden.", + JUnitTestRef.ofMethod(() -> DoublePowerRobotTest.class.getDeclaredMethod("testClassHeader")), + JUnitTestRef.ofMethod(() -> DoublePowerRobotTest.class.getDeclaredMethod("testFields")), + JUnitTestRef.ofMethod(() -> DoublePowerRobotTest.class.getDeclaredMethod("testMethodHeaders")) + ), + criterion( + "Der Konstruktor initialisiert doublePowerTypes korrekt mit den aktuellen und nächsten Typen.", + JUnitTestRef.ofMethod(() -> DoublePowerRobotTest.class.getDeclaredMethod("testConstructorSetsField", boolean.class)) + ), + criterion( + "Die Methode shuffle(int itNr) für DoublePowerRobot funktioniert korrekt.", + JUnitTestRef.ofMethod(() -> DoublePowerRobotTest.class.getDeclaredMethod("testShuffleWithParams", int.class)) + ), + criterion( + "Die Methode shuffle() für DoublePowerRobot aktualisiert den zweiten Typ korrekt.", + JUnitTestRef.ofMethod(() -> DoublePowerRobotTest.class.getDeclaredMethod("testShuffleNoParams", int.class)) + ) + ) + .build(); + + private static final Criterion H3_2_2 = Criterion.builder() + .shortDescription("H3.2.2 | VersatileRobot") + .maxPoints(4).addChildCriteria( + criterion( + "Die Klasse VersatileRobot ist korrekt deklariert.", + JUnitTestRef.ofMethod(() -> VersatileRobotTest.class.getDeclaredMethod("testClassHeader")) + ), + criterion( + "Der Konstruktor der Klasse VersatileRobot setzt y = x, wenn der Typ DIAGONAL ist.", + JUnitTestRef.ofMethod(() -> VersatileRobotTest.class.getDeclaredMethod("testConstructor")) + ), + criterion( + "Die Methode shuffle(int itNr) funktioniert korrekt.", + JUnitTestRef.ofMethod(() -> VersatileRobotTest.class.getDeclaredMethod("testShuffleWithParams")) + ), + criterion( + "Die Methode shuffle() setzt korrekt die y-Koordinate, wenn der Typ DIAGONAL ist.", + JUnitTestRef.ofMethod(() -> VersatileRobotTest.class.getDeclaredMethod("testShuffleNoParams")) + ) + ) + .build(); + + private static final Criterion H3_2 = Criterion.builder() + .shortDescription("H3.2 | Special Hacking Robots") + .maxPoints(8).addChildCriteria(H3_2_1, H3_2_2) + .build(); + + private static final Criterion H3_3_1 = Criterion.builder() + .shortDescription("H3.3.1 | First things first") + .maxPoints(1) + .addChildCriteria( + criterion( + "Die Klasse RobotsChallenge ist korrekt deklariert.", + JUnitTestRef.ofMethod(() -> RobotsChallengeTest.class.getDeclaredMethod("testClassHeader")) + ) + ) + .build(); + + private static final Criterion H3_3_2 = Criterion.builder() + .shortDescription("H3.3.2 | Participators over here") + .maxPoints(2) + .addChildCriteria( + criterion( + "Der Konstruktor von RobotsChallenge weist korrekt die Parameter begin, goal, und robots zu.", + JUnitTestRef.ofMethod(() -> RobotsChallengeTest.class.getDeclaredMethod("testConstructor", int.class)) + ), + criterion( + "Der Konstruktor sorgt dafür, dass winThreshold auf 2 gesetzt wird (direkt oder indirekt).", + JUnitTestRef.ofMethod(() -> RobotsChallengeTest.class.getDeclaredMethod("testWinThreshold")) + ) + ) + .build(); + + private static final Criterion H3_3_3 = Criterion.builder() + .shortDescription("H3.3.3 | Quick maths") + .maxPoints(3) + .addChildCriteria( + criterion( + "Die Methode calculateStepsDiagonal ist korrekt implementiert und berechnet die Schritte für den Typ DIAGONAL.", + JUnitTestRef.ofMethod(() -> RobotsChallengeTest.class.getDeclaredMethod("testCalculateStepsDiagonal", JsonParameterSet.class)) + ), + criterion( + "Die Methode calculateStepsOverstep ist korrekt implementiert und berechnet die Schritte für den Typ OVERSTEP.", + JUnitTestRef.ofMethod(() -> RobotsChallengeTest.class.getDeclaredMethod("testCalculateStepsOverstep", JsonParameterSet.class)) + ), + criterion( + "Die Methode calculateStepsTeleport ist korrekt implementiert und berechnet die Schritte für den Typ TELEPORT.", + JUnitTestRef.ofMethod(() -> RobotsChallengeTest.class.getDeclaredMethod("testCalculateStepsTeleport", JsonParameterSet.class)) + ) + ) + .build(); + + private static final Criterion H3_3_4 = Criterion.builder() + .shortDescription("H3.3.4 | Let the show begin") + .maxPoints(3) + .addChildCriteria( + criterion( + "Die Methode findWinners berechnet korrekt die Schritte für jeden Roboter.", + JUnitTestRef.ofMethod(() -> RobotsChallengeTest.class.getDeclaredMethod("testFindWinnersCalc")) + ), + criterion( + "Die Methode verwendet Math.min korrekt, um die minimalen Schritte zu berechnen.", + JUnitTestRef.ofMethod(() -> RobotsChallengeTest.class.getDeclaredMethod("testFindWinnersMin")) + ), + criterion( + "Gewinner werden korrekt in der Liste winners gespeichert.", + JUnitTestRef.ofMethod(() -> RobotsChallengeTest.class.getDeclaredMethod("testFindWinnersReturn")) + ) + ) + .build(); + + private static final Criterion H3_3 = Criterion.builder() + .shortDescription("H3.3 | Let Robots Compete!") + .maxPoints(9) + .addChildCriteria( + H3_3_1, + H3_3_2, + H3_3_3, + H3_3_4 + ) + .build(); + + private static final Criterion H3_4 = Criterion.builder() + .shortDescription("H3.4 | Documentation") + .addChildCriteria( + Criterion.builder() + .shortDescription("Alle öffentlichen Klassen, Methoden und Konstruktoren sind mit JavaDoc korrekt dokumentiert.") + .maxPoints(3) + .grader(manualGrader(3)) + .build() + ) + .build(); + + private static final Rubric RUBRIC = Rubric.builder() + .title("H03 | Mission Robots: The Ultimate Grid Race") + .addChildCriteria( + H3_1, + H3_2, + H3_3, + H3_4 + ) + .build(); + + + @Override + public Rubric getRubric() { + return RUBRIC; + } + + @Override + public void configure(RubricConfiguration configuration) { + configuration.addTransformer(() -> new SolutionMergingClassTransformer.Builder("h03") + .addSolutionClass("h03.Main") + .addSolutionClass("h03.RobotsChallenge", "h03.robots.RobotsChallenge", "robots.RobotsChallenge") + .addSolutionClass("h03.robots.DoublePowerRobot", "h03.DoublePowerRobot", "robot.DoublePowerRobot") + .addSolutionClass("h03.robots.HackingRobot", "h03.HackingRobot", "robot.HackingRobot") + .addSolutionClass("h03.robots.MovementType", "h03.MovementType", "robot.MovementType") + .addSolutionClass("h03.robots.VersatileRobot", "h03.VersatileRobot", "robot.VersatileRobot") + .addMethodReplacement( + MethodHeader.of(Math.class, "min", int.class, int.class), + MethodHeader.of(MathMinMock.class, "min", int.class, int.class)) + .addMethodReplacement( + MethodHeader.of(Math.class, "min", float.class, float.class), + MethodHeader.of(MathMinMock.class, "min", float.class, float.class)) + .addMethodReplacement( + MethodHeader.of(Math.class, "min", long.class, long.class), + MethodHeader.of(MathMinMock.class, "min", long.class, long.class)) + .addMethodReplacement( + MethodHeader.of(Math.class, "min", double.class, double.class), + MethodHeader.of(MathMinMock.class, "min", double.class, double.class)) + .setSimilarity(0.80) + .build()); + } +} diff --git a/src/graderPrivate/java/h03/MathMinMock.java b/src/graderPrivate/java/h03/MathMinMock.java new file mode 100644 index 0000000..23991c9 --- /dev/null +++ b/src/graderPrivate/java/h03/MathMinMock.java @@ -0,0 +1,31 @@ +package h03; + +import kotlin.Pair; + +import java.util.ArrayList; +import java.util.List; + +public class MathMinMock { + + public static final List> MIN_INVOCATIONS = new ArrayList<>(); + + public static int min(int a, int b) { + MIN_INVOCATIONS.add(new Pair<>(a, b)); + return Math.min(a, b); + } + + public static long min(long a, long b) { + MIN_INVOCATIONS.add(new Pair<>((int) a, (int) b)); + return Math.min(a, b); + } + + public static float min(float a, float b) { + MIN_INVOCATIONS.add(new Pair<>((int) a, (int) b)); + return Math.min(a, b); + } + + public static double min(double a, double b) { + MIN_INVOCATIONS.add(new Pair<>((int) a, (int) b)); + return Math.min(a, b); + } +} diff --git a/src/graderPrivate/java/h03/TestConstants.java b/src/graderPrivate/java/h03/TestConstants.java new file mode 100644 index 0000000..97b118f --- /dev/null +++ b/src/graderPrivate/java/h03/TestConstants.java @@ -0,0 +1,11 @@ +package h03; + +import java.util.concurrent.ThreadLocalRandom; + +public class TestConstants { + public static long RANDOM_SEED = ThreadLocalRandom.current().nextLong(); + + public static final int TEST_TIMEOUT_IN_SECONDS = 5; + + public static final int TEST_ITERATIONS = 5; +} diff --git a/src/graderPrivate/java/h03/TestJsonGenerators.java b/src/graderPrivate/java/h03/TestJsonGenerators.java new file mode 100644 index 0000000..92d331c --- /dev/null +++ b/src/graderPrivate/java/h03/TestJsonGenerators.java @@ -0,0 +1,69 @@ +package h03; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIf; + +import java.io.IOException; + +import static h03.TestConstants.TEST_ITERATIONS; + +@DisabledIf("org.tudalgo.algoutils.tutor.general.Utils#isJagrRun()") +public class TestJsonGenerators { + + @Test + public void generateCalculateStepsDiagonalDataSet() throws IOException { + TestUtils.generateJsonTestData( + (mapper, index, rnd) -> { + int beginOrig = rnd.nextInt(10); + int begin = beginOrig / 2; + int goal = rnd.nextInt(20); + int expected = Math.abs(begin - goal); + + return mapper.createObjectNode() + .put("begin", beginOrig) + .put("goal", goal) + .put("expected", expected); + }, + TEST_ITERATIONS, + "CalculateStepsDiagonalDataSet" + ); + } + + @Test + public void generateCalculateStepsOverstepDataSet() throws IOException { + TestUtils.generateJsonTestData( + (mapper, index, rnd) -> { + int beginOrig = rnd.nextInt(10); + int begin = beginOrig / 2; + int goal = rnd.nextInt(20); + int expected = (Math.abs(begin - goal) % 2 == 0) ? Math.abs(begin - goal) : Math.abs(begin - goal) + 1; + + return mapper.createObjectNode() + .put("begin", beginOrig) + .put("goal", goal) + .put("expected", expected); + }, + TEST_ITERATIONS, + "CalculateStepsOverstepDataSet" + ); + } + + @Test + public void generateCalculateStepsTeleportDataSet() throws IOException { + TestUtils.generateJsonTestData( + (mapper, index, rnd) -> { + int beginOrig = rnd.nextInt(10); + int begin = beginOrig / 2; + int goal = rnd.nextInt(20); + int expected = (Math.abs(begin - goal) % 2 == 0) ? Math.abs(begin - goal) / 2 : (Math.abs(begin - goal) / 2) + 2; + + return mapper.createObjectNode() + .put("begin", beginOrig) + .put("goal", goal) + .put("expected", expected); + }, + TEST_ITERATIONS, + "CalculateStepsTeleportDataSet" + ); + } +} diff --git a/src/graderPrivate/java/h03/TestUtils.java b/src/graderPrivate/java/h03/TestUtils.java new file mode 100644 index 0000000..03d322d --- /dev/null +++ b/src/graderPrivate/java/h03/TestUtils.java @@ -0,0 +1,60 @@ +package h03; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.io.IOException; +import java.nio.file.Paths; +import java.util.Random; + +import static h03.TestConstants.RANDOM_SEED; + +public abstract class TestUtils { + + /** + * A generator for JSON test data. + */ + public interface JsonGenerator { + /** + * Generates a JSON object node. + * + * @param mapper The object mapper to use. + * @param index The index of the object node. + * @param rnd The random number generator to use. + * @return The generated JSON object node. + */ + ObjectNode generateJson(ObjectMapper mapper, int index, Random rnd); + } + + /** + * Generates and saves JSON test data. + * + * @param generator The generator to use. + * @param amount The amount of test data to generate. + * @param fileName The file name to save the test data to (without extension). + * @throws IOException If an I/O error occurs. + */ + public static void generateJsonTestData(final JsonGenerator generator, final int amount, final String fileName) throws IOException { + final var seed = RANDOM_SEED; + final var random = new java.util.Random(seed); + final ObjectMapper mapper = new ObjectMapper(); + final ArrayNode arrayNode = mapper.createArrayNode(); + System.out.println("Generating test data with seed: " + seed); + for (int i = 0; i < amount; i++) { + arrayNode.add(generator.generateJson(mapper, i, random)); + } + + final var path = Paths.get( + "src", + "graderPrivate", + "resources", + "h03", + fileName + ".generated.json" + ).toAbsolutePath(); + System.out.printf("Saving to file: %s%n", path); + final var file = path.toFile(); + file.createNewFile(); + mapper.writerWithDefaultPrettyPrinter().writeValue(file, arrayNode); + } +} diff --git a/src/graderPrivate/java/h03/h3_1/HackingRobotTest.java b/src/graderPrivate/java/h03/h3_1/HackingRobotTest.java new file mode 100644 index 0000000..e599b13 --- /dev/null +++ b/src/graderPrivate/java/h03/h3_1/HackingRobotTest.java @@ -0,0 +1,326 @@ +package h03.h3_1; + +import fopbot.Robot; +import fopbot.World; +import h03.robots.HackingRobot; +import h03.robots.MovementType; +import kotlin.Triple; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.objectweb.asm.Type; +import org.sourcegrade.jagr.api.rubric.TestForSubmission; +import org.tudalgo.algoutils.transform.util.headers.ClassHeader; +import org.tudalgo.algoutils.transform.util.headers.FieldHeader; +import org.tudalgo.algoutils.transform.util.headers.MethodHeader; +import org.tudalgo.algoutils.tutor.general.assertions.Context; + +import java.lang.reflect.*; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +import static org.tudalgo.algoutils.transform.SubmissionExecutionHandler.*; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.*; + +@TestForSubmission +public class HackingRobotTest { + + @BeforeAll + public static void setup() { + World.setSize(5, 5); + } + + @AfterEach + public void tearDown() { + resetAll(); + } + + @Test + public void testClassHeader() { + ClassHeader originalClassHeader = getOriginalClassHeader(HackingRobot.class); + + assertTrue(Modifier.isPublic(originalClassHeader.access()), emptyContext(), result -> + "Class HackingRobot is not declared public"); + assertEquals(Type.getInternalName(Robot.class), originalClassHeader.superName(), emptyContext(), result -> + "Class HackingRobot does not have correct superclass"); + } + + @Test + public void testFields() { + FieldHeader type = assertNotNull(getOriginalFieldHeader(HackingRobot.class, "type"), emptyContext(), + result -> "Field 'type' does not exist"); + assertTrue(Modifier.isPrivate(type.modifiers()), emptyContext(), result -> + "Field 'type' in HackingRobot is not declared private"); + assertFalse(Modifier.isStatic(type.modifiers()), emptyContext(), result -> + "Field 'type' in HackingRobot is declared static"); + assertEquals(Type.getDescriptor(MovementType.class), type.descriptor(), emptyContext(), result -> + "Field 'type' in HackingRobot does not have correct type"); + + FieldHeader robotTypes = assertNotNull(getOriginalFieldHeader(HackingRobot.class, "robotTypes"), emptyContext(), + result -> "Field 'robotTypes' does not exist"); + assertTrue(Modifier.isPrivate(robotTypes.modifiers()), emptyContext(), result -> + "Field robotTypes in HackingRobot is not declared private"); + assertFalse(Modifier.isStatic(robotTypes.modifiers()), emptyContext(), result -> + "Field robotTypes in HackingRobot is declared static"); + assertEquals(Type.getDescriptor(MovementType[].class), robotTypes.descriptor(), emptyContext(), result -> + "Field robotTypes in HackingRobot does not have correct type"); + + // NOTE: it's impossible to test for default value when field is modified in constructor + } + + @Test + public void testConstructorHeader() { + MethodHeader constructor = assertNotNull(getOriginalMethodHeader(HackingRobot.class, int.class, int.class, boolean.class), + emptyContext(), + result -> "Constructor 'HackingRobot(int, int, boolean)' does not exist"); + assertTrue(Modifier.isPublic(constructor.modifiers()), emptyContext(), result -> + "Constructor 'HackingRobot(int, int, boolean)' is not declared public"); + } + + @Test + public void testConstructorSuperCall() { + Delegation.disable(MethodHeader.of(HackingRobot.class, int.class, int.class, boolean.class)); + + int x = 2; + int y = 2; + Context.Builder contextBuilder = contextBuilder() + .add("x", x) + .add("y", y); + Robot hackingRobotInstance = getHackingRobotInstance(x, y, false, contextBuilder); + Context context = contextBuilder.add("HackingRobot instance", hackingRobotInstance).build(); + + assertEquals(x, hackingRobotInstance.getX(), context, result -> + "Incorrect value for parameter x passed to super constructor"); + assertEquals(y, hackingRobotInstance.getY(), context, result -> + "Incorrect value for parameter y passed to super constructor"); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testConstructorSetsFields(boolean order) { + Delegation.disable(MethodHeader.of(HackingRobot.class, int.class, int.class, boolean.class)); + + List expectedRobotTypes = order ? + List.of("DIAGONAL", "TELEPORT", "OVERSTEP") : + List.of("OVERSTEP", "DIAGONAL", "TELEPORT"); + int x = 2; + int y = 2; + Context.Builder contextBuilder = contextBuilder() + .add("x", x) + .add("y", y); + Robot hackingRobotInstance = getHackingRobotInstance(x, y, order, contextBuilder); + Context context = contextBuilder.add("HackingRobot instance", hackingRobotInstance).build(); + + assertEquals(expectedRobotTypes, + Arrays.stream(FieldHeader.of(HackingRobot.class, "robotTypes").getValue(hackingRobotInstance)) + .map(Enum::name) + .toList(), + context, + result -> "The values of array robotTypes in HackingRobot were not shifted correctly"); + } + + @Test + public void testGetType() { + Delegation.disable(MethodHeader.of(HackingRobot.class, "getType")); + + int x = 2; + int y = 2; + Context.Builder contextBuilder = contextBuilder() + .add("x", x) + .add("y", y); + HackingRobot hackingRobotInstance = getHackingRobotInstance(x, y, null, contextBuilder); + Context baseContext = contextBuilder.add("HackingRobot instance", hackingRobotInstance).build(); + + for (MovementType movementType : MovementType.values()) { + FieldHeader.of(HackingRobot.class, "type").setValue(hackingRobotInstance, movementType); + Context context = contextBuilder() + .add(baseContext) + .add("Field 'type'", movementType) + .build(); + assertCallEquals(movementType, hackingRobotInstance::getType, context, result -> + "The enum constant returned by 'getType()' is incorrect"); + } + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2}) + public void testGetNextTypeNoMod(int offset) { + testGetNextTypeMod(offset); + } + + @ParameterizedTest + @ValueSource(ints = {3, 4, 5, 6}) + public void testGetNextTypeMod(int offset) { + testGetNextType(offset); + } + +/* + @Test + public void testGetRandom() throws Throwable { + // Header + assertTrue((HACKING_ROBOT_GET_RANDOM_LINK.get().modifiers() & Modifier.PUBLIC) != 0, emptyContext(), result -> + "Method getRandom(int) in HackingRobot was not declared public"); + assertEquals(int.class, HACKING_ROBOT_GET_RANDOM_LINK.get().returnType().reflection(), emptyContext(), result -> + "Method getRandom(int) has incorrect return type"); + + // Code + Object hackingRobotInstance = Mockito.mock(HACKING_ROBOT_LINK.get().reflection(), Mockito.CALLS_REAL_METHODS); + List returnedInts = new LinkedList<>(); + for (int i = 50; i <= 100; i++) { + int result = HACKING_ROBOT_GET_RANDOM_LINK.get().invoke(hackingRobotInstance, i); + final int finalI = i; + assertTrue(result >= 0 && result < i, contextBuilder().add("limit", i).build(), r -> + "Result of getRandom(%d) is not within bounds [0, %d]".formatted(finalI, finalI - 1)); + returnedInts.add(result); + } + + assertTrue(returnedInts.stream().anyMatch(i -> i >= 3), emptyContext(), result -> + "50 invocations of getRandom(int) didn't return any number > 2, which is extremely unlikely"); + } +*/ + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2}) + public void testShuffleWithParams_SetField(int index) { + MovementType[] movementTypeConstants = MovementType.values(); + Triple invocationResult = testShuffleWithParams(index); + + assertEquals(movementTypeConstants[index], + FieldHeader.of(HackingRobot.class, "type").getValue(invocationResult.getSecond()), + invocationResult.getFirst(), + result -> "Field 'type' in HackingRobot was not set to the correct value"); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2}) + public void testShuffleWithParams_ReturnValue(int index) { + Triple invocationResult = testShuffleWithParams(index); + + assertEquals(index != 0, invocationResult.getThird(), invocationResult.getFirst(), result -> + "Method 'shuffle(int)' in HackingRobot did not return the expected value"); + } + + @Test + public void testShuffleNoParams() { + // Header + MethodHeader shuffle = assertNotNull(getOriginalMethodHeader(HackingRobot.class, "shuffle"), emptyContext(), + result -> "Method 'shuffle()' does not exist"); + + assertTrue(Modifier.isPublic(shuffle.modifiers()), emptyContext(), result -> + "Method 'shuffle()' in HackingRobot was not declared public"); + assertEquals(Type.VOID_TYPE, Type.getReturnType(shuffle.descriptor()), emptyContext(), result -> + "Method 'shuffle()' has incorrect return type"); + + // Body + int limit = 5; + AtomicInteger counter = new AtomicInteger(0); + Delegation.disable(MethodHeader.of(HackingRobot.class, "shuffle")); + Substitution.enable(MethodHeader.of(HackingRobot.class, "shuffle", int.class), + invocation -> counter.incrementAndGet() >= limit); + + Context.Builder contextBuilder = contextBuilder() + .add("x", 0) + .add("y", 0); + HackingRobot hackingRobotInstance = getHackingRobotInstance(0, 0, null, contextBuilder); + Context context = contextBuilder.add("HackingRobot instance", hackingRobotInstance).build(); + + call(hackingRobotInstance::shuffle, context, result -> + "An exception occurred while invoking 'shuffle()' in HackingRobot"); + assertEquals(limit, counter.get(), context, result -> + "Method 'shuffle()' in HackingRobot did not return after 'shuffle(int)' returned true / was invoked %d times".formatted(limit)); + } + + /** + * Create a new HackingRobot instance. + * + * @param x the x coordinate + * @param y the y coordinate + * @param order the order parameter. May be {@code null}, in which case the constructor is called with + * {@code false} first and then {@code true} if an exception was thrown + * @param contextBuilder an optional context builder to append the {@code order} parameter to + * @return a new HackingRobot instance + */ + private HackingRobot getHackingRobotInstance(int x, int y, @Nullable Boolean order, @Nullable Context.Builder contextBuilder) { + Consumer appendContext = b -> { + if (contextBuilder != null) { + contextBuilder.add("order", b); + } + }; + HackingRobot hackingRobotInstance; + + if (order != null) { + hackingRobotInstance = new HackingRobot(x, y, order); + appendContext.accept(order); + } else { + try { + hackingRobotInstance = new HackingRobot(x, y, false); + appendContext.accept(false); + } catch (Throwable t1) { + System.err.printf("Could not invoke HackingRobot's constructor with params (%d, %d, false):%n%s%n", x, y, t1.getMessage()); + try { + hackingRobotInstance = new HackingRobot(x, y, true); + appendContext.accept(true); + } catch (Throwable t2) { + System.err.printf("Could not invoke HackingRobot's constructor with params (%d, %d, true):%n%s%n", x, y, t2.getMessage()); + throw new RuntimeException("Could not create an instance of HackingRobot"); + } + } + } + + return hackingRobotInstance; + } + + private void testGetNextType(int offset) { + Delegation.disable(MethodHeader.of(HackingRobot.class, "getNextType")); + + int x = 2; + int y = 2; + Context.Builder contextBuilder = contextBuilder() + .add("x", x) + .add("y", y); + HackingRobot hackingRobotInstance = getHackingRobotInstance(x, y, null, contextBuilder); + MovementType[] movementTypeConstants = MovementType.values(); + Context context = contextBuilder + .add("HackingRobot instance", hackingRobotInstance) + .add("Field 'type'", movementTypeConstants[offset % movementTypeConstants.length]) + .add("Field 'robotTypes'", movementTypeConstants) + .build(); + + FieldHeader.of(HackingRobot.class, "type") + .setValue(hackingRobotInstance, movementTypeConstants[offset % movementTypeConstants.length]); + FieldHeader.of(HackingRobot.class, "robotTypes") + .setValue(hackingRobotInstance, movementTypeConstants); + assertCallEquals(movementTypeConstants[(offset + 1) % movementTypeConstants.length], + hackingRobotInstance::getNextType, + context, + result -> "The value returned by 'getNextType()' is incorrect"); + } + + private Triple testShuffleWithParams(int index) { + Substitution.enable(MethodHeader.of(HackingRobot.class, "getRandom", int.class), invocation -> index); + Delegation.disable(MethodHeader.of(HackingRobot.class, "shuffle", int.class)); + + MovementType[] movementTypeConstants = MovementType.values(); + Context.Builder contextBuilder = contextBuilder() + .add("x", 0) + .add("y", 0); + HackingRobot hackingRobotInstance = getHackingRobotInstance(0, 0, null, contextBuilder); + contextBuilder.add("HackingRobot instance", hackingRobotInstance); + + FieldHeader.of(HackingRobot.class, "type").setValue(hackingRobotInstance, movementTypeConstants[0]); + FieldHeader.of(HackingRobot.class, "robotTypes").setValue(hackingRobotInstance, movementTypeConstants); + Context context = contextBuilder + .add("Field 'type'", movementTypeConstants[0]) + .add("Field 'robotTypes'", movementTypeConstants) + .add("getRandom(int) return value", index) + .build(); + + return new Triple<>(context, hackingRobotInstance, callObject(() -> hackingRobotInstance.shuffle(1), context, result -> + "An exception occurred while invoking shuffle(int)")); + } +} diff --git a/src/graderPrivate/java/h03/h3_1/MovementTypeTest.java b/src/graderPrivate/java/h03/h3_1/MovementTypeTest.java new file mode 100644 index 0000000..8e1e0a4 --- /dev/null +++ b/src/graderPrivate/java/h03/h3_1/MovementTypeTest.java @@ -0,0 +1,35 @@ +package h03.h3_1; + +import h03.robots.MovementType; +import org.junit.jupiter.api.Test; +import org.sourcegrade.jagr.api.rubric.TestForSubmission; +import org.tudalgo.algoutils.transform.util.headers.ClassHeader; + +import java.lang.reflect.Modifier; +import java.util.Set; + +import static org.tudalgo.algoutils.transform.SubmissionExecutionHandler.*; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.*; + +@TestForSubmission +public class MovementTypeTest { + + @Test + public void testEnum() { + ClassHeader orignalClassHeader = getOriginalClassHeader(MovementType.class); + assertTrue(Modifier.isPublic(orignalClassHeader.access()), emptyContext(), result -> + "Enum MovementType was not declared public"); + } + + @Test + public void testEnumConstants() { + Set expectedConstants = Set.of("DIAGONAL", "OVERSTEP", "TELEPORT"); + assertEquals(expectedConstants.size(), getOriginalEnumConstants(MovementType.class).size(), emptyContext(), + result -> "Enum MovementType does not have the correct number of constants"); + + for (String expected : expectedConstants) { + assertNotNull(getOriginalEnumConstant(MovementType.class, expected), emptyContext(), + result -> "Enum constant %s not found in MovementType".formatted(expected)); + } + } +} diff --git a/src/graderPrivate/java/h03/h3_2/DoublePowerRobotTest.java b/src/graderPrivate/java/h03/h3_2/DoublePowerRobotTest.java new file mode 100644 index 0000000..0c035ea --- /dev/null +++ b/src/graderPrivate/java/h03/h3_2/DoublePowerRobotTest.java @@ -0,0 +1,155 @@ +package h03.h3_2; + +import fopbot.World; +import h03.robots.DoublePowerRobot; +import h03.robots.HackingRobot; +import h03.robots.MovementType; +import net.bytebuddy.jar.asm.Type; +import org.apache.commons.lang3.function.TriConsumer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.sourcegrade.jagr.api.rubric.TestForSubmission; +import org.tudalgo.algoutils.transform.util.headers.ClassHeader; +import org.tudalgo.algoutils.transform.util.headers.FieldHeader; +import org.tudalgo.algoutils.transform.util.headers.MethodHeader; +import org.tudalgo.algoutils.tutor.general.assertions.Context; + +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.List; + +import static org.tudalgo.algoutils.transform.SubmissionExecutionHandler.*; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.*; + +@TestForSubmission +public class DoublePowerRobotTest { + + @BeforeAll + public static void setup() { + World.setSize(5, 5); + } + + @AfterEach + public void tearDown() { + resetAll(); + } + + @Test + public void testClassHeader() { + ClassHeader originalClassHeader = getOriginalClassHeader(DoublePowerRobot.class); + assertTrue(Modifier.isPublic(originalClassHeader.access()), emptyContext(), result -> + "Class DoublePowerRobot is not declared public"); + assertEquals(Type.getInternalName(HackingRobot.class), originalClassHeader.superName(), emptyContext(), result -> + "Class DoublePowerRobot does not have correct superclass"); + } + + @Test + public void testFields() { + FieldHeader doublePowerTypes = assertNotNull(getOriginalFieldHeader(DoublePowerRobot.class, "doublePowerTypes"), emptyContext(), + result -> "Field 'doublePowerTypes' does not exist"); + + assertEquals(Type.getDescriptor(MovementType[].class), doublePowerTypes.descriptor(), emptyContext(), + result -> "Field 'doublePowerTypes' in DoublePowerRobot does not have correct type"); + } + + @Test + public void testMethodHeaders() { + assertNotNull(getOriginalMethodHeader(DoublePowerRobot.class, "shuffle"), emptyContext(), + result -> "Method 'shuffle()' does not exist"); + + assertNotNull(getOriginalMethodHeader(DoublePowerRobot.class, "shuffle", int.class), emptyContext(), + result -> "Method 'shuffle(int)' does not exist"); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testConstructorSetsField(boolean order) { + Delegation.disable(MethodHeader.of(DoublePowerRobot.class, int.class, int.class, boolean.class)); + + List expectedDoublePowerTypes = order ? + List.of("DIAGONAL", "TELEPORT") : + List.of("OVERSTEP", "DIAGONAL"); + int x = 2; + int y = 2; + Context context = contextBuilder() + .add("x", x) + .add("y", y) + .add("order", order) + .build(); + + DoublePowerRobot instance = callObject(() -> new DoublePowerRobot(x, y, order), context, result -> + "An exception occurred while invoking constructor of class DoublePowerRobot"); + List actualDoublePowerTypes = Arrays.stream(FieldHeader.of(DoublePowerRobot.class, "doublePowerTypes") + .getValue(instance)) + .map(Enum::name) + .toList(); + assertEquals(expectedDoublePowerTypes, actualDoublePowerTypes, context, result -> + "Array doublePowerTypes does not contain the correct values"); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3}) + public void testShuffleWithParams(int offset) { + testShuffle(offset, MethodHeader.of(DoublePowerRobot.class, "shuffle", int.class), + (instance, context, shuffleReturnValue) -> { + boolean returnValue = callObject(() -> instance.shuffle(1), context, result -> + "An exception occurred while invoking 'shuffle(int)'"); + + assertEquals(shuffleReturnValue, returnValue, context, result -> "Return value of 'shuffle(int)' is incorrect"); + }); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3}) + public void testShuffleNoParams(int offset) { + testShuffle(offset, MethodHeader.of(DoublePowerRobot.class, "shuffle"), + (instance, context, ignored) -> { + call(instance::shuffle, context, result -> "An exception occurred while invoking 'shuffle()'"); + }); + } + + private void testShuffle(int offset, MethodHeader shuffleMethod, TriConsumer instanceConsumer) { + MovementType[] movementTypes = MovementType.values(); + MovementType getTypeReturnValue = movementTypes[offset % movementTypes.length]; + MovementType getNextTypeReturnValue = movementTypes[(offset + 1) % movementTypes.length]; + int getRandomReturnValue = 1; + boolean shuffleReturnValue = false; + + Substitution.enable(MethodHeader.of(HackingRobot.class, "getType"), invocation -> getTypeReturnValue); + Substitution.enable(MethodHeader.of(HackingRobot.class, "getNextType"), invocation -> getNextTypeReturnValue); + Substitution.enable(MethodHeader.of(HackingRobot.class, "getRandom", int.class), invocation -> getRandomReturnValue); + Substitution.enable(MethodHeader.of(HackingRobot.class, "shuffle", int.class), invocation -> shuffleReturnValue); + Substitution.enable(MethodHeader.of(HackingRobot.class, "shuffle"), invocation -> null); + Delegation.disable(shuffleMethod); + + int x = 2; + int y = 2; + boolean order = false; + Context context = contextBuilder() + .add("x", x) + .add("y", y) + .add("order", order) + .add("super.getType() return value", getTypeReturnValue) + .add("super.getNextType() return value", getNextTypeReturnValue) + .add("super.getRandom(int) return value", getRandomReturnValue) + .add("super.shuffle(int) return value", shuffleReturnValue) + .build(); + + DoublePowerRobot instance = callObject(() -> new DoublePowerRobot(x, y, order), context, result -> + "An exception occurred while invoking constructor of class DoublePowerRobot"); + instanceConsumer.accept(instance, context, shuffleReturnValue); + + FieldHeader doublePowerTypes = FieldHeader.of(DoublePowerRobot.class, "doublePowerTypes"); + assertEquals(getTypeReturnValue, + doublePowerTypes.getValue(instance)[0], + context, + result -> "Value of doublePowerTypes[0] is incorrect"); + assertEquals(getNextTypeReturnValue, + doublePowerTypes.getValue(instance)[1], + context, + result -> "Value of doublePowerTypes[1] is incorrect"); + } +} diff --git a/src/graderPrivate/java/h03/h3_2/VersatileRobotTest.java b/src/graderPrivate/java/h03/h3_2/VersatileRobotTest.java new file mode 100644 index 0000000..0eec31d --- /dev/null +++ b/src/graderPrivate/java/h03/h3_2/VersatileRobotTest.java @@ -0,0 +1,109 @@ +package h03.h3_2; + +import fopbot.World; +import h03.robots.HackingRobot; +import h03.robots.MovementType; +import h03.robots.VersatileRobot; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.objectweb.asm.Type; +import org.sourcegrade.jagr.api.rubric.TestForSubmission; +import org.tudalgo.algoutils.transform.util.headers.ClassHeader; +import org.tudalgo.algoutils.transform.util.headers.MethodHeader; +import org.tudalgo.algoutils.tutor.general.assertions.Context; + +import java.lang.reflect.Modifier; + +import static org.tudalgo.algoutils.transform.SubmissionExecutionHandler.*; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.*; + +@TestForSubmission +public class VersatileRobotTest { + + private final MovementType getTypeReturnValue = MovementType.DIAGONAL; + private final MovementType getNextTypeReturnValue = MovementType.OVERSTEP; + private final int getRandomReturnValue = 1; + private final boolean shuffleReturnValue = false; + + private final int x = 0; + private final int y = 4; + private final boolean order = false; + private final boolean exchange = false; + private final Context context = contextBuilder() + .add("x", x) + .add("y", y) + .add("order", order) + .add("exchange", exchange) + .add("super.getType() return value", getTypeReturnValue) + .add("super.getNextType() return value", getNextTypeReturnValue) + .add("super.getRandom(int) return value", getRandomReturnValue) + .add("super.shuffle(int) return value", shuffleReturnValue) + .build(); + + @BeforeAll + public static void setup() { + World.setSize(5, 5); + } + + private void setupEnvironment(MethodHeader methodHeader) { + Substitution.enable(MethodHeader.of(HackingRobot.class, "getType"), invocation -> getTypeReturnValue); + Substitution.enable(MethodHeader.of(HackingRobot.class, "getNextType"), invocation -> getNextTypeReturnValue); + Substitution.enable(MethodHeader.of(HackingRobot.class, "getRandom", int.class), invocation -> getRandomReturnValue); + Substitution.enable(MethodHeader.of(HackingRobot.class, "shuffle", int.class), invocation -> shuffleReturnValue); + Substitution.enable(MethodHeader.of(HackingRobot.class, "shuffle"), invocation -> null); + Delegation.disable(methodHeader); + } + + @AfterEach + public void tearDown() { + resetAll(); + } + + @Test + public void testClassHeader() { + ClassHeader originalClassHeader = getOriginalClassHeader(VersatileRobot.class); + + assertTrue(Modifier.isPublic(originalClassHeader.access()), emptyContext(), result -> + "Class VersatileRobot was not declared public"); + assertEquals(Type.getInternalName(HackingRobot.class), originalClassHeader.superName(), emptyContext(), result -> + "Class VersatileRobot does not extend HackingRobot"); + } + + @Test + public void testConstructor() { + setupEnvironment(MethodHeader.of(VersatileRobot.class, int.class, int.class, boolean.class, boolean.class)); + + VersatileRobot instance = callObject(() -> new VersatileRobot(x, y, order, exchange), context, result -> + "An exception occurred while invoking constructor of class VersatileRobot"); + + assertEquals(x, instance.getX(), context, result -> "The x-coordinate of this VersatileRobot is incorrect"); + assertEquals(x, instance.getY(), context, result -> "The y-coordinate of this VersatileRobot is incorrect"); + } + + @Test + public void testShuffleWithParams() { + setupEnvironment(MethodHeader.of(VersatileRobot.class, "shuffle", int.class)); + + VersatileRobot instance = callObject(() -> new VersatileRobot(x, y, order, exchange), context, result -> + "An exception occurred while invoking constructor of class VersatileRobot"); + instance.setY(y); + call(() -> instance.shuffle(1), context, result -> "An exception occurred while invoking shuffle(int)"); + + assertEquals(x, instance.getX(), context, result -> "The x-coordinate of this VersatileRobot is incorrect"); + assertEquals(x, instance.getY(), context, result -> "The y-coordinate of this VersatileRobot is incorrect"); + } + + @Test + public void testShuffleNoParams() { + setupEnvironment(MethodHeader.of(VersatileRobot.class, "shuffle")); + + VersatileRobot instance = callObject(() -> new VersatileRobot(x, y, order, exchange), context, result -> + "An exception occurred while invoking constructor of class VersatileRobot"); + instance.setY(y); + call(instance::shuffle, context, result -> "An exception occurred while invoking shuffle()"); + + assertEquals(x, instance.getX(), context, result -> "The x-coordinate of this VersatileRobot is incorrect"); + assertEquals(x, instance.getY(), context, result -> "The y-coordinate of this VersatileRobot is incorrect"); + } +} diff --git a/src/graderPrivate/java/h03/h3_3/RobotsChallengeTest.java b/src/graderPrivate/java/h03/h3_3/RobotsChallengeTest.java new file mode 100644 index 0000000..a6f8931 --- /dev/null +++ b/src/graderPrivate/java/h03/h3_3/RobotsChallengeTest.java @@ -0,0 +1,268 @@ +package h03.h3_3; + +import fopbot.World; +import h03.RobotsChallenge; +import h03.MathMinMock; +import h03.robots.DoublePowerRobot; +import h03.robots.HackingRobot; +import h03.robots.MovementType; +import kotlin.Pair; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.sourcegrade.jagr.api.rubric.TestForSubmission; +import org.tudalgo.algoutils.transform.util.headers.ClassHeader; +import org.tudalgo.algoutils.transform.util.Invocation; +import org.tudalgo.algoutils.transform.util.headers.FieldHeader; +import org.tudalgo.algoutils.transform.util.headers.MethodHeader; +import org.tudalgo.algoutils.tutor.general.assertions.Context; +import org.tudalgo.algoutils.tutor.general.json.JsonParameterSet; +import org.tudalgo.algoutils.tutor.general.json.JsonParameterSetTest; + +import java.lang.reflect.*; +import java.util.*; + +import static org.tudalgo.algoutils.transform.SubmissionExecutionHandler.*; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.*; + +@TestForSubmission +public class RobotsChallengeTest { + + @BeforeAll + public static void setup() { + World.setSize(5, 5); + World.setDelay(0); + } + + @AfterEach + public void tearDown() { + resetAll(); + } + + @Test + public void testClassHeader() { + ClassHeader originalClassHeader = getOriginalClassHeader(RobotsChallenge.class); + + assertTrue(Modifier.isPublic(originalClassHeader.access()), emptyContext(), result -> + "Class RobotsChallenge was not declared public"); + } + + @ParameterizedTest + @ValueSource(ints = {10, 15}) + public void testConstructor(int begin) { + Delegation.disable(MethodHeader.of(RobotsChallenge.class, int.class, int.class, DoublePowerRobot[].class)); + + int goal = 5; + DoublePowerRobot[] robots = new DoublePowerRobot[0]; + Context context = contextBuilder() + .add("begin", begin) + .add("goal", goal) + .add("robots", robots) + .build(); + + Object instance = callObject(() -> new RobotsChallenge(begin, goal, robots), context, result -> + "An exception occurred while invoking constructor of class RobotsChallenge"); + assertEquals(begin / 2, + FieldHeader.of(RobotsChallenge.class, "begin").getValue(instance), + context, + result -> "Value of field 'begin' is incorrect"); + assertEquals(goal, + FieldHeader.of(RobotsChallenge.class, "goal").getValue(instance), + context, + result -> "Value of field 'goal' is incorrect"); + assertSame(robots, + FieldHeader.of(RobotsChallenge.class, "robots").getValue(instance), + context, + result -> "Value of field 'robots' is incorrect"); + } + + @Test + public void testWinThreshold() { + Delegation.disable(MethodHeader.of(RobotsChallenge.class, int.class, int.class, DoublePowerRobot[].class)); + + int begin = 10; + int goal = 5; + DoublePowerRobot[] robots = new DoublePowerRobot[0]; + Context context = contextBuilder() + .add("begin", begin) + .add("goal", goal) + .add("robots", robots) + .build(); + + Object instance = callObject(() -> new RobotsChallenge(begin, goal, robots), context, result -> + "An exception occurred while invoking constructor of class RobotsChallenge"); + assertEquals(2, + FieldHeader.of(RobotsChallenge.class, "winThreshold").getValue(instance), + context, + result -> "Value of field 'winThreshold' is incorrect"); + } + + @ParameterizedTest + @JsonParameterSetTest("/h03/CalculateStepsDiagonalDataSet.generated.json") + public void testCalculateStepsDiagonal(JsonParameterSet params) throws NoSuchMethodException { + testCalculateStepsAllTypes(params, RobotsChallenge.class.getDeclaredMethod("calculateStepsDiagonal")); + } + + @ParameterizedTest + @JsonParameterSetTest("/h03/CalculateStepsOverstepDataSet.generated.json") + public void testCalculateStepsOverstep(JsonParameterSet params) throws NoSuchMethodException { + testCalculateStepsAllTypes(params, RobotsChallenge.class.getDeclaredMethod("calculateStepsOverstep")); + } + + @ParameterizedTest + @JsonParameterSetTest("/h03/CalculateStepsTeleportDataSet.generated.json") + public void testCalculateStepsTeleport(JsonParameterSet params) throws NoSuchMethodException { + testCalculateStepsAllTypes(params, RobotsChallenge.class.getDeclaredMethod("calculateStepsTeleport")); + } + + @Test + public void testFindWinnersCalc() { + MethodHeader calculateSteps = MethodHeader.of(RobotsChallenge.class, "calculateSteps", MovementType.class); + Delegation.disable(MethodHeader.of(RobotsChallenge.class, "findWinners")); + + int begin = 2; + int goal = 5; + MovementType[] movementTypes = MovementType.values(); + for (int i = 0; i < 3; i++) { + final int finalI = i; + Substitution.enable(MethodHeader.of(HackingRobot.class, "getType"), + invocation -> movementTypes[finalI % movementTypes.length]); + Substitution.enable(MethodHeader.of(HackingRobot.class, "getNextType"), + invocation -> movementTypes[(finalI + 1) % movementTypes.length]); + Logging.reset(); + Logging.enable(calculateSteps); + + DoublePowerRobot[] robots = new DoublePowerRobot[] {new DoublePowerRobot(0, 0, false)}; + Context context = contextBuilder() + .add("begin", begin) + .add("goal", goal) + .add("robots", robots) + .build(); + + RobotsChallenge robotsChallengeInstance = new RobotsChallenge(begin * 2, goal, robots); + + call(robotsChallengeInstance::findWinners, context, result -> "An exception occurred while invoking findWinners"); + List invocations = Logging.getInvocations(calculateSteps); + assertEquals(2, invocations.size(), context, result -> "calculateSteps was not called exactly twice"); + assertEquals(List.of(movementTypes[i % movementTypes.length], movementTypes[(i + 1) % movementTypes.length]), + invocations.stream() + .map(invocation -> invocation.getParameter(0, MovementType.class)) + .toList(), + context, + result -> "calculateSteps was not called with .getType() and .getNextType()"); + } + } + + @Test + public void testFindWinnersMin() { + Delegation.disable(MethodHeader.of(RobotsChallenge.class, "findWinners")); + Substitution.enable(MethodHeader.of(RobotsChallenge.class, "calculateSteps", MovementType.class), + invocation -> invocation.getParameter(0, MovementType.class).ordinal()); + + int begin = 2; + int goal = 5; + MovementType[] movementTypes = MovementType.values(); + for (int i = 0; i < 3; i++) { + final int finalI = i; + Substitution.enable(MethodHeader.of(HackingRobot.class, "getType"), + invocation -> movementTypes[finalI % movementTypes.length]); + Substitution.enable(MethodHeader.of(HackingRobot.class, "getNextType"), + invocation -> movementTypes[(finalI + 1) % movementTypes.length]); + + DoublePowerRobot[] robots = new DoublePowerRobot[] {new DoublePowerRobot(0, 0, false)}; + Context context = contextBuilder() + .add("begin", begin) + .add("goal", goal) + .add("robots", robots) + .build(); + RobotsChallenge robotsChallengeInstance = new RobotsChallenge(begin * 2, goal, robots); + + MathMinMock.MIN_INVOCATIONS.clear(); + call(robotsChallengeInstance::findWinners, context, result -> "An exception occurred while invoking findWinners"); + List> minInvocations = new ArrayList<>(MathMinMock.MIN_INVOCATIONS); + assertTrue(!minInvocations.isEmpty(), context, result -> "Math.min was not called at least once"); + Pair expectedArgs = new Pair<>(finalI % movementTypes.length, (finalI + 1) % movementTypes.length); + assertTrue( + minInvocations.stream() + .anyMatch(pair -> pair.getFirst().equals(expectedArgs.getFirst()) && pair.getSecond().equals(expectedArgs.getSecond()) || + pair.getFirst().equals(expectedArgs.getSecond()) && pair.getSecond().equals(expectedArgs.getFirst())), + contextBuilder() + .add(context) + .add("expected", "Math.min(%d, %d) or Math.min(%d, %d)".formatted(expectedArgs.getFirst(), expectedArgs.getSecond(), expectedArgs.getSecond(), expectedArgs.getFirst())) + .build(), + result -> "Math.min was not called with the expected arguments" + ); + } + } + + @Test + public void testFindWinnersReturn() { + MovementType[] movementTypes = MovementType.values(); + DoublePowerRobot[] robots = new DoublePowerRobot[] { + new DoublePowerRobot(0, 0, false), + new DoublePowerRobot(0, 0, false), + new DoublePowerRobot(0, 0, false) + }; + Substitution.enable(MethodHeader.of(HackingRobot.class, "getType"), invocation -> { + if (invocation.getInstance() == robots[0]) { + return movementTypes[0]; + } else if (invocation.getInstance() == robots[1]) { + return movementTypes[1]; + } else if (invocation.getInstance() == robots[2]) { + return movementTypes[2]; + } else { + return null; + } + }); + Substitution.enable(MethodHeader.of(HackingRobot.class, "getNextType"), invocation -> { + if (invocation.getInstance() == robots[0]) { + return movementTypes[1]; + } else if (invocation.getInstance() == robots[1]) { + return movementTypes[2]; + } else if (invocation.getInstance() == robots[2]) { + return movementTypes[0]; + } else { + return null; + } + }); + Substitution.enable(MethodHeader.of(RobotsChallenge.class, "calculateSteps", MovementType.class), + invocation -> invocation.getParameter(0, MovementType.class).ordinal() * 3); + Delegation.disable(MethodHeader.of(RobotsChallenge.class, "findWinners")); + + int begin = 2; + int goal = 5; + Context context = contextBuilder() + .add("begin", begin) + .add("goal", goal) + .add("robots", robots) + .build(); + RobotsChallenge robotsChallengeInstance = new RobotsChallenge(begin * 2, goal, robots); + + DoublePowerRobot[] returnValue = callObject(robotsChallengeInstance::findWinners, context, result -> + "An exception occurred while invoking findWinners"); + assertEquals(robots.length, returnValue.length, context, result -> "Returned array has incorrect length"); + int a = 0; + for (DoublePowerRobot robot : robots) { + if (robot.getType() == MovementType.DIAGONAL || robot.getNextType() == MovementType.DIAGONAL) { + assertSame(robot, returnValue[a++], context, result -> "Robot was not found in array / at wrong index"); + } + } + for (; a < robots.length; a++) { + assertNull(returnValue[a], context, result -> "Found unexpected robots in array"); + } + } + + private void testCalculateStepsAllTypes(JsonParameterSet params, Method method) { + Delegation.disable(method); + + Context context = params.toContext("expected"); + DoublePowerRobot[] robots = new DoublePowerRobot[0]; + RobotsChallenge instance = callObject(() -> new RobotsChallenge(params.getInt("begin"), params.getInt("goal"), robots), + context, result -> "An exception occurred while invoking constructor of class RobotsChallenge"); + + assertCallEquals(params.getInt("expected"), () -> method.invoke(instance), context, result -> + result.cause() == null ? method.getName() + " returned an incorrect value" : result.cause().getCause().getMessage()); + } +} diff --git a/src/graderPrivate/resources/classes/h03/Main.bin b/src/graderPrivate/resources/classes/h03/Main.bin new file mode 100644 index 0000000..1c1fbc5 Binary files /dev/null and b/src/graderPrivate/resources/classes/h03/Main.bin differ diff --git a/src/graderPrivate/resources/classes/h03/RobotsChallenge.bin b/src/graderPrivate/resources/classes/h03/RobotsChallenge.bin new file mode 100644 index 0000000..1c0ea20 Binary files /dev/null and b/src/graderPrivate/resources/classes/h03/RobotsChallenge.bin differ diff --git a/src/graderPrivate/resources/classes/h03/robots/DoublePowerRobot.bin b/src/graderPrivate/resources/classes/h03/robots/DoublePowerRobot.bin new file mode 100644 index 0000000..2d801eb Binary files /dev/null and b/src/graderPrivate/resources/classes/h03/robots/DoublePowerRobot.bin differ diff --git a/src/graderPrivate/resources/classes/h03/robots/HackingRobot.bin b/src/graderPrivate/resources/classes/h03/robots/HackingRobot.bin new file mode 100644 index 0000000..d63aae2 Binary files /dev/null and b/src/graderPrivate/resources/classes/h03/robots/HackingRobot.bin differ diff --git a/src/graderPrivate/resources/classes/h03/robots/MovementType.bin b/src/graderPrivate/resources/classes/h03/robots/MovementType.bin new file mode 100644 index 0000000..3ad31a9 Binary files /dev/null and b/src/graderPrivate/resources/classes/h03/robots/MovementType.bin differ diff --git a/src/graderPrivate/resources/classes/h03/robots/VersatileRobot.bin b/src/graderPrivate/resources/classes/h03/robots/VersatileRobot.bin new file mode 100644 index 0000000..f771ed8 Binary files /dev/null and b/src/graderPrivate/resources/classes/h03/robots/VersatileRobot.bin differ diff --git a/src/graderPrivate/resources/h03/.gitignore b/src/graderPrivate/resources/h03/.gitignore new file mode 100644 index 0000000..f5d73e8 --- /dev/null +++ b/src/graderPrivate/resources/h03/.gitignore @@ -0,0 +1 @@ +*.generated.json diff --git a/src/main/java/h03/Main.java b/src/main/java/h03/Main.java new file mode 100644 index 0000000..9e1cd5c --- /dev/null +++ b/src/main/java/h03/Main.java @@ -0,0 +1,70 @@ +package h03; + +import fopbot.World; +import h03.robots.DoublePowerRobot; +import h03.robots.HackingRobot; +import h03.robots.MovementType; +import h03.robots.VersatileRobot; + +/** + * Main entry point in executing the program. + */ +public class Main { + /** + * Main entry point in executing the program. + * + * @param args program arguments, currently ignored + */ + public static void main(String[] args) { + // Create a 5x5 world and make it visible + World.setSize(5, 5); + World.setVisible(true); + + // Create at least one Hacking Robot with different positions and both cases for the array shift + HackingRobot hackingRobot1 = new HackingRobot(1, 1, true); + HackingRobot hackingRobot2 = new HackingRobot(2, 2, false); + + // Change the type of the Hacking Robot and check the current and next type + hackingRobot1.shuffle(); + System.out.println("HackingRobot1 current type: " + hackingRobot1.getType()); + System.out.println("HackingRobot1 next type: " + hackingRobot1.getNextType()); + + hackingRobot2.shuffle(); + System.out.println("HackingRobot2 current type: " + hackingRobot2.getType()); + System.out.println("HackingRobot2 next type: " + hackingRobot2.getNextType()); + + // Create at least two Versatile Robots with both cases for coordinate exchange + VersatileRobot versatileRobot1 = new VersatileRobot(1, 2, true, false); + VersatileRobot versatileRobot2 = new VersatileRobot(3, 4, false, true); + + // Change the type of the Versatile Robot until the type is DIAGONAL and check coordinates + while (versatileRobot1.getType() != MovementType.DIAGONAL) { + versatileRobot1.shuffle(); + } + System.out.println("VersatileRobot1 type is DIAGONAL. x: " + versatileRobot1.getX() + ", y: " + versatileRobot1.getY()); + + while (versatileRobot2.getType() != MovementType.DIAGONAL) { + versatileRobot2.shuffle(); + } + System.out.println("VersatileRobot2 type is DIAGONAL. x: " + versatileRobot2.getX() + ", y: " + versatileRobot2.getY()); + + // Create at least three Double Power Robots and change their types to get all movement types + DoublePowerRobot doublePowerRobot1 = new DoublePowerRobot(0, 0, true); + DoublePowerRobot doublePowerRobot2 = new DoublePowerRobot(1, 1, false); + DoublePowerRobot doublePowerRobot3 = new DoublePowerRobot(2, 2, true); + + // Create a RobotsChallenge with previously created Double Power Robots + DoublePowerRobot[] robots = {doublePowerRobot1, doublePowerRobot2, doublePowerRobot3}; + RobotsChallenge challenge = new RobotsChallenge(0, 2, robots); + + // Find and display the winning Double Power Robots + DoublePowerRobot[] winners = challenge.findWinners(); + System.out.println("Winning DoublePowerRobots:"); + for (DoublePowerRobot winner : winners) { + if (winner != null) { + //print the winner robot's coordinates + System.out.println("Winner robot coordinates: x: " + winner.getX() + ", y: " + winner.getY()); + } + } + } +} diff --git a/src/main/java/h03/RobotsChallenge.java b/src/main/java/h03/RobotsChallenge.java new file mode 100644 index 0000000..30c4a0a --- /dev/null +++ b/src/main/java/h03/RobotsChallenge.java @@ -0,0 +1,88 @@ +package h03; + +import h03.robots.DoublePowerRobot; +import h03.robots.MovementType; + +/** + * The {@code RobotsChallenge} class performs a challenge between robots of the {@code DoublePowerRobot} class. + */ +public class RobotsChallenge { + + private final DoublePowerRobot[] robots; + private final int goal; + private final int begin; + private final int winThreshold = 2; + + /** + * Constructs a new {@code RobotsChallenge} with the specified starting position, goal, and array of robots. + * + * @param begin The starting position of the robots. + * @param goal The target coordinates. + * @param robots The array of {@code DoublePowerRobot} objects participating in the challenge. + */ + public RobotsChallenge(int begin, int goal, final DoublePowerRobot[] robots) { + this.begin = begin / 2; + this.goal = goal; + this.robots = robots; + } + + /** + * Calculates the number of steps needed for a robot to reach the goal for the diagonal type. + * + * @return The number of steps required to reach the goal. + */ + public int calculateStepsDiagonal() { + return Math.abs(begin - goal); + } + + /** + * Calculates the number of steps needed for a robot to reach the goal for the overstep type. + * + * @return The number of steps required to reach the goal. + */ + public int calculateStepsOverstep() { + return (Math.abs(begin - goal) % 2 == 0) ? Math.abs(begin - goal) : Math.abs(begin - goal) + 1; + } + + /** + * Calculates the number of steps needed for a robot to reach the goal for the teleport type. + * + * @return The number of steps required to reach the goal. + */ + public int calculateStepsTeleport() { + return (Math.abs(begin - goal) % 2 == 0) ? Math.abs(begin - goal) / 2 : (Math.abs(begin - goal) / 2) + 2; + } + + /** + * Calculates the number of steps needed for a robot to reach the goal based on its movement type. + * + * @param type The {@code MovementType} of the robot. + * @return The number of steps required to reach the goal. + */ + public int calculateSteps(MovementType type) { + return type == MovementType.DIAGONAL ? calculateStepsDiagonal() : type == MovementType.OVERSTEP ? calculateStepsOverstep() : calculateStepsTeleport(); + } + + /** + * Finds the winning robots in the challenge based on their movement types and the number of steps required to reach the goal. + * + * @return An array of {@code DoublePowerRobot} objects that are the winners of the challenge. + */ + public DoublePowerRobot[] findWinners() { + int winnerCount = 0; + DoublePowerRobot[] winners = new DoublePowerRobot[robots.length]; + + for (DoublePowerRobot robot : robots) { + int stepsFirstType = calculateSteps(robot.getType()); + int stepsSecondType = calculateSteps(robot.getNextType()); + int steps = Math.min(stepsFirstType, stepsSecondType); + + if (steps <= winThreshold) { + winners[winnerCount] = robot; + winnerCount++; + } + } + + return winners; + } +} diff --git a/src/main/java/h03/robots/DoublePowerRobot.java b/src/main/java/h03/robots/DoublePowerRobot.java new file mode 100644 index 0000000..f67f1fa --- /dev/null +++ b/src/main/java/h03/robots/DoublePowerRobot.java @@ -0,0 +1,57 @@ +package h03.robots; + +/** + * Subclass DoublePowerRobot, which inherits from the {@code HackingRobot} class and allows the robot to have two types simultaneously. + */ +public class DoublePowerRobot extends HackingRobot { + + /** + * Private array doublePowerTypes containing the two types for the DoublePowerRobot. + */ + private MovementType[] doublePowerTypes = new MovementType[2]; + + /** + * Constructor of the DoublePowerRobot class with parameters x, y, and order. + * Initializes the robot and assigns two movement types to the robot. + * + * @param x The x-coordinate of the robot. + * @param y The y-coordinate of the robot. + * @param order If true, the movement types are shifted to the right by one index, otherwise to the left by one index. + */ + public DoublePowerRobot(int x, int y, boolean order) { + super(x, y, order); + + // Assigning the two types to doublePowerTypes + doublePowerTypes[0] = getType(); + doublePowerTypes[1] = getNextType(); + } + + /** + * Overrides the shuffle method of the superclass. + * Shuffles the robot's type a specified number of times and updates the types in doublePowerTypes. + * + * @param itNr The number of iterations to shuffle the type. + * @return True if the types have changed, false otherwise. + */ + @Override + public boolean shuffle(int itNr) { + boolean changed = super.shuffle(itNr); + + // Updating the types in doublePowerTypes based on the new value of type + doublePowerTypes[0] = getType(); + doublePowerTypes[1] = getNextType(); + + return changed; + } + + /** + * Overrides the shuffle method of the superclass. + * Shuffles the robot's type until the type is different from the current type and updates the types in doublePowerTypes. + */ + @Override + public void shuffle() { + super.shuffle(); + doublePowerTypes[0] = getType(); + doublePowerTypes[1] = getNextType(); + } +} diff --git a/src/main/java/h03/robots/HackingRobot.java b/src/main/java/h03/robots/HackingRobot.java new file mode 100644 index 0000000..a947bcd --- /dev/null +++ b/src/main/java/h03/robots/HackingRobot.java @@ -0,0 +1,111 @@ +package h03.robots; + +import fopbot.Robot; +import java.util.Random; + +/** + * The HackingRobot class extends the Robot class and provides additional methods for movement in the grid. + * The robot can have different types of movements which can be shuffled. + */ +public class HackingRobot extends Robot { + + /** + * Private array "robotTypes" containing the elements of the enumeration MovementType in reverse alphabetical order. + */ + private MovementType[] robotTypes = {MovementType.TELEPORT, MovementType.OVERSTEP, MovementType.DIAGONAL}; + + /** + * Private variable that contains the type of the robot. + */ + private MovementType type; + + /** + * Constructs a new HackingRobot at the specified coordinates. + * The order parameter determines the initial order of the movement types. + * + * @param x The x-coordinate of the robot. + * @param y The y-coordinate of the robot. + * @param order If true, the movement types are shifted to the right by one index, otherwise to the left by one index. + */ + public HackingRobot(int x, int y, boolean order) { + super(x, y); + + if (order) { + // Move elements to the right by 1 index + MovementType lastElement = robotTypes[robotTypes.length - 1]; + for (int i = robotTypes.length - 1; i > 0; i--) { + robotTypes[i] = robotTypes[i - 1]; + } + robotTypes[0] = lastElement; + } else { + // Move elements to the left by 1 index + MovementType firstElement = robotTypes[0]; + for (int i = 0; i < robotTypes.length - 1; i++) { + robotTypes[i] = robotTypes[i + 1]; + } + robotTypes[robotTypes.length - 1] = firstElement; + } + + this.type = robotTypes[0]; + } + + /** + * Returns the current type of the robot. + * + * @return The current MovementType of the robot. + */ + public MovementType getType() { + return type; + } + + /** + * Returns the movement type located 1 index to the right of the current type of the robot. + * + * @return The next MovementType of the robot. + */ + public MovementType getNextType() { + int currentIndex = -1; + for (int i = 0; i < robotTypes.length; i++) { + if (robotTypes[i] == type) { + currentIndex = i; + break; + } + } + return robotTypes[(currentIndex + 1) % robotTypes.length]; + } + + /** + * Generates a random number between zero (inclusive) and the specified limit (exclusive). + * + * @param limit The upper bound (exclusive) for the random number. + * @return A random integer between 0 (inclusive) and the specified limit (exclusive). + */ + public int getRandom(int limit) { + Random random = new Random(); + return random.nextInt(limit); + } + + /** + * Randomly changes the type of the robot a specified number of times. + * + * @param itNr The number of iterations to shuffle the type. + * @return True if the type changed after shuffling, false otherwise. + */ + public boolean shuffle(int itNr) { + MovementType previousType = this.type; + for (int i = 0; i < itNr; i++) { + int randomIndex = getRandom(robotTypes.length); + this.type = robotTypes[randomIndex]; + } + + return this.type != previousType; + } + + /** + * Randomly changes the type of the robot until the type is different from the current type. + */ + public void shuffle() { + while (!shuffle(1)) { + } + } +} diff --git a/src/main/java/h03/robots/MovementType.java b/src/main/java/h03/robots/MovementType.java new file mode 100644 index 0000000..e5deff1 --- /dev/null +++ b/src/main/java/h03/robots/MovementType.java @@ -0,0 +1,21 @@ +package h03.robots; + +/** + * The {@code MovementType} enum represents the different types of movements that a robot can perform. + */ +public enum MovementType { + /** + * Represents diagonal movement. + */ + DIAGONAL, + + /** + * Represents overstepping movement. + */ + OVERSTEP, + + /** + * Represents teleportation movement. + */ + TELEPORT +} diff --git a/src/main/java/h03/robots/VersatileRobot.java b/src/main/java/h03/robots/VersatileRobot.java new file mode 100644 index 0000000..5d40897 --- /dev/null +++ b/src/main/java/h03/robots/VersatileRobot.java @@ -0,0 +1,59 @@ +package h03.robots; + +/** + * Subclass VersatileRobot, which inherits from the class {@code HackingRobot}. + * This robot can switch its coordinates and has specific behavior when its type is DIAGONAL. + */ +public class VersatileRobot extends HackingRobot { + + /** + * Constructor of the VersatileRobot class with the parameters x, y, order, and exchange. + * Initializes the robot and optionally exchanges its coordinates. + * + * @param x The x-coordinate of the robot. + * @param y The y-coordinate of the robot. + * @param order If true, the movement types are shifted to the right by one index, otherwise to the left by one index. + * @param exchange If true, the coordinates x and y are exchanged. + */ + public VersatileRobot(int x, int y, boolean order, boolean exchange) { + super(x, y, order); + + if (exchange) { + int aux = x; + setX(y); + setY(aux); + } + + if (getType() == MovementType.DIAGONAL) { + setY(getX()); + } + } + + /** + * Overrides the shuffle method of the superclass. + * Shuffles the robot's type a specified number of times and adjusts the y-coordinate if the type is DIAGONAL. + * + * @param itNr The number of iterations to shuffle the type. + * @return True if the types have changed, false otherwise. + */ + @Override + public boolean shuffle(int itNr) { + boolean changed = super.shuffle(itNr); + if (getType() == MovementType.DIAGONAL) { + setY(getX()); + } + return changed; + } + + /** + * Overrides the shuffle method of the superclass. + * Shuffles the robot's type until the type is different from the current type and adjusts the y-coordinate if the type is DIAGONAL. + */ + @Override + public void shuffle() { + super.shuffle(); + if (getType() == MovementType.DIAGONAL) { + setY(getX()); + } + } +} diff --git a/src/test/java/h03/ExampleJUnitTest.java b/src/test/java/h03/ExampleJUnitTest.java new file mode 100644 index 0000000..8a13962 --- /dev/null +++ b/src/test/java/h03/ExampleJUnitTest.java @@ -0,0 +1,16 @@ +package h03; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * An example JUnit test class. + */ +public class ExampleJUnitTest { + + @Test + public void testAddition() { + assertEquals(2, 1 + 1); + } +} diff --git a/version b/version new file mode 100644 index 0000000..b694fe3 --- /dev/null +++ b/version @@ -0,0 +1 @@ +0.1.0-SNAPSHOT