From 12772a43da1a63cad3365d3101f936e4bfa94a93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Kn=C3=B6dlseder?= <53149143+chrisknedl@users.noreply.github.com> Date: Sat, 16 Sep 2023 11:25:07 +0200 Subject: [PATCH] Programming exercises: Add blackbox tests as another Java project type (#6736) --- .../ProgrammingLanguageConfiguration.java | 1 + .../domain/enumeration/ProjectType.java | 4 +- .../in/www1/artemis/service/FileService.java | 2 +- ...kinsProgrammingLanguageFeatureService.java | 4 +- .../JenkinsPipelineScriptCreator.java | 3 + .../ProgrammingExerciseRepositoryService.java | 42 ++- src/main/resources/config/application.yml | 1 + .../java/maven_blackbox/exercise/Tests.txt | 0 .../java/maven_blackbox/exercise/pom.xml | 31 +++ .../src/${packageNameFolder}/Client.java | 5 + .../templates/java/maven_blackbox/readme | 134 ++++++++++ .../java/maven_blackbox/solution/Tests.txt | 35 +++ .../java/maven_blackbox/solution/pom.xml | 31 +++ .../src/${packageNameFolder}/Client.java | 67 +++++ .../src/${packageNameFolder}/Context.java | 52 ++++ .../${packageNameFolder}/input/Command.java | 41 +++ .../input/CommandParser.java | 93 +++++++ .../input/InvalidCommandException.java | 20 ++ .../src/${packageNameFolder}/Policy.java | 6 +- .../blackbox/projectTemplate/git.ignore.file | 198 ++++++++++++++ .../test/blackbox/projectTemplate/pom.xml | 109 ++++++++ .../test/blackbox/projectTemplate/readme.md | 14 + .../${packageNameFolder}.tests/advanced.exp | 14 + .../${packageNameFolder}.tests/public.exp | 9 + .../${packageNameFolder}.tests/secret.exp | 11 + .../testsuite/config/default.exp | 225 ++++++++++++++++ .../testsuite/lib/${packageNameFile}.exp | 0 .../testsuite/testfiles/public/.gitkeep | 0 .../testsuite/testfiles/secret/.gitkeep | 0 .../java/blackbox/regularRuns/pipeline.groovy | 246 ++++++++++++++++++ .../entities/programming-exercise.model.ts | 1 + .../programming-exercise-update.component.ts | 45 +++- ...ogramming-exercise-language.component.html | 12 +- src/main/webapp/i18n/de/exercise.json | 3 +- .../webapp/i18n/de/programmingExercise.json | 1 + src/main/webapp/i18n/en/exercise.json | 3 +- .../webapp/i18n/en/programmingExercise.json | 1 + ...gramming-exercise-update.component.spec.ts | 58 +++-- 38 files changed, 1472 insertions(+), 50 deletions(-) create mode 100644 src/main/resources/templates/java/maven_blackbox/exercise/Tests.txt create mode 100644 src/main/resources/templates/java/maven_blackbox/exercise/pom.xml create mode 100644 src/main/resources/templates/java/maven_blackbox/exercise/src/${packageNameFolder}/Client.java create mode 100644 src/main/resources/templates/java/maven_blackbox/readme create mode 100644 src/main/resources/templates/java/maven_blackbox/solution/Tests.txt create mode 100644 src/main/resources/templates/java/maven_blackbox/solution/pom.xml create mode 100644 src/main/resources/templates/java/maven_blackbox/solution/src/${packageNameFolder}/Client.java create mode 100644 src/main/resources/templates/java/maven_blackbox/solution/src/${packageNameFolder}/Context.java create mode 100644 src/main/resources/templates/java/maven_blackbox/solution/src/${packageNameFolder}/input/Command.java create mode 100644 src/main/resources/templates/java/maven_blackbox/solution/src/${packageNameFolder}/input/CommandParser.java create mode 100644 src/main/resources/templates/java/maven_blackbox/solution/src/${packageNameFolder}/input/InvalidCommandException.java create mode 100644 src/main/resources/templates/java/test/blackbox/projectTemplate/git.ignore.file create mode 100644 src/main/resources/templates/java/test/blackbox/projectTemplate/pom.xml create mode 100644 src/main/resources/templates/java/test/blackbox/projectTemplate/readme.md create mode 100644 src/main/resources/templates/java/test/blackbox/projectTemplate/testsuite/${packageNameFolder}.tests/advanced.exp create mode 100644 src/main/resources/templates/java/test/blackbox/projectTemplate/testsuite/${packageNameFolder}.tests/public.exp create mode 100644 src/main/resources/templates/java/test/blackbox/projectTemplate/testsuite/${packageNameFolder}.tests/secret.exp create mode 100644 src/main/resources/templates/java/test/blackbox/projectTemplate/testsuite/config/default.exp create mode 100644 src/main/resources/templates/java/test/blackbox/projectTemplate/testsuite/lib/${packageNameFile}.exp create mode 100644 src/main/resources/templates/java/test/blackbox/projectTemplate/testsuite/testfiles/public/.gitkeep create mode 100644 src/main/resources/templates/java/test/blackbox/projectTemplate/testsuite/testfiles/secret/.gitkeep create mode 100644 src/main/resources/templates/jenkins/java/blackbox/regularRuns/pipeline.groovy diff --git a/src/main/java/de/tum/in/www1/artemis/config/ProgrammingLanguageConfiguration.java b/src/main/java/de/tum/in/www1/artemis/config/ProgrammingLanguageConfiguration.java index dbf5c4cd88bf..9664eeb1970f 100644 --- a/src/main/java/de/tum/in/www1/artemis/config/ProgrammingLanguageConfiguration.java +++ b/src/main/java/de/tum/in/www1/artemis/config/ProgrammingLanguageConfiguration.java @@ -188,6 +188,7 @@ private ProjectType getConfiguredProjectType(final ProjectType actualProjectType case XCODE -> ProjectType.XCODE; case FACT -> ProjectType.FACT; case GCC -> ProjectType.GCC; + case MAVEN_BLACKBOX -> ProjectType.MAVEN_BLACKBOX; }; } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/enumeration/ProjectType.java b/src/main/java/de/tum/in/www1/artemis/domain/enumeration/ProjectType.java index f47cdc0545f4..4f5e82a9ae0f 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/enumeration/ProjectType.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/enumeration/ProjectType.java @@ -8,10 +8,10 @@ */ public enum ProjectType { - MAVEN_MAVEN, PLAIN_MAVEN, PLAIN, XCODE, FACT, GCC, PLAIN_GRADLE, GRADLE_GRADLE; + MAVEN_MAVEN, PLAIN_MAVEN, PLAIN, XCODE, FACT, GCC, PLAIN_GRADLE, GRADLE_GRADLE, MAVEN_BLACKBOX; public boolean isMaven() { - return this == MAVEN_MAVEN || this == PLAIN_MAVEN; + return this == MAVEN_MAVEN || this == PLAIN_MAVEN || this == MAVEN_BLACKBOX; } public boolean isGradle() { diff --git a/src/main/java/de/tum/in/www1/artemis/service/FileService.java b/src/main/java/de/tum/in/www1/artemis/service/FileService.java index b9a1649a5ba4..a3d77358dfdc 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/FileService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/FileService.java @@ -102,7 +102,7 @@ public class FileService implements DisposableBean { /** * These directories get falsely marked as files and should be ignored during copying. */ - private static final List IGNORED_DIRECTORY_SUFFIXES = List.of(".xcassets", ".colorset", ".appiconset", ".xcworkspace", ".xcodeproj", ".swiftpm"); + private static final List IGNORED_DIRECTORY_SUFFIXES = List.of(".xcassets", ".colorset", ".appiconset", ".xcworkspace", ".xcodeproj", ".swiftpm", ".tests"); @Override public void destroy() { diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsProgrammingLanguageFeatureService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsProgrammingLanguageFeatureService.java index 791a2920a201..1db2e5e66aac 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsProgrammingLanguageFeatureService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsProgrammingLanguageFeatureService.java @@ -18,8 +18,8 @@ public class JenkinsProgrammingLanguageFeatureService extends ProgrammingLanguag public JenkinsProgrammingLanguageFeatureService() { // Must be extended once a new programming language is added programmingLanguageFeatures.put(EMPTY, new ProgrammingLanguageFeature(EMPTY, false, false, false, false, false, List.of(), false, true, false)); - programmingLanguageFeatures.put(JAVA, - new ProgrammingLanguageFeature(JAVA, true, true, true, true, false, List.of(PLAIN_GRADLE, GRADLE_GRADLE, PLAIN_MAVEN, MAVEN_MAVEN), true, true, false)); + programmingLanguageFeatures.put(JAVA, new ProgrammingLanguageFeature(JAVA, true, true, true, true, false, + List.of(PLAIN_GRADLE, GRADLE_GRADLE, PLAIN_MAVEN, MAVEN_MAVEN, MAVEN_BLACKBOX), true, true, false)); programmingLanguageFeatures.put(KOTLIN, new ProgrammingLanguageFeature(KOTLIN, true, false, true, true, false, List.of(), true, true, false)); programmingLanguageFeatures.put(PYTHON, new ProgrammingLanguageFeature(PYTHON, false, false, true, false, false, List.of(), false, true, false)); // Jenkins is not supporting XCODE at the moment diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsPipelineScriptCreator.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsPipelineScriptCreator.java index ef181f098622..f3adeef9a4fa 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsPipelineScriptCreator.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsPipelineScriptCreator.java @@ -120,6 +120,9 @@ private Optional getProjectTypeName(final ProgrammingLanguage programmin else if (projectType.isPresent() && projectType.get().isGradle()) { return Optional.of("gradle"); } + else if (projectType.isPresent() && projectType.get().equals(ProjectType.MAVEN_BLACKBOX)) { + return Optional.of("blackbox"); + } // Maven is also the project type for all other Java exercises (also if the project type is not present) else if (ProgrammingLanguage.JAVA.equals(programmingLanguage)) { return Optional.of("maven"); diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseRepositoryService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseRepositoryService.java index 688cacd177a0..b43038b0a0b6 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseRepositoryService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseRepositoryService.java @@ -1,6 +1,7 @@ package de.tum.in.www1.artemis.service.programming; import static de.tum.in.www1.artemis.config.Constants.SETUP_COMMIT_MESSAGE; +import static de.tum.in.www1.artemis.domain.enumeration.ProjectType.isMavenProject; import java.io.FileNotFoundException; import java.io.IOException; @@ -295,16 +296,8 @@ private void setupJVMTestTemplateAndPush(final RepositoryResources resources, fi // First get files that are not dependent on the project type final Path templatePath = ProgrammingExerciseService.getProgrammingLanguageTemplatePath(programmingExercise.getProgrammingLanguage()).resolve(TEST_DIR); - - // Java both supports Gradle and Maven as a test template - Path projectTemplatePath = templatePath; - if (projectType != null && projectType.isGradle()) { - projectTemplatePath = projectTemplatePath.resolve("gradle"); - } - else { - projectTemplatePath = projectTemplatePath.resolve("maven"); - } - projectTemplatePath = projectTemplatePath.resolve("projectTemplate"); + // Java supports multiple variants as test template + final Path projectTemplatePath = getJavaProjectTemplatePath(templatePath, projectType); final Resource[] projectTemplate = resourceLoaderService.getResources(projectTemplatePath); // keep the folder structure @@ -315,6 +308,11 @@ private void setupJVMTestTemplateAndPush(final RepositoryResources resources, fi setupJVMTestTemplateProjectTypeResources(resources, programmingExercise, repoLocalPath); } + if (ProjectType.MAVEN_BLACKBOX.equals(projectType)) { + Path dejagnuLibFolderPath = repoLocalPath.resolve("testsuite").resolve("lib"); + fileService.replaceVariablesInFileName(dejagnuLibFolderPath.toString(), PACKAGE_NAME_FILE_PLACEHOLDER, programmingExercise.getPackageName()); + } + final Map sectionsMap = new HashMap<>(); // Keep or delete static code analysis configuration in the build configuration file sectionsMap.put("static-code-analysis", Boolean.TRUE.equals(programmingExercise.isStaticCodeAnalysisEnabled())); @@ -332,6 +330,22 @@ private void setupJVMTestTemplateAndPush(final RepositoryResources resources, fi commitAndPushRepository(resources.repository, "Test-Template pushed by Artemis", true, user); } + private static Path getJavaProjectTemplatePath(final Path templatePath, final ProjectType projectType) { + Path projectTemplatePath = templatePath; + + if (projectType != null && projectType.isGradle()) { + projectTemplatePath = projectTemplatePath.resolve("gradle"); + } + else if (ProjectType.MAVEN_BLACKBOX.equals(projectType)) { + projectTemplatePath = projectTemplatePath.resolve("blackbox"); + } + else { + projectTemplatePath = projectTemplatePath.resolve("maven"); + } + + return projectTemplatePath.resolve("projectTemplate"); + } + /** * Copies project type specific resources into the test repository. * @@ -378,7 +392,9 @@ private void setupTestTemplateRegularTestRuns(final RepositoryResources resource setupBuildToolProjectFile(repoLocalPath, projectType, sectionsMap); - fileService.copyResources(testFileResources, resources.prefix, packagePath, false); + if (programmingExercise.getProjectType() != ProjectType.MAVEN_BLACKBOX) { + fileService.copyResources(testFileResources, resources.prefix, packagePath, false); + } if (projectType != null) { overwriteProjectTypeSpecificFiles(resources, programmingExercise, packagePath); @@ -455,7 +471,7 @@ private void setupTestTemplateSequentialTestRuns(final RepositoryResources resou sectionsMap.put("sequential", true); // maven configuration should be set for kotlin and older exercises where no project type has been introduced where no project type is defined - final boolean isMaven = ProjectType.isMavenProject(projectType); + final boolean isMaven = isMavenProject(projectType); final String projectFileName; if (isMaven) { @@ -527,7 +543,7 @@ private void setupBuildStage(final Path resourcePrefix, final Path templatePath, final Path packagePath = buildStagePath.toAbsolutePath().resolve(TEST_DIR).resolve(PACKAGE_NAME_FOLDER_PLACEHOLDER).toAbsolutePath(); // staging project files are only required for maven - final boolean isMaven = ProjectType.isMavenProject(projectType); + final boolean isMaven = isMavenProject(projectType); if (isMaven && stagePomXml.isPresent()) { Files.copy(stagePomXml.get().getInputStream(), buildStagePath.resolve(POM_XML)); } diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml index 445e00310e2f..7ac9fda902be 100644 --- a/src/main/resources/config/application.yml +++ b/src/main/resources/config/application.yml @@ -64,6 +64,7 @@ artemis: java: # possible overrides: maven, gradle default: "ls1tum/artemis-maven-template:java17-18" + maven_blackbox: "ghcr.io/uni-passau-artemis/artemis-dejagnu:20" kotlin: # possible overrides: maven, gradle default: "ls1tum/artemis-maven-template:java17-18" diff --git a/src/main/resources/templates/java/maven_blackbox/exercise/Tests.txt b/src/main/resources/templates/java/maven_blackbox/exercise/Tests.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/main/resources/templates/java/maven_blackbox/exercise/pom.xml b/src/main/resources/templates/java/maven_blackbox/exercise/pom.xml new file mode 100644 index 000000000000..ea789a750f05 --- /dev/null +++ b/src/main/resources/templates/java/maven_blackbox/exercise/pom.xml @@ -0,0 +1,31 @@ + + 4.0.0 + ${packageName} + ${exerciseNamePomXml} + jar + 1.0 + ${exerciseNamePomXml} + + UTF-8 + + + ${project.basedir}/src + + + ${project.basedir}/resources + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 20 + + + + + diff --git a/src/main/resources/templates/java/maven_blackbox/exercise/src/${packageNameFolder}/Client.java b/src/main/resources/templates/java/maven_blackbox/exercise/src/${packageNameFolder}/Client.java new file mode 100644 index 000000000000..94d3ca2167c4 --- /dev/null +++ b/src/main/resources/templates/java/maven_blackbox/exercise/src/${packageNameFolder}/Client.java @@ -0,0 +1,5 @@ +package ${packageName}; + +public class Client { + // TODO: Create and implement interactive command line handling +} diff --git a/src/main/resources/templates/java/maven_blackbox/readme b/src/main/resources/templates/java/maven_blackbox/readme new file mode 100644 index 000000000000..53ffae9588b6 --- /dev/null +++ b/src/main/resources/templates/java/maven_blackbox/readme @@ -0,0 +1,134 @@ +# Sorting with the Strategy Pattern + +In this exercise, we want to implement sorting algorithms and control the program interactively via user input on the console. + +**Note:** This project is using `Maven`! You have to import the project as Maven project (not as Eclipse project) as otherwise, errors will occur and you won't be able to work on this exercise. + +### Part 1: User Input + +First, there has to be a way for the user to communicate with the program. +The `Client` class should handle the user input. The following commands need to be supported by your implementation: + +`add date1 date2 ...`
+Adds at least one date, but should also support multiple dates.
+ +`sort`
+Sorts the previously entered dates.
+ +`clear`
+Clears the list of dates.
+ +`help`
+Prints a dialog to the console that briefly explains the supported commands.
+ +`print`
+Prints the current list of dates to the console.
+ +`quit`
+Terminates the program.
+ +Using a `BufferedReader` might be a good starting point. + + + +### Part 2: Sorting + +We need to implement two sorting algorithms, in this case `MergeSort` and `BubbleSort`. + +**You have the following tasks:** + +1. **Implement Bubble Sort**
+Implement the method `performSort(List)` in the class `BubbleSort`. Make sure to follow the Bubble Sort algorithm exactly. + +2. **Implement Merge Sort**
+Implement the method `performSort(List)` in the class `MergeSort`. Make sure to follow the Merge Sort algorithm exactly. + +### Part 3: Strategy Pattern + +We want the application to apply different algorithms for sorting a `List` of `Date` objects. +Use the strategy pattern to select the right sorting algorithm at runtime. + +**You have the following tasks:** + +1. **SortStrategy Interface**
+Create a `SortStrategy` interface and adjust the sorting algorithms so that they implement this interface. + +2. **Context Class**
+Create and implement a `Context` class following the below class diagram + +3. **Context Policy**
+Create and implement a `Policy` class following the below class diagram with a simple configuration mechanism: + + 1. **Select MergeSort**
+ Select `MergeSort` when the List has more than 10 dates. + + 2. **Select BubbleSort**
+ Select `BubbleSort` when the List has less or equal 10 dates. + +4. Complete the `Client` class which demonstrates switching between two strategies at runtime. + +@startuml + +class Client { +} + +class Policy { + +configure() +} + +class Context { + -dates: List + +sort() +} + +interface SortStrategy { + +performSort(List) +} + +class BubbleSort { + +performSort(List) +} + +class MergeSort { + +performSort(List) +} + +MergeSort -up-|> SortStrategy +BubbleSort -up-|> SortStrategy +Policy -right-> Context: context +Context -right-> SortStrategy: sortAlgorithm +Client .down.> Policy +Client .down.> Context + +hide empty fields +hide empty methods + +@enduml + +### Part 4: Tests + +This section shows you which tests are passed by your implementation. + +1. [task][Main method exists](MainMethodChecker) + +2. [task][All lines have <= 80 characters](LineLengthChecker) + +3. [task][Tests.txt exists and is not empty](FileExistsChecker) + +4. [task][Public Tests](dejagnu[public]) + +5. [task][Advanced Tests](dejagnu[advanced]) + +6. [task][Secret Tests](dejagnu[secret]) + + +### Part 5: Optional Challenges + +(These are not tested) + +1. Create a new class `QuickSort` that implements `SortStrategy` and implement the Quick Sort algorithm. + +2. Make the method `performSort(List)` generic, so that other objects can also be sorted by the same method. +**Hint:** Have a look at Java Generics and the interface `Comparable`. + +3. Think about a useful decision in `Policy` when to use the new `QuickSort` algorithm. diff --git a/src/main/resources/templates/java/maven_blackbox/solution/Tests.txt b/src/main/resources/templates/java/maven_blackbox/solution/Tests.txt new file mode 100644 index 000000000000..50eb3340d94e --- /dev/null +++ b/src/main/resources/templates/java/maven_blackbox/solution/Tests.txt @@ -0,0 +1,35 @@ +sort> help +add: adds the given Dates to the list (format: YYYY-MM-DD) +clear: empties the list +help: prints this text +print: prints the list +sort: sorts the list +quit: quits the program +sort> +Unknown command. Use 'help' to show available commands. +sort> help +add: adds the given Dates to the list (format: YYYY-MM-DD) +clear: empties the list +help: prints this text +print: prints the list +sort: sorts the list +quit: quits the program +sort> 2015-04-01 +Unknown command. Use 'help' to show available commands. +sort> add 2015-04-01 +sort> print +[Wed Apr 01 02:00:00 CEST 2015] +sort> add 2016-04-01 +sort> add 2014-04-01 +sort> add 2015-05-01 2015-04-30 +sort> prinr +Unknown command. Use 'help' to show available commands. +sort> print +[Wed Apr 01 02:00:00 CEST 2015, Fri Apr 01 02:00:00 CEST 2016, Tue Apr 01 02:00:00 CEST 2014, Fri May 01 02:00:00 CEST 2015, Thu Apr 30 02:00:00 CEST 2015] +sort> sort +sort> print +[Tue Apr 01 02:00:00 CEST 2014, Wed Apr 01 02:00:00 CEST 2015, Thu Apr 30 02:00:00 CEST 2015, Fri May 01 02:00:00 CEST 2015, Fri Apr 01 02:00:00 CEST 2016] +sort> clear +sort> print +[] +sort> quit diff --git a/src/main/resources/templates/java/maven_blackbox/solution/pom.xml b/src/main/resources/templates/java/maven_blackbox/solution/pom.xml new file mode 100644 index 000000000000..25b901864e01 --- /dev/null +++ b/src/main/resources/templates/java/maven_blackbox/solution/pom.xml @@ -0,0 +1,31 @@ + + 4.0.0 + ${packageName} + ${exerciseNamePomXml}-Solution + jar + 1.0 + ${exerciseNamePomXml} Solution + + UTF-8 + + + ${project.basedir}/src + + + ${project.basedir}/resources + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 20 + + + + + diff --git a/src/main/resources/templates/java/maven_blackbox/solution/src/${packageNameFolder}/Client.java b/src/main/resources/templates/java/maven_blackbox/solution/src/${packageNameFolder}/Client.java new file mode 100644 index 000000000000..69da54b4e199 --- /dev/null +++ b/src/main/resources/templates/java/maven_blackbox/solution/src/${packageNameFolder}/Client.java @@ -0,0 +1,67 @@ +package ${packageName}; + +import ${packageName}.input.Command; +import ${packageName}.input.CommandParser; +import ${packageName}.input.InvalidCommandException; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +public final class Client { + private static final String PROMPT = "sort> "; + private static final Context CONTEXT = new Context(); + + private Client() { + throw new IllegalCallerException("utility class"); + } + + /** + * The entrypoint of the program. + * + * @param args The command line arguments. + */ + public static void main(String[] args) throws IOException { + BufferedReader in + = new BufferedReader(new InputStreamReader(System.in)); + CommandRunResult commandRunResult = CommandRunResult.CONTINUE; + while (commandRunResult == CommandRunResult.CONTINUE) { + System.out.print(PROMPT); + try { + Command command = CommandParser.parseCommand(in.readLine()); + commandRunResult = runCommand(command); + } catch (InvalidCommandException invalidCommandException) { + System.out.println(invalidCommandException.getMessage()); + } + } + } + + private static CommandRunResult runCommand(final Command command) { + if (command instanceof Command.AddCommand addCommand) { + CONTEXT.addDates(addCommand.dates()); + } else if (command instanceof Command.SortCommand) { + CONTEXT.sort(); + } else if (command instanceof Command.ClearCommand) { + CONTEXT.clearDates(); + } else if (command instanceof Command.HelpCommand helpCommand) { + System.out.println(helpCommand.helpMessage()); + } else if (command instanceof Command.PrintCommand) { + System.out.println(CONTEXT.getDates()); + } else if (command instanceof Command.QuitCommand) { + return CommandRunResult.QUIT; + } else { + // can never happen since all cases of the sealed interface are + // covered + // ToDo: refactor with Java 21 switch expression patterns when + // released to let the compiler check exhaustivity + throw new UnsupportedOperationException("Unknown command type."); + } + + return CommandRunResult.CONTINUE; + } + + private enum CommandRunResult { + CONTINUE, + QUIT + } +} diff --git a/src/main/resources/templates/java/maven_blackbox/solution/src/${packageNameFolder}/Context.java b/src/main/resources/templates/java/maven_blackbox/solution/src/${packageNameFolder}/Context.java new file mode 100644 index 000000000000..ee3e4e87bd2a --- /dev/null +++ b/src/main/resources/templates/java/maven_blackbox/solution/src/${packageNameFolder}/Context.java @@ -0,0 +1,52 @@ +package ${packageName}; + +import java.util.Date; +import java.util.List; +import java.util.ArrayList; + +public class Context { + private SortStrategy sortAlgorithm = new MergeSort(); + + private List dates = new ArrayList<>(); + + public List getDates() { + return dates; + } + + public void setDates(List dates) { + this.dates = dates; + } + + /** + * Adds the given dates to the internal list of dates. + * + * @param datesToAdd The dates that are added. + */ + public void addDates(List datesToAdd) { + this.dates.addAll(datesToAdd); + } + + /** + * Removes all dates from the list of dates. + */ + public void clearDates() { + this.dates = new ArrayList<>(); + } + + public void setSortAlgorithm(SortStrategy sa) { + sortAlgorithm = sa; + } + + public SortStrategy getSortAlgorithm() { + return sortAlgorithm; + } + + /** + * Runs the configured sort algorithm. + */ + public void sort() { + if (sortAlgorithm != null) { + sortAlgorithm.performSort(this.dates); + } + } +} diff --git a/src/main/resources/templates/java/maven_blackbox/solution/src/${packageNameFolder}/input/Command.java b/src/main/resources/templates/java/maven_blackbox/solution/src/${packageNameFolder}/input/Command.java new file mode 100644 index 000000000000..7d1d98eb18f2 --- /dev/null +++ b/src/main/resources/templates/java/maven_blackbox/solution/src/${packageNameFolder}/input/Command.java @@ -0,0 +1,41 @@ +package ${packageName}.input; + +import java.util.Date; +import java.util.List; + +public sealed interface Command { + /** + * Represents an add command. This ensures that only a list of dates can be + * added. + * + * @param dates The list of dates that is added to the list. + */ + record AddCommand(List dates) implements Command { } + + /** + * Represents a clear command. + */ + record ClearCommand() implements Command { } + + /** + * Reprents a help command. + * + * @param helpMessage The help message that is printed on the console. + */ + record HelpCommand(String helpMessage) implements Command { } + + /** + * Represents a print command. + */ + record PrintCommand() implements Command { } + + /** + * Represents a quit command. + */ + record QuitCommand() implements Command { } + + /** + * Represents a sort command. + */ + record SortCommand() implements Command { } +} diff --git a/src/main/resources/templates/java/maven_blackbox/solution/src/${packageNameFolder}/input/CommandParser.java b/src/main/resources/templates/java/maven_blackbox/solution/src/${packageNameFolder}/input/CommandParser.java new file mode 100644 index 000000000000..fe706430d318 --- /dev/null +++ b/src/main/resources/templates/java/maven_blackbox/solution/src/${packageNameFolder}/input/CommandParser.java @@ -0,0 +1,93 @@ +package ${packageName}.input; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.TimeZone; + +public final class CommandParser { + + private static final String HELP_MESSAGE = + """ + add: adds the given Dates to the list (format: YYYY-MM-DD) + clear: empties the list + help: prints this text + print: prints the list + sort: sorts the list + quit: quits the program"""; + + private CommandParser() { + throw new IllegalCallerException("utility class"); + } + + /** + * Parses a line that was entered via the console. The first word of the + * input determines the command, and possible arguments (if applicable) + * can be appended, separated by whitespaces. + * + * @param inputLine The console input. + * @return The corresponding command. + * @throws InvalidCommandException If the command could not be parsed. + */ + public static Command parseCommand(final String inputLine) + throws InvalidCommandException { + final String[] args = inputLine.strip().split("\\s+"); + + if (args.length == 0) { + throw new InvalidCommandException( + "Expected a command. Use 'help' to show available commands." + ); + } + + final String commandVerb = args[0]; + return switch (commandVerb) { + case "add" -> parseAddCommand(args); + case "clear" -> new Command.ClearCommand(); + case "help" -> new Command.HelpCommand(HELP_MESSAGE); + case "print" -> new Command.PrintCommand(); + case "quit" -> new Command.QuitCommand(); + case "sort" -> new Command.SortCommand(); + default -> throw new InvalidCommandException( + "Unknown command. Use 'help' to show available commands." + ); + }; + } + + private static Command.AddCommand parseAddCommand(final String[] args) + throws InvalidCommandException { + if (args.length < 2) { + throw new UnsupportedOperationException( + "The 'add' command needs some values that can be added." + ); + } + + final List dates = new ArrayList<>(); + + for (int i = 1; i < args.length; ++i) { + final String arg = args[i]; + try { + final Date date = parseDateInput(arg); + dates.add(date); + } catch (ParseException e) { + throw new InvalidCommandException( + "Dates have to follow the format YYYY-MM-DD and must" + + " be valid." + ); + } + } + + return new Command.AddCommand(Collections.unmodifiableList(dates)); + } + + private static Date parseDateInput(final String input) + throws ParseException { + final SimpleDateFormat formatter + = new SimpleDateFormat("yyyy-MM-dd"); + formatter.setTimeZone(TimeZone.getTimeZone("UTC")); + formatter.setLenient(false); + return formatter.parse(input); + } +} diff --git a/src/main/resources/templates/java/maven_blackbox/solution/src/${packageNameFolder}/input/InvalidCommandException.java b/src/main/resources/templates/java/maven_blackbox/solution/src/${packageNameFolder}/input/InvalidCommandException.java new file mode 100644 index 000000000000..043f85fd7aca --- /dev/null +++ b/src/main/resources/templates/java/maven_blackbox/solution/src/${packageNameFolder}/input/InvalidCommandException.java @@ -0,0 +1,20 @@ +package ${packageName}.input; + +import java.io.Serial; + +/** + * Error type to be used to signal that the user entered an invalid command. + */ +public class InvalidCommandException extends Exception { + @Serial + private static final long serialVersionUID = 1L; + + /** + * Builds a new exception that signals that an invalid command was entered. + * + * @param message A message that should be understandable for the user. + */ + public InvalidCommandException(final String message) { + super(message); + } +} diff --git a/src/main/resources/templates/java/solution/src/${packageNameFolder}/Policy.java b/src/main/resources/templates/java/solution/src/${packageNameFolder}/Policy.java index 81fecaa43e06..4f050e6540e7 100755 --- a/src/main/resources/templates/java/solution/src/${packageNameFolder}/Policy.java +++ b/src/main/resources/templates/java/solution/src/${packageNameFolder}/Policy.java @@ -18,10 +18,12 @@ public Policy(Context context) { */ public void configure() { if (this.context.getDates().size() > DATES_SIZE_THRESHOLD) { - System.out.println("More than " + DATES_SIZE_THRESHOLD + " dates, choosing merge sort!"); + System.out.println("More than " + DATES_SIZE_THRESHOLD + + " dates, choosing merge sort!"); this.context.setSortAlgorithm(new MergeSort()); } else { - System.out.println("Less or equal than " + DATES_SIZE_THRESHOLD + " dates. choosing quick sort!"); + System.out.println("Less or equal than " + DATES_SIZE_THRESHOLD + + " dates. choosing quick sort!"); this.context.setSortAlgorithm(new BubbleSort()); } } diff --git a/src/main/resources/templates/java/test/blackbox/projectTemplate/git.ignore.file b/src/main/resources/templates/java/test/blackbox/projectTemplate/git.ignore.file new file mode 100644 index 000000000000..44c742ee253c --- /dev/null +++ b/src/main/resources/templates/java/test/blackbox/projectTemplate/git.ignore.file @@ -0,0 +1,198 @@ +assignment/ + +# Taken from https://github.com/github/gitignore + +### Java +# Compiled class file +*.class + +# Log file +*.log + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +### Eclipse +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# CDT- autotools +.autotools + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Annotation Processing +.apt_generated/ +.apt_generated_test/ + +### JetBrains +.idea + +# Gradle and Maven with auto-import +*.iml +*.ipr + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### Vim +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +*~ +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +### MacOs +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Windows +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### Linux +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* diff --git a/src/main/resources/templates/java/test/blackbox/projectTemplate/pom.xml b/src/main/resources/templates/java/test/blackbox/projectTemplate/pom.xml new file mode 100644 index 000000000000..bad0206c911b --- /dev/null +++ b/src/main/resources/templates/java/test/blackbox/projectTemplate/pom.xml @@ -0,0 +1,109 @@ + + 4.0.0 + ${packageName} + ${exerciseNamePomXml}-Tests + ${packaging} + 1.0 + ${exerciseName} Tests + + UTF-8 + -Dfile.encoding=UTF-8 + + ${project.basedir}/staticCodeAnalysisConfig + false + + + + ${project.basedir}${studentWorkingDirectory} + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 20 + + + + + com.github.spotbugs + spotbugs-maven-plugin + 4.7.3.0 + + + ${analyzeTests} + true + + ${scaConfigDirectory}/spotbugs-exclusions.xml + + + + Default + + Low + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.2.1 + + + com.puppycrawl.tools + checkstyle + 10.8.0 + + + + + ${analyzeTests} + + ${scaConfigDirectory}/checkstyle-configuration.xml + + false + + + + org.apache.maven.plugins + maven-pmd-plugin + 3.20.0 + + + net.sourceforge.pmd + pmd-core + 6.55.0 + + + net.sourceforge.pmd + pmd-java + 6.55.0 + + + + + ${analyzeTests} + + 5 + + + ${scaConfigDirectory}/pmd-configuration.xml + + + 60 + + true + + false + + + + + + diff --git a/src/main/resources/templates/java/test/blackbox/projectTemplate/readme.md b/src/main/resources/templates/java/test/blackbox/projectTemplate/readme.md new file mode 100644 index 000000000000..73823d184e3f --- /dev/null +++ b/src/main/resources/templates/java/test/blackbox/projectTemplate/readme.md @@ -0,0 +1,14 @@ +## Test Repository instructions + +#### Project structure +The `testsuite` directory contains the test definitions used by DejaGnu. +`config/default.exp` contains the base setup shared by all tests. +The actual tests are split up into `public.exp`, `advanced.exp`, and `secret.exp`. + +In the `testfiles/` additional support files can be provided that can be loaded by the program under test. +The `secret/` directory can contain files that should only be readable during execution of the `secret` tests, but not during the other two. + +To make the `secret` tests actually secret (i.e. results not visible to the student), you need to configure the test visibility in the grading configuration of the exercise. + +#### Static Code Analysis +The pom.xml contains dependencies for the execution of static code analysis, if the option is active for this programming exercise. diff --git a/src/main/resources/templates/java/test/blackbox/projectTemplate/testsuite/${packageNameFolder}.tests/advanced.exp b/src/main/resources/templates/java/test/blackbox/projectTemplate/testsuite/${packageNameFolder}.tests/advanced.exp new file mode 100644 index 000000000000..1c5783d4a861 --- /dev/null +++ b/src/main/resources/templates/java/test/blackbox/projectTemplate/testsuite/${packageNameFolder}.tests/advanced.exp @@ -0,0 +1,14 @@ +# Sorting Testdata + +# Tests for advanced functionality +PROGRAM_test {add 2020-03-31} {} +PROGRAM_test {add 2020-03-30} {} +PROGRAM_test {add 2020-04-01} {} +PROGRAM_test {print} {[Tue Mar 31 00:00:00 UTC 2020, Mon Mar 30 00:00:00 UTC 2020, Wed Apr 01 00:00:00 UTC 2020]} +PROGRAM_test {sort} {} +PROGRAM_test {print} {[Mon Mar 30 00:00:00 UTC 2020, Tue Mar 31 00:00:00 UTC 2020, Wed Apr 01 00:00:00 UTC 2020]} + +PROGRAM_test {add 2020-01-31} {} +PROGRAM_test {print} {[Mon Mar 30 00:00:00 UTC 2020, Tue Mar 31 00:00:00 UTC 2020, Wed Apr 01 00:00:00 UTC 2020, Fri Jan 31 00:00:00 UTC 2020]} +PROGRAM_test {sort} {} +PROGRAM_test {print} {[Fri Jan 31 00:00:00 UTC 2020, Mon Mar 30 00:00:00 UTC 2020, Tue Mar 31 00:00:00 UTC 2020, Wed Apr 01 00:00:00 UTC 2020]} diff --git a/src/main/resources/templates/java/test/blackbox/projectTemplate/testsuite/${packageNameFolder}.tests/public.exp b/src/main/resources/templates/java/test/blackbox/projectTemplate/testsuite/${packageNameFolder}.tests/public.exp new file mode 100644 index 000000000000..169e0e8863af --- /dev/null +++ b/src/main/resources/templates/java/test/blackbox/projectTemplate/testsuite/${packageNameFolder}.tests/public.exp @@ -0,0 +1,9 @@ +# Sorting Testdata + +# Public Tests +PROGRAM_test {add 2020-03-31} {} +PROGRAM_test {add 2020-03-30} {} +PROGRAM_test {add 2020-04-01} {} +PROGRAM_test {print} {[Tue Mar 31 00:00:00 UTC 2020, Mon Mar 30 00:00:00 UTC 2020, Wed Apr 01 00:00:00 UTC 2020]} +PROGRAM_test {sort} {} +PROGRAM_test {print} {[Mon Mar 30 00:00:00 UTC 2020, Tue Mar 31 00:00:00 UTC 2020, Wed Apr 01 00:00:00 UTC 2020]} diff --git a/src/main/resources/templates/java/test/blackbox/projectTemplate/testsuite/${packageNameFolder}.tests/secret.exp b/src/main/resources/templates/java/test/blackbox/projectTemplate/testsuite/${packageNameFolder}.tests/secret.exp new file mode 100644 index 000000000000..fa9bdc551612 --- /dev/null +++ b/src/main/resources/templates/java/test/blackbox/projectTemplate/testsuite/${packageNameFolder}.tests/secret.exp @@ -0,0 +1,11 @@ +# Sorting Testdata + +# Secret Tests +PROGRAM_test {print} {[]} +PROGRAM_test {add 2020-03-31} {} +PROGRAM_test {add 2020-03-30} {} +PROGRAM_test {add 2020-04-01} {} +PROGRAM_test {print} {[Tue Mar 31 00:00:00 UTC 2020, Mon Mar 30 00:00:00 UTC 2020, Wed Apr 01 00:00:00 UTC 2020]} + +PROGRAM_test {clear} {} +PROGRAM_test {print} {[]} diff --git a/src/main/resources/templates/java/test/blackbox/projectTemplate/testsuite/config/default.exp b/src/main/resources/templates/java/test/blackbox/projectTemplate/testsuite/config/default.exp new file mode 100644 index 000000000000..264d5500143b --- /dev/null +++ b/src/main/resources/templates/java/test/blackbox/projectTemplate/testsuite/config/default.exp @@ -0,0 +1,225 @@ +# Set timeout, prompt, etc. +# Please adapt this. +set standard_timeout 15 +set startup_timeout 5 +set prompt "sort> " +set answer "" +set exit_cmd "quit" + +# Don't change this +set timeout $standard_timeout +# If we haven't read the right prompt in PROGRAM_start, it doesn't +# make sense to do any other test. +set prompt_error 0 + +# Load a program +proc PROGRAM_load { arg } { + # +} + +# Start program and wait for prompt +proc PROGRAM_start {} { + # it is impossible to use or in start :-( + global standard_timeout + global startup_timeout + global timeout + global prompt + global spawn_id + global prompt_error + + # Startup of the java engine needs to much time. + set timeout $startup_timeout + spawn "java" -cp CLASSPATH "MAIN_CLASS" + + # Check for prompt + expect { + "$prompt" { } + timeout { + set prompt_error 1 + send_user "\nFAIL: start " + send_user "(timeout with no prompt, expected \"$prompt\")\n"; + } + eof { + set prompt_error 1 + send_user "\nFAIL: start " + send_user "(no prompt, expected \"$prompt\")\n" + } + } + set timeout $standard_timeout +} + +# End program +proc PROGRAM_exit {} { + global exit_cmd + global prompt_error + + # Don't continue if we haven't read the right prompt + if $prompt_error { + return + } + + if [catch {send "$exit_cmd\n"}] { + send_user "\n" + fail "could not send text, is the program running?" + return + } + + expect "$exit_cmd\r\n" + + expect { + eof { pass "$exit_cmd" } + -re "." { + send_user "\n" + fail "$exit_cmd (expected end-of-output)" + } + } +} + +# Return version +proc PROGRAM_version {} { + return "unknown" +} + +# Functions to just enter data without expecting answers or errors. +# +# There are two variants: +# 1. ..._enter just expects the data to send +# 2. ..._enter_c has an additional argument: a comment that is printed +# on succes or failure. +# +proc PROGRAM_enter {expr} { + PROGRAM_enter_ "$expr" "" +} + +proc PROGRAM_enter_c {expr comment} { + PROGRAM_enter_ "$expr" "\[$comment\]" +} + +proc PROGRAM_enter_ {expr comment} { + global prompt + global prompt_error + + # Don't continue if we haven't read the right prompt + if $prompt_error { + return + } + + # Quote the command + regsub -all {[].$^()*+?|[]} $expr {\\&} cmd + + # Send expression and wait for echo + if [catch {send -- "$expr\n"}] { + fail "could not send text, is the program running? $comment" + return + } + + # Check result + expect { + -re "$expr\[\r\n\]+$prompt" { + pass "$expr $comment" + } + -re "\[:\].*\[\r\n\]+$prompt" { + fail "$expr (got answer, but none expected) $comment" + } + -re "\[!\].*\[\r\n\]+$prompt" { + send_user "\n" + fail "$expr (got error, but none expected) $comment" + } + -ex "$prompt" { + send_user "\n" + fail "$expr (expected nothing) $comment" + } + timeout { + send_user "\n" + fail "$expr (timeout with no prompt, expected \"$prompt\") $comment"; + expect "$prompt" + } + } +} + +proc PROGRAM_test {expr result} { + PROGRAM_test_ "$expr" "$result" "" +} + +proc PROGRAM_test_c {expr result comment} { + PROGRAM_test_ "$expr" "$result" "\[$comment\]" +} + +proc PROGRAM_test_ {expr result comment} { + global prompt + global answer + global prompt_error + + # Don't continue if we haven't read the right prompt + if $prompt_error { + return + } + + # Send expression and wait for echo + if [catch {send -- "$expr\n"}] { + send_user "\n" + fail "could not send text, is the program running? $comment" + return + } + + # Quote the expected result + regsub -all {[].$^()*+?|[]} $result {\\&} quoted + # Quote the command + regsub -all {[].$^()*+?|[]} $expr {\\&} cmd + + # Check result + expect { + -re "$cmd\[\r\n \]+$answer *$quoted\[ \r\n\]+$prompt" { + pass "$expr $comment" + } + -re "\[:\].*\[\r\n\]+$prompt" { + fail "$expr (got wrong answer, expected \"$result\") $comment" + } + -re "\[!\].*\[\r\n\]+$prompt" { + send_user "\n" + fail "$expr (got error, but expected \"$result\") $comment" + } + -ex "$prompt" { + send_user "\n" + fail "$expr (expected \"$result\") $comment" + } + timeout { + send_user "\n" + fail "$expr (timeout with no prompt, expected \"$prompt\") $comment"; + expect "$prompt" + } + } +} + +proc PROGRAM_write_testfile {path} { + set file_tail [file tail "$path"] + + if [catch {open "$path" "r"} f] { + fail "could not open test file $path" + + } else { + set source_file [open "$path" "r"] + + if [catch {open "tests/$file_tail" "w+"} fh] { + file mkdir "tests" + set fh [open "tests/$file_tail" "w+"] + + while {[gets $source_file line] != -1} { + puts $fh $line + } + close $fh + + } else { + set fh [open "tests/$file_tail" "w+"] + + while {[gets $source_file line] != -1} { + puts $fh $line + } + close $fh + } + } +} + + +# Now go and start the program... +PROGRAM_start diff --git a/src/main/resources/templates/java/test/blackbox/projectTemplate/testsuite/lib/${packageNameFile}.exp b/src/main/resources/templates/java/test/blackbox/projectTemplate/testsuite/lib/${packageNameFile}.exp new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/main/resources/templates/java/test/blackbox/projectTemplate/testsuite/testfiles/public/.gitkeep b/src/main/resources/templates/java/test/blackbox/projectTemplate/testsuite/testfiles/public/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/main/resources/templates/java/test/blackbox/projectTemplate/testsuite/testfiles/secret/.gitkeep b/src/main/resources/templates/java/test/blackbox/projectTemplate/testsuite/testfiles/secret/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/main/resources/templates/jenkins/java/blackbox/regularRuns/pipeline.groovy b/src/main/resources/templates/jenkins/java/blackbox/regularRuns/pipeline.groovy new file mode 100644 index 000000000000..1407c303d3d2 --- /dev/null +++ b/src/main/resources/templates/jenkins/java/blackbox/regularRuns/pipeline.groovy @@ -0,0 +1,246 @@ +MAIN_CLASS = "notFound" // default value, will be replaced in Build stage +OUT_DIR = "target/surefire-reports" +mavenFlags = "-B" + +testfiles_base_path = "./testsuite/testfiles" + +runDejagnuTests = fileExists("./testsuite") +tool = getToolName() +hasSecretTestFiles = secretTestFilesFolderExists() + +dockerImage = '#dockerImage' +dockerFlags = '#dockerArgs' +isStaticCodeAnalysisEnabled = #isStaticCodeAnalysisEnabled + +/** + * Main function called by Jenkins. + */ +void testRunner() { + setup() + build() + + // catchError-block: execute following steps even if the one inside the + // block fails, otherwise whole pipeline is aborted + catchError { + customCheckers() + } + catchError { + if (runDejagnuTests) { + dejagnuTests() + } + } + catchError { + staticCodeAnalysis() + } +} + +/** + * Runs special tasks before the actual tests can begin. + *

+ * E.g. container creation, setting docker flags. + */ +private void setup() { + // special jobs to run only for the solution repository + if ("${env.JOB_NAME}" ==~ /.+-SOLUTION$/) { + dockerFlags += ' -v artemis_blackbox_maven-cache:/maven_cache' + } else { + dockerFlags += ' -v artemis_blackbox_maven-cache:/maven_cache:ro' + + // if not solution repo, disallow network access from containers + dockerFlags += ' --network none' + mavenFlags += ' --offline' + } +} + +/** + * Builds the student code and tries to find a main method. + */ +private void build() { + stage('Build') { + docker.image(dockerImage).inside(dockerFlags) { c -> + sh "mvn ${mavenFlags} clean compile" + setMainClass() + } + } +} + +/** + * Runs the custom {@code pipeline-helper.jar} checkers. + */ +private void customCheckers() { + stage('Checkers') { + docker.image(dockerImage).inside(dockerFlags) { c -> + // all java files in the assignment folder should have maximal line length 80 + sh 'pipeline-helper -l 80 assignment/ java' + // checks that the file exists and is not empty for non gui programs + if (runDejagnuTests) { + sh 'pipeline-helper -e assignment/Tests.txt' + } + } + } +} + +/** + * Runs Dejagnu scripts. + */ +private void dejagnuTests() { + stage('Secret Tests') { + docker.image(dockerImage).inside(dockerFlags) { c -> + applyExpectScriptReplacements() + runDejagnuTestStep("secret", 60) + removeSecretFiles() + } + } + stage('Public+Advanced Tests') { + docker.image(dockerImage).inside(dockerFlags) { c -> + applyExpectScriptReplacements() + + runDejagnuTestStep("public", 60) + runDejagnuTestStep("advanced", 60) + } + } +} + +/** + * Runs a single Dejagnu test step. + * + * @param step The test step name. Runs the test file {@code step.exp}. + * @param timeoutSeconds A number of seconds after which Jenkins will kill the dejagnu process. + */ +private void runDejagnuTestStep(String step, int timeoutSeconds) { + catchError() { + timeout(time: timeoutSeconds, unit: 'SECONDS') { + sh """ + cd testsuite || exit + rm ${tool}.log || true + runtest --tool ${tool} ${step}.exp || true + """ + } + } + sh("""pipeline-helper -o customFeedbacks -d "dejagnu[${step}]" testsuite/${tool}.log""") +} + +/** + * Extracts the Dejagnu tool name from the folder names. + *

+ * E.g., for a folder {@code testsuite/gcd.tests/} the tool name is {@code gcd}. + * + * @return The Dejagnu tool name. + */ +private String getToolName() { + // Java Files API gets blocked by Jenkins sandbox + + return sh( + script: """find testsuite -name "*.tests" -type d -printf "%f" | sed 's#.tests\$##'""", + returnStdout: true + ).trim() +} + +/** + * Extracts the name of the student’s Java class that contains the main method + * and stores it in {@code MAIN_CLASS}. + *

+ * Aborts the pipeline if no main class could be found. + */ +private void setMainClass() { + def main_checker_lines = sh( + script: "pipeline-helper -m assignment/", + returnStdout: true + ).tokenize('\n') + + // first line of output is class name with main method + // second one a status message that the checker ran successfully/failed + if (main_checker_lines.size() == 2) { + MAIN_CLASS = main_checker_lines.get(0) + } else { + // no main method found: let this stage fail, this aborts all further stages + error + } +} + +/** + * Replaces variables of the expect scripts with task specific values. + */ +private void applyExpectScriptReplacements() { + sh """ + sed -i "s#CLASSPATH#../target/classes#" testsuite/config/default.exp + sed -i "s#MAIN_CLASS#${MAIN_CLASS}#" testsuite/config/default.exp + + sed -i "s#TESTFILES_DIRECTORY#../${testfiles_base_path}#" testsuite/${tool}.tests/*.exp + """ +} + +/** + * Removes all secret files from the working directory. + *

+ * Without the special Docker volume mounted then students can no longer access + * those files during the public tests. + */ +private void removeSecretFiles() { + secretExp = "testsuite/${tool}.tests/secret.exp" + if (fileExists(secretExp)) { + sh("rm ${secretExp}") + } + + if (hasSecretTestFiles) { + sh("rm -rf ${testfiles_base_path}/secret/") + } +} + +/** + * Checks if a folder with secret Dejagnu test files exists. + * + * @return True, if such a folder exists. + */ +private boolean secretTestFilesFolderExists() { + return fileExists("${testfiles_base_path}/secret") +} + +/** + * Runs the static code analysis + */ +private void staticCodeAnalysis() { + if (!isStaticCodeAnalysisEnabled) { + return + } + + stage("StaticCodeAnalysis") { + docker.image(dockerImage).inside(dockerFlags) { c -> + /* + sh """ + mvn -B spotbugs:spotbugs checkstyle:checkstyle pmd:pmd pmd:cpd + mkdir -p staticCodeAnalysisReports + cp target/spotbugsXml.xml staticCodeAnalysisReports + cp target/checkstyle-result.xml staticCodeAnalysisReports + cp target/pmd.xml staticCodeAnalysisReports + cp target/cpd.xml staticCodeAnalysisReports + """ + */ + + sh """ + mvn ${mavenFlags} checkstyle:checkstyle + mkdir -p staticCodeAnalysisReports + cp target/checkstyle-result.xml staticCodeAnalysisReports + """ + } + } +} + +/** + * Script of the post build tasks aggregating all JUnit files in $WORKSPACE/results. + * + * Called by Jenkins. + */ +void postBuildTasks() { + // we do not actually have any JUnit-XMLs => no action needed + /* + sh ''' + rm -rf results + mkdir results + cp target/surefire-reports/*.xml $WORKSPACE/results/ || true + sed -i 's/[^[:print:]\t]/�/g' $WORKSPACE/results/*.xml || true + ''' + */ +} + +return this diff --git a/src/main/webapp/app/entities/programming-exercise.model.ts b/src/main/webapp/app/entities/programming-exercise.model.ts index bc25c1dd83af..13174db12cf9 100644 --- a/src/main/webapp/app/entities/programming-exercise.model.ts +++ b/src/main/webapp/app/entities/programming-exercise.model.ts @@ -26,6 +26,7 @@ export enum ProgrammingLanguage { export enum ProjectType { MAVEN_MAVEN = 'MAVEN_MAVEN', PLAIN_MAVEN = 'PLAIN_MAVEN', + MAVEN_BLACKBOX = 'MAVEN_BLACKBOX', PLAIN_GRADLE = 'PLAIN_GRADLE', GRADLE_GRADLE = 'GRADLE_GRADLE', PLAIN = 'PLAIN', diff --git a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.ts b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.ts index 25a2ed74c9c5..3c7750282d31 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.ts @@ -84,6 +84,9 @@ export class ProgrammingExerciseUpdateComponent implements OnInit { // with the restriction to a-z,A-Z,_ as "Java letter" and 0-9 as digits due to JavaScript/Browser Unicode character class limitations packageNamePatternForJavaKotlin = '^(?!.*(?:\\.|^)(?:abstract|continue|for|new|switch|assert|default|if|package|synchronized|boolean|do|goto|private|this|break|double|implements|protected|throw|byte|else|import|public|throws|case|enum|instanceof|return|transient|catch|extends|int|short|try|char|final|interface|static|void|class|finally|long|strictfp|volatile|const|float|native|super|while|_|true|false|null)(?:\\.|$))[A-Z_a-z][0-9A-Z_a-z]*(?:\\.[A-Z_a-z][0-9A-Z_a-z]*)*$'; + // No dots allowed for the blackbox project type, because the folder naming works slightly different here. + packageNamePatternForJavaBlackbox = + '^(?!.*(?:\\.|^)(?:abstract|continue|for|new|switch|assert|default|if|package|synchronized|boolean|do|goto|private|this|break|double|implements|protected|throw|byte|else|import|public|throws|case|enum|instanceof|return|transient|catch|extends|int|short|try|char|final|interface|static|void|class|finally|long|strictfp|volatile|const|float|native|super|while|_|true|false|null)(?:\\.|$))[A-Z_a-z][0-9A-Z_a-z]*$'; // Swift package name Regex derived from (https://docs.swift.org/swift-book/ReferenceManual/LexicalStructure.html#ID412), // with the restriction to a-z,A-Z as "Swift letter" and 0-9 as digits where no separators are allowed appNamePatternForSwift = @@ -314,8 +317,15 @@ export class ProgrammingExerciseUpdateComponent implements OnInit { // update the project types for java programming exercises according to whether dependencies should be included if (this.programmingExercise.programmingLanguage === ProgrammingLanguage.JAVA) { - if (type === ProjectType.PLAIN_MAVEN || type === ProjectType.MAVEN_MAVEN) { + if (type == ProjectType.MAVEN_BLACKBOX) { + this.selectedProjectTypeValue = ProjectType.MAVEN_BLACKBOX; + this.programmingExercise.projectType = ProjectType.MAVEN_BLACKBOX; + this.sequentialTestRunsAllowed = false; + this.testwiseCoverageAnalysisSupported = false; + } else if (type === ProjectType.PLAIN_MAVEN || type === ProjectType.MAVEN_MAVEN) { this.selectedProjectTypeValue = ProjectType.PLAIN_MAVEN; + this.sequentialTestRunsAllowed = true; + this.testwiseCoverageAnalysisSupported = true; if (this.withDependenciesValue) { this.programmingExercise.projectType = ProjectType.MAVEN_MAVEN; } else { @@ -323,6 +333,8 @@ export class ProgrammingExerciseUpdateComponent implements OnInit { } } else { this.selectedProjectTypeValue = ProjectType.PLAIN_GRADLE; + this.sequentialTestRunsAllowed = true; + this.testwiseCoverageAnalysisSupported = true; if (this.withDependenciesValue) { this.programmingExercise.projectType = ProjectType.GRADLE_GRADLE; } else { @@ -670,12 +682,13 @@ export class ProgrammingExerciseUpdateComponent implements OnInit { * Sets the regex pattern for the package name for the selected programming language. * * @param language to choose from + * @param useBlackboxPattern whether to allow points in the regex */ - setPackageNamePattern(language: ProgrammingLanguage) { + setPackageNamePattern(language: ProgrammingLanguage, useBlackboxPattern = false) { if (language === ProgrammingLanguage.SWIFT) { this.packageNamePattern = this.appNamePatternForSwift; } else { - this.packageNamePattern = this.packageNamePatternForJavaKotlin; + this.packageNamePattern = useBlackboxPattern ? this.packageNamePatternForJavaBlackbox : this.packageNamePatternForJavaKotlin; } } @@ -695,6 +708,8 @@ export class ProgrammingExerciseUpdateComponent implements OnInit { } } this.selectedProjectType = projectType; + const useBlackboxPattern = projectType === ProjectType.MAVEN_BLACKBOX; + this.setPackageNamePattern(this.programmingExercise.programmingLanguage!, useBlackboxPattern); return projectType; } @@ -907,12 +922,26 @@ export class ProgrammingExerciseUpdateComponent implements OnInit { }); } else { const patternMatchJavaKotlin: RegExpMatchArray | null = this.programmingExercise.packageName.match(this.packageNamePatternForJavaKotlin); + const patternMatchJavaBlackbox: RegExpMatchArray | null = this.programmingExercise.packageName.match(this.packageNamePatternForJavaBlackbox); const patternMatchSwift: RegExpMatchArray | null = this.programmingExercise.packageName.match(this.appNamePatternForSwift); - if (this.programmingExercise.programmingLanguage === ProgrammingLanguage.JAVA && (patternMatchJavaKotlin === null || patternMatchJavaKotlin.length === 0)) { - validationErrorReasons.push({ - translateKey: 'artemisApp.exercise.form.packageName.pattern.JAVA', - translateValues: {}, - }); + const projectTypeDependentPatternMatch: RegExpMatchArray | null = + this.programmingExercise.projectType === ProjectType.MAVEN_BLACKBOX ? patternMatchJavaBlackbox : patternMatchJavaKotlin; + + if ( + this.programmingExercise.programmingLanguage === ProgrammingLanguage.JAVA && + (projectTypeDependentPatternMatch === null || projectTypeDependentPatternMatch.length === 0) + ) { + if (this.programmingExercise.projectType === ProjectType.MAVEN_BLACKBOX) { + validationErrorReasons.push({ + translateKey: 'artemisApp.exercise.form.packageName.pattern.JAVA_BLACKBOX', + translateValues: {}, + }); + } else { + validationErrorReasons.push({ + translateKey: 'artemisApp.exercise.form.packageName.pattern.JAVA', + translateValues: {}, + }); + } } else if (this.programmingExercise.programmingLanguage === ProgrammingLanguage.KOTLIN && (patternMatchJavaKotlin === null || patternMatchJavaKotlin.length === 0)) { validationErrorReasons.push({ translateKey: 'artemisApp.exercise.form.packageName.pattern.KOTLIN', diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-language.component.html b/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-language.component.html index d5bf29aaccb1..b0be991447ec 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-language.component.html +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-language.component.html @@ -40,7 +40,8 @@

-
+
+
diff --git a/src/main/webapp/i18n/de/exercise.json b/src/main/webapp/i18n/de/exercise.json index 0c27cd8f2323..410728f5d4e9 100644 --- a/src/main/webapp/i18n/de/exercise.json +++ b/src/main/webapp/i18n/de/exercise.json @@ -218,7 +218,8 @@ "pattern": { "JAVA": "Der Package-Name muss aus einem oder mehreren gültigen Java-Bezeichnern bestehen, die mit '.' getrennt sind, z.B. \"net.java\"!", "KOTLIN": "Der Package-Name muss aus einem oder mehreren gültigen Kotlin-Bezeichnern bestehen, die mit '.' getrennt sind, z.B. \"net.kotlin\"!", - "SWIFT": "Der Package-Name muss aus einem oder mehreren gültigen Swift-Bezeichnern bestehen, die nicht voneinander separiert sind, z.B. \"SwiftEx\"!" + "SWIFT": "Der Package-Name muss aus einem oder mehreren gültigen Swift-Bezeichnern bestehen, die nicht voneinander separiert sind, z.B. \"SwiftEx\"!", + "JAVA_BLACKBOX": "Der Paketname muss ein gültiger Java-Bezeicher sein. Zusätzlich sind Trennpunkte im Paketnamen für den DejaGnu Aufgabentyp nicht erlaubt." } }, "points": { diff --git a/src/main/webapp/i18n/de/programmingExercise.json b/src/main/webapp/i18n/de/programmingExercise.json index b9fc060d6d75..7b14978fc28e 100644 --- a/src/main/webapp/i18n/de/programmingExercise.json +++ b/src/main/webapp/i18n/de/programmingExercise.json @@ -531,6 +531,7 @@ "projectTypes": { "PLAIN_GRADLE": "Gradle", "PLAIN_MAVEN": "Maven", + "MAVEN_BLACKBOX": "DejaGnu", "PLAIN": "Plain", "XCODE": "Xcode", "FACT": "FACT", diff --git a/src/main/webapp/i18n/en/exercise.json b/src/main/webapp/i18n/en/exercise.json index c7e72292427a..f09fa7cfeebc 100644 --- a/src/main/webapp/i18n/en/exercise.json +++ b/src/main/webapp/i18n/en/exercise.json @@ -218,7 +218,8 @@ "pattern": { "JAVA": "The package name must consist of one or more valid Java identifiers separated by '.', e.g. \"net.java\"!", "KOTLIN": "The package name must consist of one or more valid Kotlin identifiers separated by '.', e.g. \"net.kotlin\"!", - "SWIFT": "The package name must consist of one or more valid Swift identifiers without any separator, e.g. \"SwiftEx\"!" + "SWIFT": "The package name must consist of one or more valid Swift identifiers without any separator, e.g. \"SwiftEx\"!", + "JAVA_BLACKBOX": "The package name must be a valid Java identifier. In addition, no dots are allowed in the package name of DejaGnu projects" } }, "points": { diff --git a/src/main/webapp/i18n/en/programmingExercise.json b/src/main/webapp/i18n/en/programmingExercise.json index ecc84fce027b..fae5c6f2c45a 100644 --- a/src/main/webapp/i18n/en/programmingExercise.json +++ b/src/main/webapp/i18n/en/programmingExercise.json @@ -526,6 +526,7 @@ "projectTypes": { "PLAIN_GRADLE": "Gradle", "PLAIN_MAVEN": "Maven", + "MAVEN_BLACKBOX": "DejaGnu", "PLAIN": "Plain", "XCODE": "Xcode", "FACT": "FACT", diff --git a/src/test/javascript/spec/component/programming-exercise/programming-exercise-update.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/programming-exercise-update.component.spec.ts index 8162decd1fd7..2e5de9756348 100644 --- a/src/test/javascript/spec/component/programming-exercise/programming-exercise-update.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/programming-exercise-update.component.spec.ts @@ -791,28 +791,50 @@ describe('ProgrammingExercise Management Update Component', () => { }); }); - it('should disable checkboxes for certain options of existing exercise', fakeAsync(() => { - const entity = new ProgrammingExercise(new Course(), undefined); - entity.id = 123; - entity.channelName = 'notificationText'; - comp.programmingExercise = entity; - comp.programmingExercise.course = course; - comp.programmingExercise.programmingLanguage = ProgrammingLanguage.JAVA; + describe('disable features based on selected language and project type', () => { + beforeEach(() => { + const entity = new ProgrammingExercise(new Course(), undefined); + entity.id = 123; + entity.channelName = 'notificationText'; + comp.programmingExercise = entity; + comp.programmingExercise.course = course; + comp.programmingExercise.programmingLanguage = ProgrammingLanguage.JAVA; - const route = TestBed.inject(ActivatedRoute); - route.params = of({ courseId }); - route.url = of([{ path: 'edit' } as UrlSegment]); - route.data = of({ programmingExercise: entity }); + const route = TestBed.inject(ActivatedRoute); + route.params = of({ courseId }); + route.url = of([{ path: 'edit' } as UrlSegment]); + route.data = of({ programmingExercise: comp.programmingExercise }); + }); - const getFeaturesStub = jest.spyOn(programmingExerciseFeatureService, 'getProgrammingLanguageFeature'); - getFeaturesStub.mockImplementation((language: ProgrammingLanguage) => getProgrammingLanguageFeature(language)); + it('should disable checkboxes for certain options of existing exercise', fakeAsync(() => { + const getFeaturesStub = jest.spyOn(programmingExerciseFeatureService, 'getProgrammingLanguageFeature'); + getFeaturesStub.mockImplementation((language: ProgrammingLanguage) => getProgrammingLanguageFeature(language)); - fixture.detectChanges(); - tick(); + fixture.detectChanges(); + tick(); - expect(comp.programmingExercise.staticCodeAnalysisEnabled).toBeFalse(); - expect(comp.programmingExercise.testwiseCoverageEnabled).toBeFalse(); - })); + expect(comp.programmingExercise.staticCodeAnalysisEnabled).toBeFalse(); + expect(comp.programmingExercise.testwiseCoverageEnabled).toBeFalse(); + })); + + it('should disable options for java dejagnu project type and re-enable them after changing back to maven or gradle', fakeAsync(() => { + comp.selectedProjectType = ProjectType.MAVEN_BLACKBOX; + expect(comp.sequentialTestRunsAllowed).toBeFalse(); + expect(comp.testwiseCoverageAnalysisSupported).toBeFalse(); + + comp.selectedProjectType = ProjectType.MAVEN_MAVEN; + expect(comp.sequentialTestRunsAllowed).toBeTrue(); + expect(comp.testwiseCoverageAnalysisSupported).toBeTrue(); + + comp.selectedProjectType = ProjectType.MAVEN_BLACKBOX; + expect(comp.sequentialTestRunsAllowed).toBeFalse(); + expect(comp.testwiseCoverageAnalysisSupported).toBeFalse(); + + comp.selectedProjectType = ProjectType.GRADLE_GRADLE; + expect(comp.sequentialTestRunsAllowed).toBeTrue(); + expect(comp.testwiseCoverageAnalysisSupported).toBeTrue(); + })); + }); it('should toggle the wizard mode', fakeAsync(() => { const route = TestBed.inject(ActivatedRoute);