commit 858fd03c30bd8d5559b7826cd90805097475a862 Author: Oshgnacknak Date: Sat Jan 11 16:41:11 2025 +0100 Squashed 'solution/H06/' content from commit 24f4b42 git-subtree-dir: solution/H06 git-subtree-split: 24f4b42e8e9b4703a06f5d8f48a58125c4eb9682 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..77c2513 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# Musterlösung zu Hausübung 06 + +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..6c339aa --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,37 @@ +plugins { + alias(libs.plugins.algomate) + alias(libs.plugins.style) +} + +version = file("version").readLines().first() + +exercise { + assignmentId.set("h06") +} + +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 +} + +jagr { + graders { + val graderPublic by getting + val graderPrivate by creating { + parent(graderPublic) + graderName.set("FOP-2425-H06-Private") + rubricProviderName.set("h06.H06_RubricProvider") + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..2a961b5 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,3 @@ +[plugins] +algomate = { id = "org.tudalgo.algomate", version = "0.7.1" } +style = { id = "org.sourcegrade.style", version = "3.0.0" } 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..3e14a11 --- /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 = "H06-Root" diff --git a/src/graderPrivate/java/h06/FibonacciTest.java b/src/graderPrivate/java/h06/FibonacciTest.java new file mode 100644 index 0000000..4119e34 --- /dev/null +++ b/src/graderPrivate/java/h06/FibonacciTest.java @@ -0,0 +1,173 @@ +package h06; + +import h06.problems.Fibonacci; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; +import org.sourcegrade.jagr.api.rubric.TestForSubmission; +import org.tudalgo.algoutils.tutor.general.annotation.SkipAfterFirstFailedTest; +import org.tudalgo.algoutils.tutor.general.assertions.Context; +import spoon.reflect.code.CtIf; +import spoon.reflect.code.CtInvocation; +import spoon.reflect.code.CtLoop; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.declaration.CtMethod; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static h06.TestUtils.getCtMethod; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.assertEquals; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.assertFalse; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.assertTrue; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.callObject; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.contextBuilder; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.emptyContext; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.fail; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions4.assertIsNotRecursively; + +@TestForSubmission +@Timeout( + value = TestConstants.TEST_TIMEOUT_IN_SECONDS, + unit = TimeUnit.SECONDS, + threadMode = Timeout.ThreadMode.SEPARATE_THREAD +) +@SkipAfterFirstFailedTest(TestConstants.SKIP_AFTER_FIRST_FAILED_TEST) +public class FibonacciTest { + + private static Method doTheRecursionMethod; + + @BeforeAll + public static void setup() throws ReflectiveOperationException { + doTheRecursionMethod = Fibonacci.class.getDeclaredMethod("doTheRecursion", int.class, int.class, int.class); + doTheRecursionMethod.setAccessible(true); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20}) + public void testDoTheRecursion(int n) { + List> args = new ArrayList<>(); + Answer answer = invocation -> { + if (invocation.getMethod().equals(doTheRecursionMethod)) { + args.add(Map.of( + "a", invocation.getArgument(0), + "b", invocation.getArgument(1), + "n", invocation.getArgument(2) + )); + } + return invocation.callRealMethod(); + }; + Context context = contextBuilder() + .add("a (first invocation)", 0) + .add("b (first invocation)", 1) + .add("n (first invocation)", n) + .build(); + + try (MockedStatic ignored = Mockito.mockStatic(Fibonacci.class, answer)) { + int expected = Fibonacci.fibonacciRecursiveClassic(n); + int actual = (int) callObject(() -> doTheRecursionMethod.invoke(null, 0, 1, n), context, result -> + "An exception occurred while invoking method doTheRecursion"); + assertEquals(expected, actual, context, result -> "Method doTheRecursion returned an incorrect value"); + } + + int a = 0; + int b = 1; + int intermediateN = n; + for (int i = 0; i < n; i++) { + context = contextBuilder() + .add("a", a) + .add("b", b) + .add("n", intermediateN) + .add("recursion depth", i) // i.e. how many recursive calls came before this one + .build(); + if (i + 1 >= args.size()) { + fail(context, result -> "Expected method doTheRecursion to call itself at least one more time (" + n + " times total)"); + } + Map invocation = args.get(i + 1); // skip first invocation, we only want recursive calls + assertEquals(b, invocation.get("a"), context, result -> + "Expected method doTheRecursion to call itself with parameter a = " + result.expected().behavior()); + assertEquals(a + b, invocation.get("b"), context, result -> + "Expected method doTheRecursion to call itself with parameter b = " + result.expected().behavior()); + assertEquals(intermediateN - 1, invocation.get("n"), context, result -> + "Expected method doTheRecursion to call itself with parameter n = " + result.expected().behavior()); + + // Setup for next iteration + int tmpA = a; + int tmpB = b; + a = b; + b = tmpA + tmpB; + intermediateN--; + } + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20}) + public void testFibonacciRecursiveDifferent(int n) { + Context context = contextBuilder() + .add("n", n) + .build(); + int expected = Fibonacci.fibonacciRecursiveClassic(n); + int actual = callObject(() -> Fibonacci.fibonacciRecursiveDifferent(n), context, result -> + "An exception occurred while invoking method fibonacciRecursiveDifferent"); + assertEquals(expected, actual, context, result -> + "Method fibonacciRecursiveDifferent did not return the same value as fibonacciRecursiveClassic"); + } + + @Test + public void testFibonacciRecursiveVAnforderung() { + CtMethod fibonacciRecursiveDifferentCtMethod = getCtMethod(Fibonacci.class, "fibonacciRecursiveDifferent", int.class); + CtMethod doTheRecursionCtMethod = getCtMethod(Fibonacci.class, "doTheRecursion", int.class, int.class, int.class); + + Iterator fibonacciRecursiveDifferentIterator = fibonacciRecursiveDifferentCtMethod.getBody().descendantIterator(); + boolean callsHelperMethod = false; + while (!callsHelperMethod && fibonacciRecursiveDifferentIterator.hasNext()) { + if (fibonacciRecursiveDifferentIterator.next() instanceof CtInvocation ctInvocation) { + callsHelperMethod = ctInvocation.getExecutable().equals(doTheRecursionCtMethod.getReference()); + } + } + assertTrue(callsHelperMethod, emptyContext(), result -> "Method fibonacciRecursiveDifferent does not call doTheRecursion"); + + Iterator doTheRecursionIterator = doTheRecursionCtMethod.getBody().descendantIterator(); + boolean usesLoops = false; + boolean usesIf = false; + while (!usesLoops && !usesIf && doTheRecursionIterator.hasNext()) { + CtElement ctElement = doTheRecursionIterator.next(); + if (ctElement instanceof CtLoop) { + usesLoops = true; + } else if (ctElement instanceof CtIf) { + usesIf = true; + } + } + assertFalse(usesLoops, emptyContext(), result -> "Method doTheRecursion uses loops"); + assertFalse(usesIf, emptyContext(), result -> "Method doTheRecursion uses if statements"); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20}) + public void testFibonacciIterative(int n) { + Context context = contextBuilder() + .add("n", n) + .build(); + int expected = Fibonacci.fibonacciRecursiveClassic(n); + int actual = callObject(() -> Fibonacci.fibonacciIterative(n), context, result -> + "An exception occurred while invoking method fibonacciIterative"); + assertEquals(expected, actual, context, result -> + "Method fibonacciIterative did not return the same value as fibonacciRecursiveClassic"); + } + + @Test + public void testFibonacciIterativeVAnforderung() { + CtMethod fibonacciIterativeCtMethod = getCtMethod(Fibonacci.class, "fibonacciIterative", int.class); + assertIsNotRecursively(fibonacciIterativeCtMethod, emptyContext(), result -> + "Method fibonacciIterative uses recursion"); + } +} diff --git a/src/graderPrivate/java/h06/FractalsTest.java b/src/graderPrivate/java/h06/FractalsTest.java new file mode 100644 index 0000000..6c25843 --- /dev/null +++ b/src/graderPrivate/java/h06/FractalsTest.java @@ -0,0 +1,349 @@ +package h06; + +import h06.problems.Fractals; +import h06.ui.DrawInstruction; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.sourcegrade.jagr.api.rubric.TestForSubmission; +import org.tudalgo.algoutils.tutor.general.annotation.SkipAfterFirstFailedTest; +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 spoon.reflect.code.CtInvocation; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.declaration.CtMethod; + +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static h06.TestUtils.getCtMethod; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.assertEquals; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.assertFalse; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.assertNotSame; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.callObject; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.contextBuilder; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.emptyContext; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions4.assertIsNotIteratively; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions4.assertIsNotRecursively; + +@TestForSubmission +@Timeout( + value = TestConstants.TEST_TIMEOUT_IN_SECONDS, + unit = TimeUnit.SECONDS, + threadMode = Timeout.ThreadMode.SEPARATE_THREAD +) +@SkipAfterFirstFailedTest(TestConstants.SKIP_AFTER_FIRST_FAILED_TEST) +public class FractalsTest { + + private Context concatContext; // say it three times + private DrawInstruction[] arr1; + private DrawInstruction[] arr2; + + private DrawInstruction[] concatenateSetup(JsonParameterSet params) { + concatContext = params.toContext(); + arr1 = toDrawInstructions(params.get("arr1")); + arr2 = toDrawInstructions(params.get("arr2")); + return callObject(() -> Fractals.concatenate(arr1, arr2), concatContext, result -> + "An exception occurred while invoking method concatenate"); + } + + private Context replaceAtIndexContext; + private DrawInstruction[] originalArr; + private DrawInstruction[] arr; + private int idx; + private DrawInstruction elem; + + private DrawInstruction[] replaceAtIndexSetup(JsonParameterSet params) { + replaceAtIndexContext = params.toContext(); + originalArr = toDrawInstructions(params.get("arr")); + arr = toDrawInstructions(params.get("arr")); + idx = params.getInt("idx"); + elem = DrawInstruction.valueOf(params.get("elem")); + + return callObject(() -> Fractals.replaceAtIndex(arr, idx, elem), replaceAtIndexContext, result -> + "An exception occurred while invoking method replaceAtIndex"); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}) + public void testPowExponentZero(int a) { + Context context = contextBuilder() + .add("a", a) + .add("b", 0) + .build(); + int expected = 1; + int actual = callObject(() -> Fractals.pow(a, 0), context, result -> + "An exception occurred while invoking method pow"); + assertEquals(expected, actual, context, result -> "Method pow returned an incorrect value"); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}) + public void testPowExponentPositive(int a) { + Context.Builder contextBuilder = contextBuilder().add("a", a); + for (int b = 1; b <= 5; b++) { + Context context = contextBuilder + .add("b", b) + .build(); + final int finalB = b; + int expected = (int) Math.pow(a, b); + int actual = callObject(() -> Fractals.pow(a, finalB), context, result -> + "An exception occurred while invoking method pow"); + assertEquals(expected, actual, context, result -> "Method pow returned an incorrect value"); + } + } + + @Test + public void testPowVAnforderung() { + assertIsNotIteratively(getCtMethod(Fractals.class, "pow", int.class, int.class), + emptyContext(), + result -> "Method pow is not recursive"); + } + + @ParameterizedTest + @JsonParameterSetTest("FractalsConcatDataSetBoth.generated.json") + public void testConcatenateLength(JsonParameterSet params) { + DrawInstruction[] arrResult = concatenateSetup(params); + assertEquals(arr1.length + arr2.length, arrResult.length, concatContext, result -> + "The length of the array returned by method concatenate is incorrect"); + } + + @ParameterizedTest + @JsonParameterSetTest("FractalsConcatDataSetFirst.generated.json") + public void testConcatenateFirstOnly(JsonParameterSet params) { + DrawInstruction[] arrResult = concatenateSetup(params); + + assertEquals(arr1.length, arrResult.length, concatContext, result -> + "The length of the array returned by method concatenate is incorrect"); + for (int i = 0; i < arr1.length; i++) { + final int finalI = i; + assertEquals(arr1[i], arrResult[i], concatContext, result -> + "Value at index %d of the returned array differs from expected value".formatted(finalI)); + } + } + + @ParameterizedTest + @JsonParameterSetTest("FractalsConcatDataSetSecond.generated.json") + public void testConcatenateSecondOnly(JsonParameterSet params) { + DrawInstruction[] arrResult = concatenateSetup(params); + + assertEquals(arr2.length, arrResult.length, concatContext, result -> + "The length of the array returned by method concatenate is incorrect"); + for (int i = 0; i < arr2.length; i++) { + final int finalI = i; + assertEquals(arr2[i], arrResult[i], concatContext, result -> + "Value at index %d of the returned array differs from expected value".formatted(finalI)); + } + } + + @ParameterizedTest + @JsonParameterSetTest("FractalsConcatDataSetBoth.generated.json") + public void testConcatenateBoth(JsonParameterSet params) { + DrawInstruction[] arrResult = concatenateSetup(params); + + assertEquals(arr1.length + arr2.length, arrResult.length, concatContext, result -> + "The length of the array returned by method concatenate is incorrect"); + for (int i = 0; i < arr1.length; i++) { + final int finalI = i; + assertEquals(arr1[i], arrResult[i], concatContext, result -> + "Value at index %d of the returned array differs from expected value".formatted(finalI)); + } + for (int i = 0; i < arr2.length; i++) { + final int finalI = i; + assertEquals(arr2[i], arrResult[arr1.length + i], concatContext, result -> + "Value at index %d of the returned array differs from expected value".formatted(finalI)); + } + } + + @Test + public void testConcatenateVAnforderung() { + CtMethod concatenateCtMethod = getCtMethod(Fractals.class, "concatenate", DrawInstruction[].class, DrawInstruction[].class); + assertIsNotRecursively(concatenateCtMethod, emptyContext(), result -> "Method concatenate is not iterative"); + + Iterator concatenateIterator = concatenateCtMethod.getBody().descendantIterator(); + boolean calledArraycopy = false; + while (!calledArraycopy && concatenateIterator.hasNext()) { + if (concatenateIterator.next() instanceof CtInvocation ctInvocation) { + calledArraycopy = ctInvocation.toStringDebug().startsWith("java.lang.System.arraycopy("); + } + } + assertFalse(calledArraycopy, emptyContext(), result -> "Method concatenate calls System.arraycopy"); + } + + @ParameterizedTest + @JsonParameterSetTest("FractalsReplaceAtIndexDataSet.generated.json") + public void testReplaceAtIndexLength(JsonParameterSet params) { + DrawInstruction[] arrResult = replaceAtIndexSetup(params); + assertEquals(arr.length, arrResult.length, replaceAtIndexContext, result -> + "The length of the array returned by replaceAtIndex does not equal the length of parameter arr"); + } + + @ParameterizedTest + @JsonParameterSetTest("FractalsReplaceAtIndexDataSet.generated.json") + public void testReplaceAtIndexSameElements(JsonParameterSet params) { + DrawInstruction[] arrResult = replaceAtIndexSetup(params); + for (int i = 0; i < originalArr.length; i++) { + if (i == idx) { + continue; + } + + final int finalI = i; + assertEquals(originalArr[i], arrResult[i], replaceAtIndexContext, result -> + "Value at index %d of the returned array differs from expected value".formatted(finalI)); + } + } + + @ParameterizedTest + @JsonParameterSetTest("FractalsReplaceAtIndexDataSet.generated.json") + public void testReplaceAtIndexReplacedElement(JsonParameterSet params) { + DrawInstruction[] arrResult = replaceAtIndexSetup(params); + if (originalArr.length == 0) { + return; + } + assertEquals(elem, arrResult[idx], replaceAtIndexContext, result -> + "Value at index %d of the returned array differs from expected value".formatted(idx)); + } + + @ParameterizedTest + @JsonParameterSetTest("FractalsReplaceAtIndexDataSet.generated.json") + public void testReplaceAtIndexNewArray(JsonParameterSet params) { + DrawInstruction[] arrResult = replaceAtIndexSetup(params); + assertNotSame(arr, arrResult, replaceAtIndexContext, result -> + "Method replaceAtIndex did not return a new array"); + for (int i = 0; i < originalArr.length; i++) { + final int finalI = i; + assertEquals(originalArr[i], arr[i], replaceAtIndexContext, result -> + "Input array arr was modified at index " + finalI); + } + } + + @Test + public void testReplaceAtIndexVAnforderung() { + CtMethod ctMethod = getCtMethod(Fractals.class, "replaceAtIndex", DrawInstruction[].class, int.class, DrawInstruction.class); + assertIsNotRecursively(ctMethod, emptyContext(), result -> "Method replaceAtIndex is not iterative"); + } + + @ParameterizedTest + @ValueSource(ints = {0, -1, -5, -10}) + public void testDragonCurveNonPositive(int n) { + testDragonCurve(n, new DrawInstruction[] {DrawInstruction.DRAW_LINE}); + } + + @Test + public void testDragonCurveOne() { + testDragonCurve(1, new DrawInstruction[] {DrawInstruction.DRAW_LINE, DrawInstruction.TURN_RIGHT, DrawInstruction.DRAW_LINE}); + } + + @ParameterizedTest + @JsonParameterSetTest("FractalsDragonCurveDataSet.generated.json") + public void testDragonCurveN(JsonParameterSet params) { + int n = params.getInt("n"); + DrawInstruction[] expected = toDrawInstructions(params.get("expected")); + testDragonCurve(n, expected); + } + + @Test + public void testDragonCurveVAnforderung() { + CtMethod ctMethod = getCtMethod(Fractals.class, "dragonCurve", int.class); + assertIsNotIteratively(ctMethod, emptyContext(), result -> "Method dragonCurve is not recursive"); + } + + @ParameterizedTest + @ValueSource(ints = {0, -1, -5, -10}) + public void testKochSnowflakeNonPositive(int n) { + testKochSnowflake(n, new DrawInstruction[] { + DrawInstruction.DRAW_LINE, + DrawInstruction.TURN_RIGHT, + DrawInstruction.TURN_RIGHT, + DrawInstruction.DRAW_LINE, + DrawInstruction.TURN_RIGHT, + DrawInstruction.TURN_RIGHT, + DrawInstruction.DRAW_LINE, + }); + } + + @Test + public void testKochSnowflakeOne() { + testKochSnowflake(1, new DrawInstruction[] { + DrawInstruction.DRAW_LINE, + DrawInstruction.TURN_LEFT, + DrawInstruction.DRAW_LINE, + DrawInstruction.TURN_RIGHT, + DrawInstruction.TURN_RIGHT, + DrawInstruction.DRAW_LINE, + DrawInstruction.TURN_LEFT, + DrawInstruction.DRAW_LINE, + DrawInstruction.TURN_RIGHT, + DrawInstruction.TURN_RIGHT, + DrawInstruction.DRAW_LINE, + DrawInstruction.TURN_LEFT, + DrawInstruction.DRAW_LINE, + DrawInstruction.TURN_RIGHT, + DrawInstruction.TURN_RIGHT, + DrawInstruction.DRAW_LINE, + DrawInstruction.TURN_LEFT, + DrawInstruction.DRAW_LINE, + DrawInstruction.TURN_RIGHT, + DrawInstruction.TURN_RIGHT, + DrawInstruction.DRAW_LINE, + DrawInstruction.TURN_LEFT, + DrawInstruction.DRAW_LINE, + DrawInstruction.TURN_RIGHT, + DrawInstruction.TURN_RIGHT, + DrawInstruction.DRAW_LINE, + DrawInstruction.TURN_LEFT, + DrawInstruction.DRAW_LINE + }); + } + + @ParameterizedTest + @JsonParameterSetTest("FractalsKochSnowflakeDataSet.generated.json") + public void testKochSnowflakeN(JsonParameterSet params) { + int n = params.getInt("n"); + DrawInstruction[] expected = toDrawInstructions(params.get("expected")); + testKochSnowflake(n, expected); + } + + private void testDragonCurve(int n, DrawInstruction[] expected) { + Context context = contextBuilder() + .add("n", n) + .build(); + DrawInstruction[] actual = callObject(() -> Fractals.dragonCurve(n), context, result -> + "An exception occurred while invoking method dragonCurve"); + + assertEquals(expected.length, actual.length, context, result -> + "The length of the returned array differs from expected value"); + for (int i = 0; i < expected.length; i++) { + final int finalI = i; + assertEquals(expected[i], actual[i], context, result -> + "Value at index %d of the returned array differs from expected value".formatted(finalI)); + } + } + + private void testKochSnowflake(int n, DrawInstruction[] expected) { + Context context = contextBuilder() + .add("n", n) + .build(); + DrawInstruction[] actual = callObject(() -> Fractals.kochSnowflake(n), context, result -> + "An exception occurred while invoking method kochSnowflake"); + + assertEquals(expected.length, actual.length, context, result -> + "The length of the returned array differs from expected value"); + for (int i = 0; i < expected.length; i++) { + final int finalI = i; + assertEquals(expected[i], actual[i], context, result -> + "Value at index %d of the returned array differs from expected value".formatted(finalI)); + } + } + + private static DrawInstruction[] toDrawInstructions(List drawInstructions) { + DrawInstruction[] instructions = new DrawInstruction[drawInstructions.size()]; + for (int i = 0; i < instructions.length; i++) { + instructions[i] = DrawInstruction.valueOf(drawInstructions.get(i)); + } + return instructions; + } +} diff --git a/src/graderPrivate/java/h06/H06_RubricProvider.java b/src/graderPrivate/java/h06/H06_RubricProvider.java new file mode 100644 index 0000000..3b71954 --- /dev/null +++ b/src/graderPrivate/java/h06/H06_RubricProvider.java @@ -0,0 +1,285 @@ +package h06; + +import org.sourcegrade.jagr.api.rubric.Criterion; +import org.sourcegrade.jagr.api.rubric.JUnitTestRef; +import org.sourcegrade.jagr.api.rubric.Rubric; +import org.sourcegrade.jagr.api.rubric.RubricProvider; +import org.tudalgo.algoutils.tutor.general.json.JsonParameterSet; + +import static org.tudalgo.algoutils.tutor.general.jagr.RubricUtils.criterion; + +public class H06_RubricProvider implements RubricProvider { + + private static final Criterion H6_1_1 = Criterion.builder() + .shortDescription("H6.1.1 | Fibonacci rekursiv") + .minPoints(0) + .maxPoints(2) + .addChildCriteria( + criterion( + "Die Hilfsmethode doTheRecursion wurde korrekt implementiert.", + JUnitTestRef.ofMethod(() -> FibonacciTest.class.getDeclaredMethod("testDoTheRecursion", int.class)) + ), + criterion( + "Die Methode fibonacciRecursiveDifferent ruft die Hilfsmethode auf und gibt die selben Werte wie fibonacciRecursiveClassic zurück.", + JUnitTestRef.ofMethod(() -> FibonacciTest.class.getDeclaredMethod("testFibonacciRecursiveDifferent", int.class)) + ), + criterion( + "Verbindliche Anforderung nicht erfüllt", + -2, + JUnitTestRef.ofMethod(() -> FibonacciTest.class.getDeclaredMethod("testFibonacciRecursiveVAnforderung")) + ) // Punktabzug wenn nicht erfüllt + ) + .build(); + + private static final Criterion H6_1_2 = Criterion.builder() + .shortDescription("H6.1.2 | Fibonacci iterativ") + .minPoints(0) + .maxPoints(3) + .addChildCriteria( + criterion( + "Die Methode fibonacciIterative gibt die selben Werte wie fibonacciRecursiveClassic zurück.", + 3, + JUnitTestRef.ofMethod(() -> FibonacciTest.class.getDeclaredMethod("testFibonacciIterative", int.class)) + ), + criterion( + "Verbindliche Anforderung nicht erfüllt", + -3, + JUnitTestRef.ofMethod(() -> FibonacciTest.class.getDeclaredMethod("testFibonacciIterativeVAnforderung")) + ) // Punktabzug wenn nicht erfüllt + ) + .build(); + + private static final Criterion H6_1 = Criterion.builder() + .shortDescription("H6.1 | Fibonacci Zahlenfolge") + .addChildCriteria( + H6_1_1, + H6_1_2 + ) + .build(); + + private static final Criterion H6_2_1 = Criterion.builder() + .shortDescription("H6.2.1 | Lineare Suche rekursiv") + .minPoints(0) + .maxPoints(4) + .addChildCriteria( + criterion( + "Die Methode linearSearchRecursive gibt den korrekten Index zurück.", + JUnitTestRef.ofMethod(() -> LinearSearchTest.class.getDeclaredMethod("testLinearSearchRecursive", JsonParameterSet.class)) + ), + criterion( + "Die Hilfsmethode wird mit den korrekten Parametern aufgerufen.", + JUnitTestRef.ofMethod(() -> LinearSearchTest.class.getDeclaredMethod("testLinearSearchRecursiveHelperCall", JsonParameterSet.class)) + ), + criterion( + "Die Methode linearSearchRecursiveHelper gibt den korrekten Index zurück.", + JUnitTestRef.ofMethod(() -> LinearSearchTest.class.getDeclaredMethod("testLinearSearchRecursiveHelper", JsonParameterSet.class)) + ), + criterion( + "Es wird -1 zurückgegeben, wenn das Element nicht gefunden wird.", + JUnitTestRef.ofMethod(() -> LinearSearchTest.class.getDeclaredMethod("testLinearSearchRecursiveHelperTargetNotInArray", JsonParameterSet.class)) + ), + criterion( + "Verbindliche Anforderung nicht erfüllt", + -4, + JUnitTestRef.ofMethod(() -> LinearSearchTest.class.getDeclaredMethod("testLinearSearchRecursiveHelperVAnforderung")) + ) // Punktabzug wenn nicht erfüllt + ) + .build(); + + private static final Criterion H6_2_2 = Criterion.builder() + .shortDescription("H6.2.2 | Lineare Suche iterativ") + .minPoints(0) + .maxPoints(3) + .addChildCriteria( + criterion( + "Die Methode linearSearchIterative gibt den korrekten Index zurück.", + 2, + JUnitTestRef.ofMethod(() -> LinearSearchTest.class.getDeclaredMethod("testLinearSearchIterative", JsonParameterSet.class)) + ), + criterion( + "Es wird -1 zurückgegeben, wenn das Element nicht gefunden wird.", + JUnitTestRef.ofMethod(() -> LinearSearchTest.class.getDeclaredMethod("testLinearSearchIterativeTargetNotInArray", JsonParameterSet.class)) + ), + criterion( + "Verbindliche Anforderung nicht erfüllt", + -3, + JUnitTestRef.ofMethod(() -> LinearSearchTest.class.getDeclaredMethod("testLinearSearchIterativeVAnforderung")) + ) // Punktabzug wenn nicht erfüllt + ) + .build(); + + private static final Criterion H6_2 = Criterion.builder() + .shortDescription("H6.2 | Lineare Suche") + .addChildCriteria( + H6_2_1, + H6_2_2 + ) + .build(); + + private static final Criterion H6_3_1 = Criterion.builder() + .shortDescription("H6.3.1 | Funktion pow") + .minPoints(0) + .maxPoints(2) + .addChildCriteria( + criterion( + "Die Methode pow gibt das korrekte Ergebnis für b = 0 zurück.", + JUnitTestRef.ofMethod(() -> FractalsTest.class.getDeclaredMethod("testPowExponentZero", int.class)) + ), + criterion( + "Die Methode pow gibt das korrekte Ergebnis für b > 0 zurück.", + JUnitTestRef.ofMethod(() -> FractalsTest.class.getDeclaredMethod("testPowExponentPositive", int.class)) + ), + criterion( + "Verbindliche Anforderung nicht erfüllt", + -2, + JUnitTestRef.ofMethod(() -> FractalsTest.class.getDeclaredMethod("testPowVAnforderung")) + ) // Punktabzug wenn nicht erfüllt + ) + .build(); + + private static final Criterion H6_3_2 = Criterion.builder() + .shortDescription("H6.3.2 | Funktion concatenate") + .minPoints(0) + .maxPoints(4) + .addChildCriteria( + criterion( + "Die Länge des Ergebnisarrays ist die Summe der Längen der beiden Eingabearrays.", + JUnitTestRef.ofMethod(() -> FractalsTest.class.getDeclaredMethod("testConcatenateLength", JsonParameterSet.class)) + ), + criterion( + "Das resultierende Array enthält nur die Elemente von arr1, wenn arr2.length == 0 ist.", + JUnitTestRef.ofMethod(() -> FractalsTest.class.getDeclaredMethod("testConcatenateFirstOnly", JsonParameterSet.class)) + ), + criterion( + "Das resultierende Array enthält nur die Elemente von arr2, wenn arr1.length == 0 ist.", + JUnitTestRef.ofMethod(() -> FractalsTest.class.getDeclaredMethod("testConcatenateSecondOnly", JsonParameterSet.class)) + ), + criterion( + "Das resultierende Array enthält alle Elemente von arr1 gefolgt von allen Elementen von arr2, wenn beide Arrays Elemente enthalten.", + JUnitTestRef.ofMethod(() -> FractalsTest.class.getDeclaredMethod("testConcatenateBoth", JsonParameterSet.class)) + ), + criterion( + "Verbindliche Anforderung nicht erfüllt", + -4, + JUnitTestRef.ofMethod(() -> FractalsTest.class.getDeclaredMethod("testConcatenateVAnforderung")) + ) // Punktabzug wenn nicht erfüllt + ) + .build(); + + private static final Criterion H6_3_3 = Criterion.builder() + .shortDescription("H6.3.3 | Funktion replaceAtIndex") + .minPoints(0) + .maxPoints(4) + .addChildCriteria( + criterion( + "Die Länge des Ergebnisarrays ist die gleiche wie die des Eingabearrays.", + JUnitTestRef.ofMethod(() -> FractalsTest.class.getDeclaredMethod("testReplaceAtIndexLength", JsonParameterSet.class)) + ), + criterion( + "Das Ergebnisarray enthält bis auf das Element an der angegebenen Stelle die gleichen Elemente wie das Eingabearray.", + JUnitTestRef.ofMethod(() -> FractalsTest.class.getDeclaredMethod("testReplaceAtIndexSameElements", JsonParameterSet.class)) + ), + criterion( + "Das Ergebnisarray enthält das neue Element an der angegebenen Stelle.", + JUnitTestRef.ofMethod(() -> FractalsTest.class.getDeclaredMethod("testReplaceAtIndexReplacedElement", JsonParameterSet.class)) + ), + criterion( + "Es wird ein neues Array zurückgegeben, das das Eingabearray nicht verändert.", + JUnitTestRef.ofMethod(() -> FractalsTest.class.getDeclaredMethod("testReplaceAtIndexNewArray", JsonParameterSet.class)) + ), + criterion( + "Verbindliche Anforderung nicht erfüllt", + -4, + JUnitTestRef.ofMethod(() -> FractalsTest.class.getDeclaredMethod("testReplaceAtIndexVAnforderung")) + ) // Punktabzug wenn nicht erfüllt + ) + .build(); + + private static final Criterion H6_3_4 = Criterion.builder() + .shortDescription("H6.3.4 | Drachenkurve") + .minPoints(0) + .maxPoints(4) + .addChildCriteria( + criterion( + "Die Methode dragonCurve gibt für n <= 0 das korrekte Ergebnis zurück.", + JUnitTestRef.ofMethod(() -> FractalsTest.class.getDeclaredMethod("testDragonCurveNonPositive", int.class)) + ), + criterion( + "Die Methode dragonCurve gibt für n == 1 das korrekte Ergebnis zurück.", + JUnitTestRef.ofMethod(() -> FractalsTest.class.getDeclaredMethod("testDragonCurveOne")) + ), + criterion( + "Die Methode dragonCurve gibt für n > 1 das korrekte Ergebnis zurück.", + 2, + JUnitTestRef.ofMethod(() -> FractalsTest.class.getDeclaredMethod("testDragonCurveN", JsonParameterSet.class)) + ), + criterion( + "Verbindliche Anforderung nicht erfüllt", + -4, + JUnitTestRef.ofMethod(() -> FractalsTest.class.getDeclaredMethod("testDragonCurveVAnforderung")) + ) // Punktabzug wenn nicht erfüllt + ) + .build(); + + private static final Criterion H6_3_5 = Criterion.builder() + .shortDescription("H6.3.5 | Koch-Schneeflocke") + .minPoints(0) + .maxPoints(5) + .addChildCriteria( + criterion( + "Die Methode kochSnowflake gibt für n <= 0 das korrekte Ergebnis zurück.", + JUnitTestRef.ofMethod(() -> FractalsTest.class.getDeclaredMethod("testKochSnowflakeNonPositive", int.class)) + ), + criterion( + "Die Methode kochSnowflake gibt für n == 1 das korrekte Ergebnis zurück.", + JUnitTestRef.ofMethod(() -> FractalsTest.class.getDeclaredMethod("testKochSnowflakeOne")) + ), + criterion( + "Die Methode kochSnowflake gibt für n > 1 das korrekte Ergebnis zurück.", + 3, + JUnitTestRef.ofMethod(() -> FractalsTest.class.getDeclaredMethod("testKochSnowflakeN", JsonParameterSet.class)) + ) + ) + .build(); + + private static final Criterion H6_3_6 = Criterion.builder() + .shortDescription("H6.3.6 | Visualisieren der Fraktale") + .minPoints(0) + .maxPoints(1) + .addChildCriteria( + criterion( + "Ein Objekt vom Typ FractalVisualizer wird erstellt und bekommt die korrekten Parameter übergeben.", + JUnitTestRef.or( + JUnitTestRef.ofMethod(() -> MainTest.class.getDeclaredMethod("testMainDragonCurve")), + JUnitTestRef.ofMethod(() -> MainTest.class.getDeclaredMethod("testMainKochSnowflake")) + ) + ) + ) + .build(); + + private static final Criterion H6_3 = Criterion.builder() + .shortDescription("H6.3 | Fraktale") + .addChildCriteria( + H6_3_1, + H6_3_2, + H6_3_3, + H6_3_4, + H6_3_5, + H6_3_6 + ) + .build(); + + public static final Rubric RUBRIC = Rubric.builder() + .title("H06 | Racket zu Java: Rekursive Algorithmen für Fibonacci, Lineare Suche und Fraktale") + .addChildCriteria( + H6_1, + H6_2, + H6_3 + ) + .build(); + + @Override + public Rubric getRubric() { + return RUBRIC; + } +} diff --git a/src/graderPrivate/java/h06/LinearSearchTest.java b/src/graderPrivate/java/h06/LinearSearchTest.java new file mode 100644 index 0000000..a711bd1 --- /dev/null +++ b/src/graderPrivate/java/h06/LinearSearchTest.java @@ -0,0 +1,159 @@ +package h06; + +import h06.problems.LinearSearch; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; +import org.sourcegrade.jagr.api.rubric.TestForSubmission; +import org.tudalgo.algoutils.tutor.general.annotation.SkipAfterFirstFailedTest; +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.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static h06.TestUtils.getCtMethod; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.assertEquals; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.assertSame; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.call; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.callObject; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.emptyContext; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions4.assertIsNotIteratively; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions4.assertIsNotRecursively; + +@TestForSubmission +@Timeout( + value = TestConstants.TEST_TIMEOUT_IN_SECONDS, + unit = TimeUnit.SECONDS, + threadMode = Timeout.ThreadMode.SEPARATE_THREAD +) +@SkipAfterFirstFailedTest(TestConstants.SKIP_AFTER_FIRST_FAILED_TEST) +public class LinearSearchTest { + + private int[] arr; + private int target; + private int expectedIndex; + private Context baseContext; + + public void setup(JsonParameterSet params) { + List arrParam = params.get("arr"); + arr = new int[arrParam.size()]; + for (int i = 0; i < arrParam.size(); i++) { + arr[i] = arrParam.get(i); + } + target = params.get("target"); + expectedIndex = params.get("expectedIndex"); + baseContext = params.toContext("expectedIndex"); + } + + @ParameterizedTest + @JsonParameterSetTest("LinearSearchDataSet.generated.json") + public void testLinearSearchRecursive(JsonParameterSet params) { + setup(params); + int actual = callObject(() -> LinearSearch.linearSearchRecursive(arr, target), baseContext, result -> + "An exception occurred while invoking method linearSearchRecursive"); + assertEquals(expectedIndex, actual, baseContext, result -> "Method linearSearchRecursive returned an incorrect value"); + } + + @ParameterizedTest + @JsonParameterSetTest("LinearSearchDataSet.generated.json") + public void testLinearSearchRecursiveHelperCall(JsonParameterSet params) throws ReflectiveOperationException { + setup(params); + Method linearSearchRecursiveHelperMethod = LinearSearch.class.getDeclaredMethod("linearSearchRecursiveHelper", int[].class, int.class, int.class); + List> args = new ArrayList<>(); + Answer answer = invocation -> { + if (invocation.getMethod().equals(linearSearchRecursiveHelperMethod)) { + args.add(Map.of( + "arr", invocation.getArgument(0), + "target", invocation.getArgument(1), + "index", invocation.getArgument(2) + )); + return -1; + } else { + return invocation.callRealMethod(); + } + }; + + try (MockedStatic ignored = Mockito.mockStatic(LinearSearch.class, answer)) { + call(() -> LinearSearch.linearSearchRecursive(arr, target), baseContext, result -> + "An exception occurred while invoking method linearSearchRecursive"); + } + + assertEquals(1, args.size(), baseContext, result -> + "Method linearSearchRecursive did not invoke linearSearchRecursiveHelper exactly once"); + Map invocation = args.get(0); + assertSame(arr, invocation.get("arr"), baseContext, result -> + "Method linearSearchRecursive did not invoke linearSearchRecursiveHelper with parameter arr"); + assertEquals(target, invocation.get("target"), baseContext, result -> + "Method linearSearchRecursive did not invoke linearSearchRecursiveHelper with parameter target"); + assertEquals(0, invocation.get("index"), baseContext, result -> + "Method linearSearchRecursive did not invoke linearSearchRecursiveHelper with index = 0"); + } + + @ParameterizedTest + @JsonParameterSetTest("LinearSearchDataSet.generated.json") + public void testLinearSearchRecursiveHelper(JsonParameterSet params) { + setup(params); + int actual = callObject(() -> LinearSearch.linearSearchRecursiveHelper(arr, target, 0), baseContext, result -> + "An exception occurred while invoking method linearSearchRecursiveHelper"); + assertEquals(expectedIndex, actual, baseContext, result -> "Method linearSearchRecursiveHelper returned an incorrect value"); + } + + @ParameterizedTest + @JsonParameterSetTest("LinearSearchDataSet.generated.json") + public void testLinearSearchRecursiveHelperTargetNotInArray(JsonParameterSet params) { + if (params.getInt("expectedIndex") != -1) { + System.out.println("Test ignored. Value target exists in arr"); + return; + } + + setup(params); + int actual = callObject(() -> LinearSearch.linearSearchRecursiveHelper(arr, target, 0), baseContext, result -> + "An exception occurred while invoking method linearSearchRecursiveHelper"); + assertEquals(-1, actual, baseContext, result -> "Method linearSearchRecursiveHelper returned an incorrect value"); + } + + @Test + public void testLinearSearchRecursiveHelperVAnforderung() { + assertIsNotIteratively(getCtMethod(LinearSearch.class, "linearSearchRecursiveHelper", int[].class, int.class, int.class), + emptyContext(), + result -> "Method linearSearchRecursiveHelper is not recursive"); + } + + @ParameterizedTest + @JsonParameterSetTest("LinearSearchDataSet.generated.json") + public void testLinearSearchIterative(JsonParameterSet params) { + setup(params); + int actual = callObject(() -> LinearSearch.linearSearchIterative(arr, target), baseContext, result -> + "An exception occurred while invoking method linearSearchIterative"); + assertEquals(expectedIndex, actual, baseContext, result -> "Method linearSearchIterative returned an incorrect value"); + } + + @ParameterizedTest + @JsonParameterSetTest("LinearSearchDataSet.generated.json") + public void testLinearSearchIterativeTargetNotInArray(JsonParameterSet params) { + if (params.getInt("expectedIndex") != -1) { + System.out.println("Test ignored. Value target exists in arr"); + return; + } + + setup(params); + int actual = callObject(() -> LinearSearch.linearSearchIterative(arr, target), baseContext, result -> + "An exception occurred while invoking method linearSearchIterative"); + assertEquals(-1, actual, baseContext, result -> "Method linearSearchIterative returned an incorrect value"); + } + + @Test + public void testLinearSearchIterativeVAnforderung() { + assertIsNotRecursively(getCtMethod(LinearSearch.class, "linearSearchIterative", int[].class, int.class), + emptyContext(), + result -> "Method linearSearchIterative is not recursive"); + } +} diff --git a/src/graderPrivate/java/h06/MainTest.java b/src/graderPrivate/java/h06/MainTest.java new file mode 100644 index 0000000..b602443 --- /dev/null +++ b/src/graderPrivate/java/h06/MainTest.java @@ -0,0 +1,94 @@ +package h06; + +import h06.problems.Fractals; +import h06.ui.DrawInstruction; +import h06.ui.FractalVisualizer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.mockito.MockedConstruction; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; +import org.sourcegrade.jagr.api.rubric.TestForSubmission; +import org.tudalgo.algoutils.tutor.general.annotation.SkipAfterFirstFailedTest; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.assertTrue; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.emptyContext; + +@TestForSubmission +@Timeout( + value = TestConstants.TEST_TIMEOUT_IN_SECONDS, + unit = TimeUnit.SECONDS, + threadMode = Timeout.ThreadMode.SEPARATE_THREAD +) +@SkipAfterFirstFailedTest(TestConstants.SKIP_AFTER_FIRST_FAILED_TEST) +public class MainTest { + + private List dragonCurveReturnValues; + private List kochSnowflakeReturnValues; + private Answer fractalsAnswer; + private List> constructorArgs; + private MockedConstruction.MockInitializer initializer; + + @BeforeEach + public void setup() throws ReflectiveOperationException { + dragonCurveReturnValues = new ArrayList<>(); + kochSnowflakeReturnValues = new ArrayList<>(); + Method dragonCurveMethod = Fractals.class.getDeclaredMethod("dragonCurve", int.class); + Method kochSnowflakeMethod = Fractals.class.getDeclaredMethod("kochSnowflake", int.class); + fractalsAnswer = invocation -> { + if (invocation.getMethod().equals(dragonCurveMethod)) { + DrawInstruction[] drawInstructions = new DrawInstruction[0]; + dragonCurveReturnValues.add(drawInstructions); + return drawInstructions; + } else if (invocation.getMethod().equals(kochSnowflakeMethod)) { + DrawInstruction[] drawInstructions = new DrawInstruction[0]; + kochSnowflakeReturnValues.add(drawInstructions); + return drawInstructions; + } else { + return invocation.callRealMethod(); + } + }; + + constructorArgs = new ArrayList<>(); + initializer = (mock, context) -> { + constructorArgs.add(context.arguments()); + }; + } + + @Test + public void testMainDragonCurve() { + try (MockedStatic fractalsMock = Mockito.mockStatic(Fractals.class, fractalsAnswer); + MockedConstruction visualizerMock = Mockito.mockConstruction(FractalVisualizer.class, initializer)) { + Main.main(new String[0]); + } + + boolean calledConstructorWithDragonCurveParams = constructorArgs.stream() + .filter(list -> list.get(1).equals(90)) + .map(list -> list.get(0)) + .anyMatch(dragonCurveReturnValues::contains); + assertTrue(calledConstructorWithDragonCurveParams, emptyContext(), result -> + "Constructor of FractalVisualizer was not called with args (, 90)"); + } + + @Test + public void testMainKochSnowflake() throws ReflectiveOperationException { + try (MockedStatic fractalsMock = Mockito.mockStatic(Fractals.class, fractalsAnswer); + MockedConstruction visualizerMock = Mockito.mockConstruction(FractalVisualizer.class, initializer)) { + Main.main(new String[0]); + } + + boolean calledConstructorWithKochSnowflakeParams = constructorArgs.stream() + .filter(list -> list.get(1).equals(60)) + .map(list -> list.get(0)) + .anyMatch(kochSnowflakeReturnValues::contains); + assertTrue(calledConstructorWithKochSnowflakeParams, emptyContext(), result -> + "Constructor of FractalVisualizer was not called with args (, 60)"); + } +} diff --git a/src/graderPrivate/java/h06/TestConstants.java b/src/graderPrivate/java/h06/TestConstants.java new file mode 100644 index 0000000..2700fb5 --- /dev/null +++ b/src/graderPrivate/java/h06/TestConstants.java @@ -0,0 +1,13 @@ +package h06; + +import java.util.concurrent.ThreadLocalRandom; + +public class TestConstants { + public static long RANDOM_SEED = ThreadLocalRandom.current().nextLong(); + + public static final int TEST_TIMEOUT_IN_SECONDS = 30; + + public static final int TEST_ITERATIONS = 30; + + public static final boolean SKIP_AFTER_FIRST_FAILED_TEST = true; +} diff --git a/src/graderPrivate/java/h06/TestJsonGenerators.java b/src/graderPrivate/java/h06/TestJsonGenerators.java new file mode 100644 index 0000000..42bc2cc --- /dev/null +++ b/src/graderPrivate/java/h06/TestJsonGenerators.java @@ -0,0 +1,169 @@ +package h06; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import h06.problems.Fractals; +import h06.ui.DrawInstruction; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIf; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiFunction; +import java.util.stream.Collectors; + +import static h06.TestConstants.TEST_ITERATIONS; + +@DisabledIf("org.tudalgo.algoutils.tutor.general.Utils#isJagrRun()") +public class TestJsonGenerators { + + @Test + public void generateLinearSearchDataSet() throws IOException { + TestUtils.generateJsonTestData( + (mapper, index, rnd) -> { + boolean containsTarget = rnd.nextBoolean(); + int target = rnd.nextInt(); + List arr = rnd.ints() + .distinct() + .filter(i -> i != target) + .limit(rnd.nextInt(10, 20)) + .boxed() + .collect(Collectors.toList()); + int expectedIndex; + if (containsTarget) { + expectedIndex = rnd.nextInt(arr.size()); + arr.set(expectedIndex, target); + } else { + expectedIndex = -1; + } + + ArrayNode arrayNode = mapper.createArrayNode(); + arr.forEach(arrayNode::add); + ObjectNode objectNode = mapper.createObjectNode(); + objectNode.put("target", target); + objectNode.set("arr", arrayNode); + objectNode.put("expectedIndex", expectedIndex); + + return objectNode; + }, + TEST_ITERATIONS, + "LinearSearchDataSet" + ); + } + + @ParameterizedTest + @ValueSource(strings = {"First", "Second", "Both"}) + public void generateFractalsConcatDataSet(String concatType) throws IOException { + DrawInstruction[] drawInstructions = DrawInstruction.values(); + BiFunction arr1Length = (s, rnd) -> s.equals("Second") ? 0 : rnd.nextInt(1, 10); + BiFunction arr2Length = (s, rnd) -> s.equals("First") ? 0 : rnd.nextInt(1, 10); + + TestUtils.generateJsonTestData( + (mapper, index, rnd) -> { + int arr1Size = arr1Length.apply(concatType, rnd); + List arr1 = new ArrayList<>(); + for (int i = 0; i < arr1Size; i++) { + arr1.add(drawInstructions[rnd.nextInt(drawInstructions.length)].name()); + } + int arr2Size = arr2Length.apply(concatType, rnd); + List arr2 = new ArrayList<>(); + for (int i = 0; i < arr2Size; i++) { + arr2.add(drawInstructions[rnd.nextInt(drawInstructions.length)].name()); + } + + ArrayNode arr1ArrayNode = mapper.createArrayNode(); + arr1.forEach(arr1ArrayNode::add); + ArrayNode arr2ArrayNode = mapper.createArrayNode(); + arr2.forEach(arr2ArrayNode::add); + ObjectNode objectNode = mapper.createObjectNode(); + objectNode.set("arr1", arr1ArrayNode); + objectNode.set("arr2", arr2ArrayNode); + return objectNode; + }, + TEST_ITERATIONS, + "FractalsConcatDataSet" + concatType + ); + } + + @Test + public void generateFractalsReplaceAtIndexDataSet() throws IOException { + DrawInstruction[] drawInstructions = DrawInstruction.values(); + + TestUtils.generateJsonTestData( + (mapper, index, rnd) -> { + int arrLength = rnd.nextInt(1, 10); + List arr = new ArrayList<>(); + for (int i = 0; i < arrLength; i++) { + arr.add(drawInstructions[rnd.nextInt(drawInstructions.length)].name()); + } + int idx = rnd.nextInt(arr.size()); + String elem = drawInstructions[rnd.nextInt(drawInstructions.length)].name(); + + ArrayNode arrArrayNode = mapper.createArrayNode(); + arr.forEach(arrArrayNode::add); + ObjectNode objectNode = mapper.createObjectNode(); + objectNode.set("arr", arrArrayNode); + objectNode.put("idx", idx); + objectNode.put("elem", elem); + return objectNode; + }, + TEST_ITERATIONS, + "FractalsReplaceAtIndexDataSet" + ); + } + + @Test + public void generateFractalsDragonCurveDataSet() throws IOException { + AtomicInteger atomicN = new AtomicInteger(2); + + TestUtils.generateJsonTestData( + (mapper, index, rnd) -> { + int n = atomicN.getAndIncrement(); + DrawInstruction[] dragonCurveInstructions = Fractals.dragonCurve(n); + List expected = new ArrayList<>(); + for (int i = 0; i < dragonCurveInstructions.length; i++) { + expected.add(dragonCurveInstructions[i].name()); + } + + ArrayNode arrayNode = mapper.createArrayNode(); + expected.forEach(arrayNode::add); + ObjectNode objectNode = mapper.createObjectNode(); + objectNode.put("n", n); + objectNode.set("expected", arrayNode); + return objectNode; + }, + 8, + "FractalsDragonCurveDataSet" + ); + } + + @Test + public void generateFractalsKochSnowflakeDataSet() throws IOException { + AtomicInteger atomicN = new AtomicInteger(2); + + TestUtils.generateJsonTestData( + (mapper, index, rnd) -> { + int n = atomicN.getAndIncrement(); + DrawInstruction[] kochSnowflakeInstructions = Fractals.kochSnowflake(n); + List expected = new ArrayList<>(); + for (int i = 0; i < kochSnowflakeInstructions.length; i++) { + expected.add(kochSnowflakeInstructions[i].name()); + } + + ArrayNode arrayNode = mapper.createArrayNode(); + expected.forEach(arrayNode::add); + ObjectNode objectNode = mapper.createObjectNode(); + objectNode.put("n", n); + objectNode.set("expected", arrayNode); + return objectNode; + }, + 4, + "FractalsKochSnowflakeDataSet" + ); + } +} diff --git a/src/graderPrivate/java/h06/TestUtils.java b/src/graderPrivate/java/h06/TestUtils.java new file mode 100644 index 0000000..76893e2 --- /dev/null +++ b/src/graderPrivate/java/h06/TestUtils.java @@ -0,0 +1,88 @@ +package h06; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.tudalgo.algoutils.tutor.general.SpoonUtils; +import spoon.reflect.declaration.CtMethod; +import spoon.reflect.declaration.CtParameter; + +import java.io.IOException; +import java.nio.file.Paths; +import java.util.List; +import java.util.Random; + +import static h06.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", + "h06", + 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); + } + + /** + * Get the Spoon representation of a method. + * + * @param clazz the class the method is defined in + * @param methodName the name of the method + * @param paramTypes the formal parameters of the method + * @return the Spoon {@link CtMethod} object + */ + public static CtMethod getCtMethod(Class clazz, String methodName, Class... paramTypes) { + return SpoonUtils.getType(clazz.getName()) + .getMethodsByName(methodName) + .stream() + .filter(ctMethod -> { + List> parameters = ctMethod.getParameters(); + boolean result = parameters.size() == paramTypes.length; + for (int i = 0; result && i < parameters.size(); i++) { + result = parameters.get(i).getType().getQualifiedName().equals(paramTypes[i].getTypeName()); + } + return result; + }) + .findAny() + .orElseThrow(); + } +} diff --git a/src/graderPrivate/resources/h06/.gitignore b/src/graderPrivate/resources/h06/.gitignore new file mode 100644 index 0000000..f5d73e8 --- /dev/null +++ b/src/graderPrivate/resources/h06/.gitignore @@ -0,0 +1 @@ +*.generated.json diff --git a/src/main/java/h06/Main.java b/src/main/java/h06/Main.java new file mode 100644 index 0000000..bb8af04 --- /dev/null +++ b/src/main/java/h06/Main.java @@ -0,0 +1,28 @@ +package h06; + +import h06.problems.*; +import h06.ui.DrawInstruction; +import h06.ui.FractalVisualizer; +import org.tudalgo.algoutils.student.annotation.StudentImplementationRequired; + +/** + * Main entry point in executing the program. + */ +public class Main { + /** + * Main entry point in executing the program. + * + * @param args program arguments, currently ignored + */ + @StudentImplementationRequired + public static void main(String[] args) { + DrawInstruction[] dragonCurveInstructions = Fractals.dragonCurve(14); + DrawInstruction[] kochSnowflakeInstructions = Fractals.kochSnowflake(5); + + FractalVisualizer fracVis = new FractalVisualizer(dragonCurveInstructions, 90); + fracVis.setVisible(true); + + FractalVisualizer fracVis2 = new FractalVisualizer(kochSnowflakeInstructions, 60); + fracVis2.setVisible(true); + } +} diff --git a/src/main/java/h06/problems/Fibonacci.java b/src/main/java/h06/problems/Fibonacci.java new file mode 100644 index 0000000..e4b1fd8 --- /dev/null +++ b/src/main/java/h06/problems/Fibonacci.java @@ -0,0 +1,71 @@ +package h06.problems; + +import org.tudalgo.algoutils.student.annotation.DoNotTouch; +import org.tudalgo.algoutils.student.annotation.StudentImplementationRequired; + +/** + * A class containing different implementations for computing the nth number in the Fibonacci sequence. + * + * @author Manuel Peters + */ +public class Fibonacci { + + /** + * Default Constructor for this class. + */ + @DoNotTouch + public Fibonacci() {} + + /** + * Computes the nth number from the Fibonacci sequence recursively. + * + * @param n The index of the Fibonacci sequence to compute. + * @return The nth number from the Fibonacci sequence. + */ + @DoNotTouch + public static int fibonacciRecursiveClassic(int n) { + if ( n <= 1) { + return n; + } else { + return fibonacciRecursiveClassic(n - 1) + fibonacciRecursiveClassic(n - 2); + } + } + + /** + * Computes the nth number from the Fibonacci sequence using a different recursive approach. + * + * @param n The index of the Fibonacci sequence to compute. + * @return The nth number from the Fibonacci sequence. + */ + @StudentImplementationRequired + public static int fibonacciRecursiveDifferent(int n) { + return doTheRecursion(0, 1, n); + } + + @StudentImplementationRequired + private static int doTheRecursion(int a, int b, int n) { + // Verbindliche Anforderung: Bedingungsoperator verwenden! + return n <= 0 ? a : doTheRecursion(b, a+b, n-1); + } + + /** + * Computes the nth number from the Fibonacci sequence iteratively. + * + * @param n The index of the Fibonacci sequence to compute. + * @return The nth number from the Fibonacci sequence. + */ + @StudentImplementationRequired + public static int fibonacciIterative(int n) { + int result = n; + int twoBefore = 0; + int oneBefore = 1; + + for (int i = 2; i <= n; i++) { + result = oneBefore + twoBefore; + twoBefore = oneBefore; + oneBefore = result; + } + + return result; + } +} \ No newline at end of file diff --git a/src/main/java/h06/problems/Fractals.java b/src/main/java/h06/problems/Fractals.java new file mode 100644 index 0000000..4c3f16e --- /dev/null +++ b/src/main/java/h06/problems/Fractals.java @@ -0,0 +1,155 @@ +package h06.problems; + +import h06.ui.DrawInstruction; +import org.tudalgo.algoutils.student.annotation.DoNotTouch; +import org.tudalgo.algoutils.student.annotation.StudentImplementationRequired; + +/** + * A class to generate draw instructions in order to draw a dragon curve. + * + * @author Manuel Peters + */ +public class Fractals { + + /** + * Default Constructor for this class. + */ + @DoNotTouch + public Fractals() {} + + /** + * This method calculates a raised to the power of b using recursion. + * a and b are expected to be non-negative integers. + * + * @param a the base, must be non-negative + * @param b the exponent, must be non-negative + * @return the result of a raised to the power of b + */ + @StudentImplementationRequired + public static int pow(int a, int b) { + if(b == 0) { + return 1; + } else { + return a * pow(a, b-1); + } + } + + /** + * This method combines two arrays of DrawInstruction objects into a single array. + * The elements of the first array are followed by the elements of the second array in the new array. + * + * @param arr1 the first array of type DrawInstruction + * @param arr2 the second array of type DrawInstruction + * @return A new array containing all elements of arr1 followed by all elements of arr2 + */ + @StudentImplementationRequired + public static DrawInstruction[] concatenate(DrawInstruction[] arr1, DrawInstruction[] arr2) { + DrawInstruction[] newArr = new DrawInstruction[arr1.length + arr2.length]; + + for (int i = 0; i < arr1.length; i++) { + newArr[i] = arr1[i]; + } + + for (int i = arr1.length; i < arr1.length+arr2.length; i++) { + newArr[i] = arr2[i-arr1.length]; + } + + return newArr; + } + + /** + * This method creates a new array that is a copy of the input array arr, but with the element at the specified + * index idx replaced by elem. + * + * @param arr the original array of type DrawInstruction + * @param idx the index at which to replace the element + * @param elem the new DrawInstruction to place at the specified index + * @return A new array with the element at idx replaced by elem + */ + @StudentImplementationRequired + public static DrawInstruction[] replaceAtIndex(DrawInstruction[] arr, int idx, DrawInstruction elem) { + DrawInstruction[] newArr = new DrawInstruction[arr.length]; + + for (int i = 0; i < newArr.length; i++) { + if(i == idx) { + newArr[idx] = elem; + } else { + newArr[i] = arr[i]; + } + } + + return newArr; + } + + /** + * Generates an array of DrawInstruction objects to draw a dragon curve of order n + * + * @param n The order of the dragon curve to generate + * @return an array of DrawInstruction objects to draw a dragon curve of order n + */ + @StudentImplementationRequired + public static DrawInstruction[] dragonCurve(int n) { + if(n <= 0) { + return new DrawInstruction[]{ + DrawInstruction.DRAW_LINE + }; + } + if (n == 1) { + return new DrawInstruction[]{ + DrawInstruction.DRAW_LINE, + DrawInstruction.TURN_RIGHT, + DrawInstruction.DRAW_LINE + }; + } + else { + DrawInstruction[] lastOrder = dragonCurve(n - 1); + DrawInstruction[] firstHalf = concatenate(lastOrder, new DrawInstruction[]{DrawInstruction.TURN_RIGHT}); + DrawInstruction[] secondHalf = replaceAtIndex(lastOrder, pow(2, n-1)-1, DrawInstruction.TURN_LEFT); + return concatenate(firstHalf, secondHalf); + } + } + + /** + * Generates an array of DrawInstruction objects to draw a koch snowflake of order n + * + * @param n The order of the koch snowflake to generate + * @return an array of DrawInstruction objects to draw a koch snowflake of order n + */ + @StudentImplementationRequired + public static DrawInstruction[] kochSnowflake(int n) { + if (n <= 0) { + return new DrawInstruction[]{ + DrawInstruction.DRAW_LINE, + DrawInstruction.TURN_RIGHT, + DrawInstruction.TURN_RIGHT, + DrawInstruction.DRAW_LINE, + DrawInstruction.TURN_RIGHT, + DrawInstruction.TURN_RIGHT, + DrawInstruction.DRAW_LINE, + }; + } else { + DrawInstruction[] lastOrder = kochSnowflake(n - 1); + DrawInstruction[] currentOrder = new DrawInstruction[lastOrder.length * 4]; + int index = 0; + + for (DrawInstruction instruction : lastOrder) { + if (instruction == DrawInstruction.DRAW_LINE) { + currentOrder[index++] = DrawInstruction.DRAW_LINE; + currentOrder[index++] = DrawInstruction.TURN_LEFT; + currentOrder[index++] = DrawInstruction.DRAW_LINE; + currentOrder[index++] = DrawInstruction.TURN_RIGHT; + currentOrder[index++] = DrawInstruction.TURN_RIGHT; + currentOrder[index++] = DrawInstruction.DRAW_LINE; + currentOrder[index++] = DrawInstruction.TURN_LEFT; + currentOrder[index++] = DrawInstruction.DRAW_LINE; + } else if (instruction == DrawInstruction.TURN_LEFT) { + currentOrder[index++] = instruction; + } else if (instruction == DrawInstruction.TURN_RIGHT) { + currentOrder[index++] = instruction; + } + } + + return currentOrder; + } + } +} diff --git a/src/main/java/h06/problems/LinearSearch.java b/src/main/java/h06/problems/LinearSearch.java new file mode 100644 index 0000000..c235777 --- /dev/null +++ b/src/main/java/h06/problems/LinearSearch.java @@ -0,0 +1,54 @@ +package h06.problems; + +import org.tudalgo.algoutils.student.annotation.StudentImplementationRequired; + +public class LinearSearch { + + /** + * Recursively searches for a target in an array using linear search. + * + * @param arr the array to search in + * @param target the target to search for + * @return the index of the target in the array, or -1 if the target is not found + */ + @StudentImplementationRequired + public static int linearSearchRecursive(int[] arr, int target) { + return linearSearchRecursiveHelper(arr, target, 0); + } + + /** + * Recursively searches for a target in an array using linear search. + * + * @param arr the array to search in + * @param target the target to search for + * @param index the index to start searching from + * @return the index of the target in the array, or -1 if the target is not found + */ + @StudentImplementationRequired + public static int linearSearchRecursiveHelper(int[] arr, int target, int index) { + if (index == arr.length) { + return -1; + } else if (arr[index] == target) { + return index; + } else { + return linearSearchRecursiveHelper(arr, target, index + 1); + } + } + + /** + * Iteratively searches for a target in an array using linear search. + * + * @param arr the array to search in + * @param target the target to search for + * @return the index of the target in the array, or -1 if the target is not found + */ + @StudentImplementationRequired + public static int linearSearchIterative(int[] arr, int target) { + for (int i = 0; i < arr.length; i++) { + if (arr[i] == target) { + return i; + } + } + return -1; + } +} diff --git a/src/main/java/h06/ui/DrawInstruction.java b/src/main/java/h06/ui/DrawInstruction.java new file mode 100644 index 0000000..e65170c --- /dev/null +++ b/src/main/java/h06/ui/DrawInstruction.java @@ -0,0 +1,15 @@ +package h06.ui; + +import org.tudalgo.algoutils.student.annotation.DoNotTouch; + +/** + * The DrawInstruction enum represents draw instructions in order to draw a fractal. + * + * @author Manuel Peters + */ +@DoNotTouch +public enum DrawInstruction { + DRAW_LINE, + TURN_LEFT, + TURN_RIGHT +} diff --git a/src/main/java/h06/ui/FractalDrawer.java b/src/main/java/h06/ui/FractalDrawer.java new file mode 100644 index 0000000..cab0c5a --- /dev/null +++ b/src/main/java/h06/ui/FractalDrawer.java @@ -0,0 +1,103 @@ +package h06.ui; + +import org.tudalgo.algoutils.student.annotation.DoNotTouch; + +import javax.swing.*; +import java.awt.*; +import java.awt.geom.Line2D; +import java.awt.geom.Rectangle2D; +import java.util.ArrayList; +import java.util.List; + +/** + * A class for drawing fractals, given their drawing instructions. + * + * @author Manuel Peters + */ +@DoNotTouch +public class FractalDrawer extends JPanel { + private final DrawInstruction[] drawInstructions; + private final List lines; + + /** + * The angle in radians for a turn. + * This angle determines how far a left or right turn will go. + */ + private final double angle; + + /** + * Constructor for this class. + * + * @param drawInstructions the instructions to draw the fractal + * @param angle the angle in degrees for a turn + */ + public FractalDrawer(DrawInstruction[] drawInstructions, int angle) { + this.drawInstructions = drawInstructions; + this.lines = new ArrayList<>(); + this.angle = Math.toRadians(angle); + } + + /** + * Draws on the canvas by following the drawing instructions + */ + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + setBackground(Color.WHITE); + + Graphics2D g2d = (Graphics2D) g; + + // Set the initial position and direction + double x = 0; + double y = 0; + // The first line is drawn to the right + double currentAngle = 0; + + double minX = x; + double minY = y; + double maxX = x; + double maxY = y; + + // Store the lines based on instructions + for (DrawInstruction instruction : drawInstructions) { + if (instruction.equals(DrawInstruction.DRAW_LINE)) { + double x2 = x + Math.cos(currentAngle); + double y2 = y + Math.sin(currentAngle); + lines.add(new Line2D.Double(x, y, x2, y2)); + x = x2; + y = y2; + + // Update Bounding Box values + minX = Math.min(minX, x); + minY = Math.min(minY, y); + maxX = Math.max(maxX, x); + maxY = Math.max(maxY, y); + } else if (instruction.equals(DrawInstruction.TURN_LEFT)) { + currentAngle -= angle; + } else if (instruction.equals(DrawInstruction.TURN_RIGHT)) { + currentAngle += angle; + } + } + + // Define Bounding Box + Rectangle2D bounds = new Rectangle2D.Double(minX - 1, minY - 1, maxX - minX + 2, maxY - minY + 2); + + // Calculate scaling factor to fit all the lines within the panel + double scaleX = getWidth() / bounds.getWidth(); + double scaleY = getHeight() / bounds.getHeight(); + double scale = Math.min(scaleX, scaleY); + + // Calculate the initial position to center the fractal + double offsetX = (getWidth() - bounds.getWidth() * scale) / 2 - bounds.getX() * scale; + double offsetY = (getHeight() - bounds.getHeight() * scale) / 2 - bounds.getY() * scale; + + // Draw the stored lines with scaling and offset + for (Line2D line : lines) { + double x1 = offsetX + line.getX1() * scale; + double y1 = offsetY + line.getY1() * scale; + double x2 = offsetX + line.getX2() * scale; + double y2 = offsetY + line.getY2() * scale; + g2d.draw(new Line2D.Double(x1, y1, x2, y2)); + } + } +} diff --git a/src/main/java/h06/ui/FractalVisualizer.java b/src/main/java/h06/ui/FractalVisualizer.java new file mode 100644 index 0000000..cd5d539 --- /dev/null +++ b/src/main/java/h06/ui/FractalVisualizer.java @@ -0,0 +1,32 @@ +package h06.ui; + +import org.tudalgo.algoutils.student.annotation.DoNotTouch; + +import javax.swing.*; +import java.awt.*; + +/** + * A class to visualize fractals, given their drawing instructions. + * + * @author Manuel Peters + */ +@DoNotTouch +public class FractalVisualizer extends JFrame { + + /** + * Constructor for this class. + * + * @param drawInstructions the instructions to draw the fractal + * @param angle the angle in degrees for a turn + */ + public FractalVisualizer(DrawInstruction[] drawInstructions, int angle) { + super("Fractal Visualizer"); + setSize(new Dimension(800,800)); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setLocationRelativeTo(null); + setLayout(new GridLayout(1,1)); + + FractalDrawer fractalDrawer = new FractalDrawer(drawInstructions, angle); + add(fractalDrawer); + } +} diff --git a/src/test/java/h06/ExampleJUnitTest.java b/src/test/java/h06/ExampleJUnitTest.java new file mode 100644 index 0000000..70753f2 --- /dev/null +++ b/src/test/java/h06/ExampleJUnitTest.java @@ -0,0 +1,16 @@ +package h06; + +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