diff --git a/build.gradle b/build.gradle index 545f196e5bbb..d0e176d5c0f2 100644 --- a/build.gradle +++ b/build.gradle @@ -242,6 +242,7 @@ dependencies { implementation "de.jplag:c:${jplag_version}" implementation "de.jplag:cpp:${jplag_version}" implementation "de.jplag:csharp:${jplag_version}" + implementation "de.jplag:golang:${jplag_version}" implementation "de.jplag:java:${jplag_version}" implementation "de.jplag:javascript:${jplag_version}" implementation "de.jplag:kotlin:${jplag_version}" diff --git a/docs/user/exercises/programming-exercise-features.inc b/docs/user/exercises/programming-exercise-features.inc index edf413fd5d99..18434aaed70f 100644 --- a/docs/user/exercises/programming-exercise-features.inc +++ b/docs/user/exercises/programming-exercise-features.inc @@ -45,6 +45,8 @@ Instructors can still use those templates to generate programming exercises and +----------------------+----------+---------+ | C# | yes | yes | +----------------------+----------+---------+ + | Go | yes | yes | + +----------------------+----------+---------+ - Not all ``templates`` support the same feature set and supported features can also change depending on the continuous integration system setup. Depending on the feature set, some options might not be available during the creation of the programming exercise. @@ -87,6 +89,8 @@ Instructors can still use those templates to generate programming exercises and +----------------------+----------------------+----------------------+---------------------+--------------+------------------------------------------+------------------------------+----------------------------+------------------------+ | C# | no | no | yes | no | n/a | no | no | L: yes, J: no | +----------------------+----------------------+----------------------+---------------------+--------------+------------------------------------------+------------------------------+----------------------------+------------------------+ + | Go | no | no | yes | yes | n/a | no | no | L: yes, J: no | + +----------------------+----------------------+----------------------+---------------------+--------------+------------------------------------------+------------------------------+----------------------------+------------------------+ - *Sequential Test Runs*: ``Artemis`` can generate a build plan which first executes structural and then behavioral tests. This feature can help students to better concentrate on the immediate challenge at hand. - *Static Code Analysis*: ``Artemis`` can generate a build plan which additionally executes static code analysis tools. diff --git a/src/main/java/de/tum/cit/aet/artemis/plagiarism/service/ProgrammingPlagiarismDetectionService.java b/src/main/java/de/tum/cit/aet/artemis/plagiarism/service/ProgrammingPlagiarismDetectionService.java index 6760538078c7..a1b16c64ade3 100644 --- a/src/main/java/de/tum/cit/aet/artemis/plagiarism/service/ProgrammingPlagiarismDetectionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/plagiarism/service/ProgrammingPlagiarismDetectionService.java @@ -33,6 +33,7 @@ import de.jplag.cpp.CPPLanguage; import de.jplag.csharp.CSharpLanguage; import de.jplag.exceptions.ExitException; +import de.jplag.golang.GoLanguage; import de.jplag.java.JavaLanguage; import de.jplag.javascript.JavaScriptLanguage; import de.jplag.kotlin.KotlinLanguage; @@ -317,6 +318,7 @@ private Language getJPlagProgrammingLanguage(ProgrammingExercise programmingExer case C -> new CLanguage(); case C_PLUS_PLUS -> new CPPLanguage(); case C_SHARP -> new CSharpLanguage(); + case GO -> new GoLanguage(); case JAVA -> new JavaLanguage(); case JAVASCRIPT -> new JavaScriptLanguage(); case KOTLIN -> new KotlinLanguage(); @@ -325,7 +327,7 @@ private Language getJPlagProgrammingLanguage(ProgrammingExercise programmingExer case RUST -> new RustLanguage(); case SWIFT -> new SwiftLanguage(); case TYPESCRIPT -> new TypeScriptLanguage(); - case EMPTY, PHP, DART, HASKELL, ASSEMBLER, OCAML, SQL, GO, MATLAB, BASH, VHDL, RUBY, POWERSHELL, ADA -> throw new BadRequestAlertException( + case EMPTY, PHP, DART, HASKELL, ASSEMBLER, OCAML, SQL, MATLAB, BASH, VHDL, RUBY, POWERSHELL, ADA -> throw new BadRequestAlertException( "Programming language " + programmingExercise.getProgrammingLanguage() + " not supported for plagiarism check.", "ProgrammingExercise", "notSupported"); }; } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingLanguage.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingLanguage.java index a2a11cfc36d8..fa3aa6d65577 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingLanguage.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingLanguage.java @@ -42,6 +42,7 @@ public enum ProgrammingLanguage { C, C_PLUS_PLUS, C_SHARP, + GO, HASKELL, JAVA, JAVASCRIPT, diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseRepositoryService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseRepositoryService.java index 978508ce5c95..43523ec566f1 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseRepositoryService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseRepositoryService.java @@ -674,6 +674,7 @@ void replacePlaceholders(final ProgrammingExercise programmingExercise, final Re replacements.put(PACKAGE_NAME_PLACEHOLDER, programmingExercise.getPackageName()); } case SWIFT -> replaceSwiftPlaceholders(replacements, programmingExercise, repository); + case GO -> replacements.put(PACKAGE_NAME_PLACEHOLDER, programmingExercise.getPackageName()); default -> { // no special package name replacements needed for other programming languages } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseService.java index c60fbc5b9d34..d85af7904c4c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseService.java @@ -107,18 +107,27 @@ public class ProgrammingExerciseService { * (https://docs.oracle.com/javase/specs/jls/se14/html/jls-7.html#jls-7.4.1) * with the restriction to a-z,A-Z,_ as "Java letter" and 0-9 as digits due to JavaScript/Browser Unicode character class limitations */ - private static final String PACKAGE_NAME_REGEX = "^(?!.*(?:\\.|^)(?: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]\\w*(?:\\.[A-Z_a-z]\\w*)*$"; + private static final String PACKAGE_NAME_REGEX_FOR_JAVA_KOTLIN = "^(?!.*(?:\\.|^)(?: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]\\w*(?:\\.[A-Z_a-z]\\w*)*$"; /** * 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 */ - private static final String SWIFT_PACKAGE_NAME_REGEX = "^(?!(?:associatedtype|class|deinit|enum|extension|fileprivate|func|import|init|inout|internal|let|open|operator|private|protocol|public|rethrows|static|struct|subscript|typealias|var|break|case|continue|default|defer|do|else|fallthrough|for|guard|if|in|repeat|return|switch|where|while|as|Any|catch|false|is|nil|super|self|Self|throw|throws|true|try|_|[sS]wift)$)[A-Za-z][\\dA-Za-z]*$"; + private static final String PACKAGE_NAME_REGEX_FOR_SWIFT = "^(?!(?:associatedtype|class|deinit|enum|extension|fileprivate|func|import|init|inout|internal|let|open|operator|private|protocol|public|rethrows|static|struct|subscript|typealias|var|break|case|continue|default|defer|do|else|fallthrough|for|guard|if|in|repeat|return|switch|where|while|as|Any|catch|false|is|nil|super|self|Self|throw|throws|true|try|_|[sS]wift)$)[A-Za-z][\\dA-Za-z]*$"; - private final Pattern packageNamePattern = Pattern.compile(PACKAGE_NAME_REGEX); + /** + * Go package name Regex derived from The Go Programming Language Specification limited to ASCII. Package names are + * identifiers. + * They allow letters, digits and underscore. They cannot start with a digit. The package name cannot be a keyword or "_". + */ + private static final String PACKAGE_NAME_REGEX_FOR_GO = "^(?!(?:break|default|func|interface|select|case|defer|go|map|struct|chan|else|goto|package|switch|const|fallthrough|if|range|type|continue|for|import|return|var|_)$)[A-Za-z_][A-Za-z0-9_]*$"; + + private static final Pattern PACKAGE_NAME_PATTERN_FOR_JAVA_KOTLIN = Pattern.compile(PACKAGE_NAME_REGEX_FOR_JAVA_KOTLIN); - private final Pattern packageNamePatternForSwift = Pattern.compile(SWIFT_PACKAGE_NAME_REGEX); + private static final Pattern PACKAGE_NAME_PATTERN_FOR_SWIFT = Pattern.compile(PACKAGE_NAME_REGEX_FOR_SWIFT); + + private static final Pattern PACKAGE_NAME_PATTERN_FOR_GO = Pattern.compile(PACKAGE_NAME_REGEX_FOR_GO); private static final Logger log = LoggerFactory.getLogger(ProgrammingExerciseService.class); @@ -434,12 +443,12 @@ private void validatePackageName(ProgrammingExercise programmingExercise, Progra } // Check if package name matches regex - Matcher packageNameMatcher; - switch (programmingExercise.getProgrammingLanguage()) { - case JAVA, KOTLIN -> packageNameMatcher = packageNamePattern.matcher(programmingExercise.getPackageName()); - case SWIFT -> packageNameMatcher = packageNamePatternForSwift.matcher(programmingExercise.getPackageName()); + Matcher packageNameMatcher = switch (programmingExercise.getProgrammingLanguage()) { + case JAVA, KOTLIN -> PACKAGE_NAME_PATTERN_FOR_JAVA_KOTLIN.matcher(programmingExercise.getPackageName()); + case SWIFT -> PACKAGE_NAME_PATTERN_FOR_SWIFT.matcher(programmingExercise.getPackageName()); + case GO -> PACKAGE_NAME_PATTERN_FOR_GO.matcher(programmingExercise.getPackageName()); default -> throw new IllegalArgumentException("Programming language not supported"); - } + }; if (!packageNameMatcher.matches()) { throw new BadRequestAlertException("The package name is invalid", "Exercise", "packagenameInvalid"); } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/TemplateUpgradePolicyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/TemplateUpgradePolicyService.java index 2ec466bb943c..a60e8be4b050 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/TemplateUpgradePolicyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/TemplateUpgradePolicyService.java @@ -32,8 +32,8 @@ public TemplateUpgradePolicyService(JavaTemplateUpgradeService javaRepositoryUpg public TemplateUpgradeService getUpgradeService(ProgrammingLanguage programmingLanguage) { return switch (programmingLanguage) { case JAVA -> javaRepositoryUpgradeService; - case KOTLIN, PYTHON, C, HASKELL, VHDL, ASSEMBLER, SWIFT, OCAML, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS, TYPESCRIPT, C_SHARP -> defaultRepositoryUpgradeService; - case SQL, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + programmingLanguage); + case KOTLIN, PYTHON, C, HASKELL, VHDL, ASSEMBLER, SWIFT, OCAML, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS, TYPESCRIPT, C_SHARP, GO -> defaultRepositoryUpgradeService; + case SQL, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + programmingLanguage); }; } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ci/ContinuousIntegrationService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ci/ContinuousIntegrationService.java index 6aaeb9602110..9fb2ac0b6f56 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ci/ContinuousIntegrationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ci/ContinuousIntegrationService.java @@ -219,8 +219,8 @@ enum RepositoryCheckoutPath implements CustomizableCheckoutPath { @Override public String forProgrammingLanguage(ProgrammingLanguage language) { return switch (language) { - case JAVA, PYTHON, C, HASKELL, KOTLIN, VHDL, ASSEMBLER, SWIFT, OCAML, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS, TYPESCRIPT, C_SHARP -> "assignment"; - case SQL, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + language); + case JAVA, PYTHON, C, HASKELL, KOTLIN, VHDL, ASSEMBLER, SWIFT, OCAML, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS, TYPESCRIPT, C_SHARP, GO -> "assignment"; + case SQL, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + language); }; } }, @@ -230,8 +230,8 @@ public String forProgrammingLanguage(ProgrammingLanguage language) { public String forProgrammingLanguage(ProgrammingLanguage language) { return switch (language) { case JAVA, PYTHON, HASKELL, KOTLIN, SWIFT, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS, TYPESCRIPT -> ""; - case C, VHDL, ASSEMBLER, OCAML, C_SHARP -> "tests"; - case SQL, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + language); + case C, VHDL, ASSEMBLER, OCAML, C_SHARP, GO -> "tests"; + case SQL, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + language); }; } }, diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/JenkinsProgrammingLanguageFeatureService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/JenkinsProgrammingLanguageFeatureService.java index 0a35810528af..cca2995cdba0 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/JenkinsProgrammingLanguageFeatureService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/JenkinsProgrammingLanguageFeatureService.java @@ -4,6 +4,7 @@ import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.C_PLUS_PLUS; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.C_SHARP; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.EMPTY; +import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.GO; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.HASKELL; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.JAVA; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.JAVASCRIPT; @@ -40,6 +41,7 @@ public JenkinsProgrammingLanguageFeatureService() { programmingLanguageFeatures.put(C, new ProgrammingLanguageFeature(C, false, false, true, false, false, List.of(FACT, GCC), false, false)); programmingLanguageFeatures.put(C_PLUS_PLUS, new ProgrammingLanguageFeature(C_PLUS_PLUS, false, false, true, false, false, List.of(), false, false)); programmingLanguageFeatures.put(C_SHARP, new ProgrammingLanguageFeature(C_SHARP, false, false, true, false, false, List.of(), false, false)); + programmingLanguageFeatures.put(GO, new ProgrammingLanguageFeature(GO, false, false, true, true, false, List.of(), false, false)); programmingLanguageFeatures.put(HASKELL, new ProgrammingLanguageFeature(HASKELL, false, false, false, false, true, List.of(), false, 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, false)); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/build_plan/JenkinsBuildPlanService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/build_plan/JenkinsBuildPlanService.java index 3a1440d72e7f..22e9dd1b0233 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/build_plan/JenkinsBuildPlanService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/build_plan/JenkinsBuildPlanService.java @@ -184,8 +184,8 @@ private JenkinsXmlConfigBuilder builderFor(ProgrammingLanguage programmingLangua throw new UnsupportedOperationException("Xcode templates are not available for Jenkins."); } return switch (programmingLanguage) { - case JAVA, KOTLIN, PYTHON, C, HASKELL, SWIFT, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS, TYPESCRIPT, C_SHARP -> jenkinsBuildPlanCreator; - case VHDL, ASSEMBLER, OCAML, SQL, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> + case JAVA, KOTLIN, PYTHON, C, HASKELL, SWIFT, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS, TYPESCRIPT, C_SHARP, GO -> jenkinsBuildPlanCreator; + case VHDL, ASSEMBLER, OCAML, SQL, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException(programmingLanguage + " templates are not available for Jenkins."); }; } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIProgrammingLanguageFeatureService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIProgrammingLanguageFeatureService.java index 54126903a6d0..d764193795c9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIProgrammingLanguageFeatureService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIProgrammingLanguageFeatureService.java @@ -6,6 +6,7 @@ import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.C_PLUS_PLUS; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.C_SHARP; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.EMPTY; +import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.GO; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.HASKELL; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.JAVA; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.JAVASCRIPT; @@ -47,6 +48,7 @@ public LocalCIProgrammingLanguageFeatureService() { programmingLanguageFeatures.put(C, new ProgrammingLanguageFeature(C, false, true, true, false, false, List.of(FACT, GCC), false, true)); programmingLanguageFeatures.put(C_PLUS_PLUS, new ProgrammingLanguageFeature(C_PLUS_PLUS, false, false, true, false, false, List.of(), false, true)); programmingLanguageFeatures.put(C_SHARP, new ProgrammingLanguageFeature(C_SHARP, false, false, true, false, false, List.of(), false, true)); + programmingLanguageFeatures.put(GO, new ProgrammingLanguageFeature(GO, false, false, true, true, false, List.of(), false, true)); programmingLanguageFeatures.put(HASKELL, new ProgrammingLanguageFeature(HASKELL, true, false, false, false, true, List.of(), false, true)); programmingLanguageFeatures.put(JAVA, new ProgrammingLanguageFeature(JAVA, true, true, true, true, false, List.of(PLAIN_GRADLE, GRADLE_GRADLE, PLAIN_MAVEN, MAVEN_MAVEN), false, true)); diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml index 631e07fbcfae..5a2441c2f919 100644 --- a/src/main/resources/config/application.yml +++ b/src/main/resources/config/application.yml @@ -99,6 +99,8 @@ artemis: default: "ghcr.io/ls1intum/artemis-csharp-docker:v1.0.0" typescript: default: "ghcr.io/ls1intum/artemis-javascript-docker:v1.0.0" + go: + default: "ghcr.io/ls1intum/artemis-go-docker:v1.0.0" # The following properties are used to configure the Artemis build agent. # The build agent is responsible for executing the buildJob to test student submissions. diff --git a/src/main/resources/templates/aeolus/go/default.sh b/src/main/resources/templates/aeolus/go/default.sh new file mode 100644 index 000000000000..af8a2038e4d1 --- /dev/null +++ b/src/main/resources/templates/aeolus/go/default.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -e +export AEOLUS_INITIAL_DIRECTORY=${PWD} +build () { + echo '⚙️ executing build' + cd "${testWorkingDirectory}" + go test -c -o /dev/null ./... + +} + +test () { + echo '⚙️ executing test' + cd "${testWorkingDirectory}" + go test ./... -json 2>&1 | go-junit-report -parser gojson -out test-results.xml + +} + +main () { + if [[ "${1}" == "aeolus_sourcing" ]]; then + return 0 # just source to use the methods in the subshell, no execution + fi + local _script_name + _script_name=${BASH_SOURCE[0]:-$0} + cd "${AEOLUS_INITIAL_DIRECTORY}" + bash -c "source ${_script_name} aeolus_sourcing; build" + cd "${AEOLUS_INITIAL_DIRECTORY}" + bash -c "source ${_script_name} aeolus_sourcing; test" +} + +main "${@}" diff --git a/src/main/resources/templates/aeolus/go/default.yaml b/src/main/resources/templates/aeolus/go/default.yaml new file mode 100644 index 000000000000..545743d6a0ff --- /dev/null +++ b/src/main/resources/templates/aeolus/go/default.yaml @@ -0,0 +1,17 @@ +api: v0.0.1 +metadata: + name: "Go" + id: Go +actions: + - name: build + script: | + cd "${testWorkingDirectory}" + go test -c -o /dev/null ./... + - name: test + script: | + cd "${testWorkingDirectory}" + go test ./... -json 2>&1 | go-junit-report -parser gojson -out test-results.xml + results: + - name: Go Test Results + path: "${testWorkingDirectory}/test-results.xml" + type: junit diff --git a/src/main/resources/templates/go/exercise/bubblesort.go b/src/main/resources/templates/go/exercise/bubblesort.go new file mode 100644 index 000000000000..2a05e9ca9e13 --- /dev/null +++ b/src/main/resources/templates/go/exercise/bubblesort.go @@ -0,0 +1,7 @@ +package ${packageName} + +type BubbleSort struct{} + +func NewBubbleSort() *BubbleSort { + panic("not implemented") +} diff --git a/src/main/resources/templates/go/exercise/client/client.go b/src/main/resources/templates/go/exercise/client/client.go new file mode 100644 index 000000000000..d806185dab47 --- /dev/null +++ b/src/main/resources/templates/go/exercise/client/client.go @@ -0,0 +1,63 @@ +package main + +import ( + "fmt" + "math/rand" + "time" +) + +// Constants define iteration and random date generation bounds. +const ( + Iterations = 10 + RandomFloor = 5 + RandomCeiling = 15 +) + +// main demonstrates the sorting process. +func main() { + // TODO: Init Context and Policy + + // Run multiple times to simulate different sorting strategies + for i := 0; i < Iterations; i++ { + dates := createRandomDates() + + // TODO: Configure context + + fmt.Println("Unsorted Array of course dates:") + printDates(dates) + fmt.Println() + + // TODO: Sort dates + + fmt.Println("Sorted Array of course dates:") + printDates(dates) + fmt.Println() + } +} + +// createRandomDates generates a slice of random dates with size between RandomFloor and RandomCeiling. +func createRandomDates() []time.Time { + listLength := rand.Intn(RandomCeiling-RandomFloor) + RandomFloor + list := make([]time.Time, listLength) + + lowestDate := time.Date(2024, 10, 15, 0, 0, 0, 0, time.UTC) + highestDate := time.Date(2025, 1, 15, 0, 0, 0, 0, time.UTC) + + for i := 0; i < listLength; i++ { + list[i] = randomDateWithin(lowestDate, highestDate) + } + return list +} + +// randomDateWithin creates a random time.Time value within the given range. +func randomDateWithin(low, high time.Time) time.Time { + randomTime := rand.Int63n(high.Unix()-low.Unix()) + low.Unix() + return time.Unix(randomTime, 0) +} + +// printDates prints out the given slice of time.Time values as dates. +func printDates(dates []time.Time) { + for _, date := range dates { + fmt.Println(date.Format(time.DateOnly)) + } +} diff --git a/src/main/resources/templates/go/exercise/context.go b/src/main/resources/templates/go/exercise/context.go new file mode 100644 index 000000000000..aced7eb6bea0 --- /dev/null +++ b/src/main/resources/templates/go/exercise/context.go @@ -0,0 +1,7 @@ +package ${packageName} + +type Context struct{} + +func NewContext() *Context { + panic("not implemented") +} diff --git a/src/main/resources/templates/go/exercise/go.mod b/src/main/resources/templates/go/exercise/go.mod new file mode 100644 index 000000000000..224363e133d4 --- /dev/null +++ b/src/main/resources/templates/go/exercise/go.mod @@ -0,0 +1,3 @@ +module artemis/${packageName} + +go 1.23.2 diff --git a/src/main/resources/templates/go/exercise/mergesort.go b/src/main/resources/templates/go/exercise/mergesort.go new file mode 100644 index 000000000000..e3c8f12f29d6 --- /dev/null +++ b/src/main/resources/templates/go/exercise/mergesort.go @@ -0,0 +1,7 @@ +package ${packageName} + +type MergeSort struct{} + +func NewMergeSort() *MergeSort { + panic("not implemented") +} diff --git a/src/main/resources/templates/go/exercise/policy.go b/src/main/resources/templates/go/exercise/policy.go new file mode 100644 index 000000000000..7676b8bfbaeb --- /dev/null +++ b/src/main/resources/templates/go/exercise/policy.go @@ -0,0 +1,7 @@ +package ${packageName} + +type Policy struct{} + +func NewPolicy(context *Context) *Policy { + panic("not implemented") +} diff --git a/src/main/resources/templates/go/exercise/sortstrategy.go b/src/main/resources/templates/go/exercise/sortstrategy.go new file mode 100644 index 000000000000..3f3884429007 --- /dev/null +++ b/src/main/resources/templates/go/exercise/sortstrategy.go @@ -0,0 +1,3 @@ +package ${packageName} + +type SortStrategy interface{} diff --git a/src/main/resources/templates/go/readme b/src/main/resources/templates/go/readme new file mode 100755 index 000000000000..cc50dd5e3984 --- /dev/null +++ b/src/main/resources/templates/go/readme @@ -0,0 +1,91 @@ +# Sorting with the Strategy Pattern + +In this exercise, we want to implement sorting algorithms and choose them based on runtime specific variables. + +### Part 1: Sorting + +First, we need to implement two sorting algorithms, in this case `MergeSort` and `BubbleSort`. + +**You have the following tasks:** + +1. [task][Implement Bubble Sort](TestBubbleSort,TestBubbleSortSortsCorrectly) +Implement the method `PerformSort([]time.Time)` in the class `BubbleSort`. Make sure to follow the Bubble Sort algorithm exactly. + +2. [task][Implement Merge Sort](TestMergeSort,TestMergeSortSortsCorrectly) +Implement the method `PerformSort([]time.Time)` in the class `MergeSort`. Make sure to follow the Merge Sort algorithm exactly. + +### Part 2: Strategy Pattern + +We want the application to apply different algorithms for sorting a slice of `time.Time` objects. +Use the strategy pattern to select the right sorting algorithm at runtime. +Implement exported getters and setters for the UML attributes and associations. + +**You have the following tasks:** + +1. SortStrategy Interface +Create a `SortStrategy` interface and adjust the sorting algorithms so that they implement this interface. + +2. [task][Context Class](TestContext) +Create and implement a `Context` class following the below class diagram + +3. [task][Context Policy](TestPolicy) +Create and implement a `Policy` class following the below class diagram with a simple configuration mechanism: + + 1. [task][Select MergeSort](TestUseMergeSortForBigList) + Select `MergeSort` when the slice has more than 10 dates. + + 2. [task][Select BubbleSort](TestUseBubbleSortForSmallList) + Select `BubbleSort` when the slice has less or equal 10 dates. + +4. Complete the `Client` class which demonstrates switching between two strategies at runtime. + +@startuml + +class Client { +} + +class Policy ##testsColor(TestPolicy) { + +NewPolicy() <> + +Configure() +} + +class Context ##testsColor(TestContext) { + -dates: []time.Time + +NewContext() <> + +Sort() +} + +interface SortStrategy { + +PerformSort([]time.Time) +} + +class BubbleSort ##testsColor(TestBubbleSort) { + +NewBubbleSort() <> + +PerformSort([]time.Time) +} + +class MergeSort ##testsColor(TestMergeSort) { + +NewMergeSort() <> + +PerformSort([]time.Time) +} + +MergeSort -up-|> SortStrategy +BubbleSort -up-|> SortStrategy +Policy -right-> Context #testsColor(TestPolicy): context +Context -right-> SortStrategy #testsColor(TestContext): sortAlgorithm +Client .down.> Policy +Client .down.> Context + +hide empty fields +hide empty methods + +@enduml + + +### Part 3: Optional Challenges + +(These are not tested) + +1. Create a new class `QuickSort` that implements `SortStrategy` and implement the Quick Sort algorithm. + +2. Think about a useful decision in `Policy` when to use the new `QuickSort` algorithm. diff --git a/src/main/resources/templates/go/solution/bubblesort.go b/src/main/resources/templates/go/solution/bubblesort.go new file mode 100644 index 000000000000..647472b573ca --- /dev/null +++ b/src/main/resources/templates/go/solution/bubblesort.go @@ -0,0 +1,19 @@ +package ${packageName} + +import "time" + +type BubbleSort struct{} + +func NewBubbleSort() *BubbleSort { + return new(BubbleSort) +} + +func (b *BubbleSort) PerformSort(input []time.Time) { + for i := len(input) - 1; i >= 0; i-- { + for j := 0; j < i; j++ { + if input[j].After(input[j+1]) { + input[j], input[j+1] = input[j+1], input[j] + } + } + } +} diff --git a/src/main/resources/templates/go/solution/client/client.go b/src/main/resources/templates/go/solution/client/client.go new file mode 100644 index 000000000000..290be6aa2a22 --- /dev/null +++ b/src/main/resources/templates/go/solution/client/client.go @@ -0,0 +1,68 @@ +package main + +import ( + "fmt" + "math/rand" + "time" + + implementation "artemis/${packageName}" +) + +// Constants define iteration and random date generation bounds. +const ( + Iterations = 10 + RandomFloor = 5 + RandomCeiling = 15 +) + +// main demonstrates the sorting process. +func main() { + // Init Context and Policy + context := implementation.NewContext() + policy := implementation.NewPolicy(context) + + // Run multiple times to simulate different sorting strategies + for i := 0; i < Iterations; i++ { + dates := createRandomDates() + + context.SetDates(dates) + policy.Configure() + + fmt.Println("Unsorted Array of course dates:") + printDates(dates) + fmt.Println() + + context.Sort() + + fmt.Println("Sorted Array of course dates:") + printDates(dates) + fmt.Println() + } +} + +// createRandomDates generates a slice of random dates with size between RandomFloor and RandomCeiling. +func createRandomDates() []time.Time { + listLength := rand.Intn(RandomCeiling-RandomFloor) + RandomFloor + list := make([]time.Time, listLength) + + lowestDate := time.Date(2024, 10, 15, 0, 0, 0, 0, time.UTC) + highestDate := time.Date(2025, 1, 15, 0, 0, 0, 0, time.UTC) + + for i := 0; i < listLength; i++ { + list[i] = randomDateWithin(lowestDate, highestDate) + } + return list +} + +// randomDateWithin creates a random time.Time value within the given range. +func randomDateWithin(low, high time.Time) time.Time { + randomTime := rand.Int63n(high.Unix()-low.Unix()) + low.Unix() + return time.Unix(randomTime, 0) +} + +// printDates prints out the given slice of time.Time values as dates. +func printDates(dates []time.Time) { + for _, date := range dates { + fmt.Println(date.Format(time.DateOnly)) + } +} diff --git a/src/main/resources/templates/go/solution/context.go b/src/main/resources/templates/go/solution/context.go new file mode 100644 index 000000000000..8165ff883668 --- /dev/null +++ b/src/main/resources/templates/go/solution/context.go @@ -0,0 +1,41 @@ +package ${packageName} + +import "time" + +type Context struct { + dates []time.Time + sortAlgorithm SortStrategy +} + +func NewContext() *Context { + return new(Context) +} + +func (c *Context) GetDates() []time.Time { + return c.dates +} + +func (c *Context) SetDates(dates []time.Time) { + c.dates = dates +} + +func (c *Context) GetSortAlgorithm() SortStrategy { + return c.sortAlgorithm +} + +func (c *Context) SetSortAlgorithm(strategy SortStrategy) { + c.sortAlgorithm = strategy +} + +// Sort runs the configured sort algorithm. +func (c *Context) Sort() { + if c.sortAlgorithm == nil { + panic("sortAlgorithm has not been set") + } + + if c.dates == nil { + panic("dates has not been set") + } + + c.sortAlgorithm.PerformSort(c.dates) +} diff --git a/src/main/resources/templates/go/solution/go.mod b/src/main/resources/templates/go/solution/go.mod new file mode 100644 index 000000000000..224363e133d4 --- /dev/null +++ b/src/main/resources/templates/go/solution/go.mod @@ -0,0 +1,3 @@ +module artemis/${packageName} + +go 1.23.2 diff --git a/src/main/resources/templates/go/solution/mergesort.go b/src/main/resources/templates/go/solution/mergesort.go new file mode 100644 index 000000000000..e37c38d5b0f2 --- /dev/null +++ b/src/main/resources/templates/go/solution/mergesort.go @@ -0,0 +1,57 @@ +package ${packageName} + +import "time" + +type MergeSort struct{} + +func NewMergeSort() *MergeSort { + return new(MergeSort) +} + +// PerformSort is a wrapper method for the real MergeSort algorithm. +func (m *MergeSort) PerformSort(input []time.Time) { + mergeSort(input) +} + +// mergeSort recursively applies the MergeSort algorithm. +func mergeSort(input []time.Time) { + if len(input) < 2 { + return + } + + middle := len(input) / 2 + mergeSort(input[:middle]) + mergeSort(input[middle:]) + merge(input, middle) +} + +// merge merges two ranges within input defined from low to mid and from mid+1 to high. +func merge(input []time.Time, mid int) { + temp := make([]time.Time, len(input)) + + left, right, k := 0, mid, 0 + + for left < mid && right < len(input) { + if input[left].Before(input[right]) { + temp[k] = input[left] + left++ + } else { + temp[k] = input[right] + right++ + } + k++ + } + + for left < mid { + temp[k] = input[left] + left++ + k++ + } + for right < len(input) { + temp[k] = input[right] + right++ + k++ + } + + copy(input, temp) +} diff --git a/src/main/resources/templates/go/solution/policy.go b/src/main/resources/templates/go/solution/policy.go new file mode 100644 index 000000000000..fedd5da02aba --- /dev/null +++ b/src/main/resources/templates/go/solution/policy.go @@ -0,0 +1,20 @@ +package ${packageName} + +type Policy struct { + context *Context +} + +const datesSizeThreshold = 10 + +func NewPolicy(context *Context) *Policy { + return &Policy{context} +} + +// Configure chooses a strategy depending on the number of date objects. +func (p *Policy) Configure() { + if len(p.context.GetDates()) > datesSizeThreshold { + p.context.SetSortAlgorithm(NewMergeSort()) + } else { + p.context.SetSortAlgorithm(NewBubbleSort()) + } +} diff --git a/src/main/resources/templates/go/solution/sortstrategy.go b/src/main/resources/templates/go/solution/sortstrategy.go new file mode 100644 index 000000000000..f61ad7d8f9ad --- /dev/null +++ b/src/main/resources/templates/go/solution/sortstrategy.go @@ -0,0 +1,7 @@ +package ${packageName} + +import "time" + +type SortStrategy interface { + PerformSort(input []time.Time) +} diff --git a/src/main/resources/templates/go/test/behavior/behavior_test.go b/src/main/resources/templates/go/test/behavior/behavior_test.go new file mode 100644 index 000000000000..0af104df30b6 --- /dev/null +++ b/src/main/resources/templates/go/test/behavior/behavior_test.go @@ -0,0 +1,118 @@ +package test + +import ( + "slices" + "testing" + "time" + + assignment "artemis/${packageName}" +) + +type SortStrategy interface { + PerformSort(input []time.Time) +} + +type Context interface { + GetDates() []time.Time + SetDates(dates []time.Time) + GetSortAlgorithm() assignment.SortStrategy + SetSortAlgorithm(strategy assignment.SortStrategy) +} + +type Policy interface { + Configure() +} + +func TestBubbleSortSortsCorrectly(t *testing.T) { + defer handlePanic(t) + + dates, datesWithCorrectOrder := createTestDates() + + var bubbleSortAny interface{} = assignment.NewBubbleSort() + bubbleSort := bubbleSortAny.(SortStrategy) + bubbleSort.PerformSort(dates) + + if !slices.Equal(dates, datesWithCorrectOrder) { + t.Fatalf("expected: %v, got %v", datesWithCorrectOrder, dates) + } +} + +func TestMergeSortSortsCorrectly(t *testing.T) { + defer handlePanic(t) + + dates, datesWithCorrectOrder := createTestDates() + + var mergeSortAny interface{} = assignment.NewMergeSort() + mergeSort := mergeSortAny.(SortStrategy) + mergeSort.PerformSort(dates) + + if !slices.Equal(dates, datesWithCorrectOrder) { + t.Fatalf("expected: %v, got %v", datesWithCorrectOrder, dates) + } +} + +func createTestDates() ([]time.Time, []time.Time) { + dates := []time.Time{ + time.Date(2018, 11, 8, 0, 0, 0, 0, time.UTC), + time.Date(2017, 4, 15, 0, 0, 0, 0, time.UTC), + time.Date(2016, 2, 15, 0, 0, 0, 0, time.UTC), + time.Date(2017, 9, 15, 0, 0, 0, 0, time.UTC), + } + datesWithCorrectOrder := []time.Time{ + time.Date(2016, 2, 15, 0, 0, 0, 0, time.UTC), + time.Date(2017, 4, 15, 0, 0, 0, 0, time.UTC), + time.Date(2017, 9, 15, 0, 0, 0, 0, time.UTC), + time.Date(2018, 11, 8, 0, 0, 0, 0, time.UTC), + } + + return dates, datesWithCorrectOrder +} + +func TestUseMergeSortForBigList(t *testing.T) { + defer handlePanic(t) + + bigList := make([]time.Time, 0) + for i := 0; i < 11; i++ { + bigList = append(bigList, time.Unix(0, 0)) + } + chosenSortStrategy := configurePolicyAndContext(bigList) + _, ok := chosenSortStrategy.(*assignment.MergeSort) + if !ok { + t.Fatal("The sort algorithm of Context was not MergeSort for a list with more than 10 dates.") + } +} + +func TestUseBubbleSortForSmallList(t *testing.T) { + defer handlePanic(t) + + bigList := make([]time.Time, 0) + for i := 0; i < 3; i++ { + bigList = append(bigList, time.Unix(0, 0)) + } + chosenSortStrategy := configurePolicyAndContext(bigList) + _, ok := chosenSortStrategy.(*assignment.BubbleSort) + if !ok { + t.Fatal("The sort algorithm of Context was not BubbleSort for a list with less than 10 dates.") + } +} + +func configurePolicyAndContext(dates []time.Time) interface{} { + contextOriginal := assignment.NewContext() + var contextAny interface{} = contextOriginal + context := contextAny.(Context) + context.SetDates(dates) + + var policyAny interface{} = assignment.NewPolicy(contextOriginal) + policy := policyAny.(Policy) + policy.Configure() + + chosenSortStrategy := context.GetSortAlgorithm() + return chosenSortStrategy +} + +// handlePanic fatally fails the test without terminating the test suite. +func handlePanic(t *testing.T) { + if err := recover(); err != nil { + t.Fatal("panic:", err) + } +} diff --git a/src/main/resources/templates/go/test/go.mod b/src/main/resources/templates/go/test/go.mod new file mode 100644 index 000000000000..ab62174fc091 --- /dev/null +++ b/src/main/resources/templates/go/test/go.mod @@ -0,0 +1,7 @@ +module artemis/testing/test + +go 1.23.2 + +replace artemis/${packageName} => ../${studentParentWorkingDirectoryName} + +require artemis/${packageName} v0.0.0-00010101000000-000000000000 diff --git a/src/main/resources/templates/go/test/structural/structural_test.go b/src/main/resources/templates/go/test/structural/structural_test.go new file mode 100644 index 000000000000..5a6b04916937 --- /dev/null +++ b/src/main/resources/templates/go/test/structural/structural_test.go @@ -0,0 +1,62 @@ +package test + +import ( + "testing" + "time" + + assignment "artemis/${packageName}" +) + +type SortStrategy interface { + PerformSort(input []time.Time) +} + +type Context interface { + GetDates() []time.Time + SetDates(dates []time.Time) + GetSortAlgorithm() assignment.SortStrategy + SetSortAlgorithm(strategy assignment.SortStrategy) +} + +type Policy interface { + Configure() +} + +func TestBubbleSort(t *testing.T) { + defer handlePanic(t) + + var bubbleSort interface{} = new(assignment.BubbleSort) + + _ = bubbleSort.(SortStrategy) +} + +func TestMergeSort(t *testing.T) { + defer handlePanic(t) + + var mergeSort interface{} = new(assignment.MergeSort) + + _ = mergeSort.(SortStrategy) +} + +func TestContext(t *testing.T) { + defer handlePanic(t) + + var context interface{} = new(assignment.Context) + + _ = context.(Context) +} + +func TestPolicy(t *testing.T) { + defer handlePanic(t) + + var policy interface{} = new(assignment.Policy) + + _ = policy.(Policy) +} + +// handlePanic fatally fails the test without terminating the test suite. +func handlePanic(t *testing.T) { + if err := recover(); err != nil { + t.Fatal("panic:", err) + } +} diff --git a/src/main/resources/templates/jenkins/go/regularRuns/pipeline.groovy b/src/main/resources/templates/jenkins/go/regularRuns/pipeline.groovy new file mode 100644 index 000000000000..09b8c81f91e2 --- /dev/null +++ b/src/main/resources/templates/jenkins/go/regularRuns/pipeline.groovy @@ -0,0 +1,55 @@ +/* + * This file configures the actual build steps for the automatic grading. + * + * !!! + * For regular exercises, there is no need to make changes to this file. + * Only this base configuration is actively supported by the Artemis maintainers + * and/or your Artemis instance administrators. + * !!! + */ + +dockerImage = '#dockerImage' +dockerFlags = '#dockerArgs' + +/** + * Main function called by Jenkins. + */ +void testRunner() { + docker.image(dockerImage).inside(dockerFlags) { c -> + runTestSteps() + } +} + +private void runTestSteps() { + test() +} + +/** + * Run unit tests + */ +private void test() { + stage('Test') { + sh ''' + cd tests + go test ./... -json 2>&1 | go-junit-report -parser gojson -out test-results.xml + ''' + } +} + +/** + * Script of the post build tasks aggregating all JUnit files in $WORKSPACE/results. + * + * Called by Jenkins. + */ +void postBuildTasks() { + sh ''' + rm -rf results + mkdir results + cp "tests/test-results.xml" $WORKSPACE/results/ || true + sed -i 's/[^[:print:]\t]/�/g' $WORKSPACE/results/*.xml || true + ''' +} + +// very important, do not remove +// required so that Jenkins finds the methods defined in this script +return this diff --git a/src/main/webapp/app/entities/programming/programming-exercise.model.ts b/src/main/webapp/app/entities/programming/programming-exercise.model.ts index 543d7f138bab..646f7bdfdf4d 100644 --- a/src/main/webapp/app/entities/programming/programming-exercise.model.ts +++ b/src/main/webapp/app/entities/programming/programming-exercise.model.ts @@ -18,6 +18,7 @@ export enum ProgrammingLanguage { C = 'C', C_PLUS_PLUS = 'C_PLUS_PLUS', C_SHARP = 'C_SHARP', + GO = 'GO', HASKELL = 'HASKELL', JAVA = 'JAVA', JAVASCRIPT = 'JAVASCRIPT', diff --git a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-creation-config.ts b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-creation-config.ts index 36c9b8df3a8c..51b9056d0b15 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-creation-config.ts +++ b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-creation-config.ts @@ -22,7 +22,6 @@ export type ProgrammingExerciseCreationConfig = { invalidRepositoryNamePattern: RegExp; isImportFromExistingExercise: boolean; isImportFromFile: boolean; - appNamePatternForSwift: string; modePickerOptions?: ModePickerOption[]; withDependencies: boolean; onWithDependenciesChanged: (withDependencies: boolean) => boolean; 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 5bdde2d8c65c..b5aa5d3bb090 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 @@ -23,6 +23,7 @@ import { INVALID_DIRECTORY_NAME_PATTERN, INVALID_REPOSITORY_NAME_PATTERN, MAX_PENALTY_PATTERN, + PACKAGE_NAME_PATTERN_FOR_GO, PACKAGE_NAME_PATTERN_FOR_JAVA_BLACKBOX, PACKAGE_NAME_PATTERN_FOR_JAVA_KOTLIN, PROGRAMMING_EXERCISE_SHORT_NAME_PATTERN, @@ -58,12 +59,13 @@ export const LOCAL_STORAGE_KEY_IS_SIMPLE_MODE = 'isSimpleMode'; export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDestroy, OnInit { protected readonly documentationType: DocumentationType = 'Programming'; protected readonly maxPenaltyPattern = MAX_PENALTY_PATTERN; - protected readonly packageNamePatternForJavaBlackbox = PACKAGE_NAME_PATTERN_FOR_JAVA_BLACKBOX; protected readonly invalidRepositoryNamePattern = INVALID_REPOSITORY_NAME_PATTERN; protected readonly invalidDirectoryNamePattern = INVALID_DIRECTORY_NAME_PATTERN; protected readonly shortNamePattern = PROGRAMMING_EXERCISE_SHORT_NAME_PATTERN; - readonly packageNamePatternForJavaKotlin = PACKAGE_NAME_PATTERN_FOR_JAVA_KOTLIN; - readonly appNamePatternForSwift = APP_NAME_PATTERN_FOR_SWIFT; + private readonly packageNameRegexForJavaKotlin = RegExp(PACKAGE_NAME_PATTERN_FOR_JAVA_KOTLIN); + private readonly packageNameRegexForJavaBlackbox = RegExp(PACKAGE_NAME_PATTERN_FOR_JAVA_BLACKBOX); + private readonly appNameRegexForSwift = RegExp(APP_NAME_PATTERN_FOR_SWIFT); + private readonly packageNameRegexForGo = RegExp(PACKAGE_NAME_PATTERN_FOR_GO); @ViewChild(ProgrammingExerciseInformationComponent) exerciseInfoComponent?: ProgrammingExerciseInformationComponent; @ViewChild(ProgrammingExerciseModeComponent) exerciseDifficultyComponent?: ProgrammingExerciseModeComponent; @@ -825,10 +827,17 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest * @param useBlackboxPattern whether to allow points in the regex */ setPackageNamePattern(language: ProgrammingLanguage, useBlackboxPattern = false) { - if (language === ProgrammingLanguage.SWIFT) { - this.packageNamePattern = this.appNamePatternForSwift; - } else { - this.packageNamePattern = useBlackboxPattern ? this.packageNamePatternForJavaBlackbox : this.packageNamePatternForJavaKotlin; + switch (language) { + case ProgrammingLanguage.SWIFT: + this.packageNamePattern = APP_NAME_PATTERN_FOR_SWIFT; + break; + case ProgrammingLanguage.JAVA: + case ProgrammingLanguage.KOTLIN: + this.packageNamePattern = useBlackboxPattern ? PACKAGE_NAME_PATTERN_FOR_JAVA_BLACKBOX : PACKAGE_NAME_PATTERN_FOR_JAVA_KOTLIN; + break; + case ProgrammingLanguage.GO: + this.packageNamePattern = PACKAGE_NAME_PATTERN_FOR_GO; + break; } } @@ -1062,13 +1071,26 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest } private validateExercisePackageName(validationErrorReasons: ValidationReason[]): void { - // validation on package name has only to be performed for Java, Kotlin and Swift - if ( - this.programmingExercise.programmingLanguage !== ProgrammingLanguage.JAVA && - this.programmingExercise.programmingLanguage !== ProgrammingLanguage.KOTLIN && - this.programmingExercise.programmingLanguage !== ProgrammingLanguage.SWIFT - ) { - return; + let regex; + switch (this.programmingExercise.programmingLanguage) { + case ProgrammingLanguage.JAVA: + if (this.programmingExercise.projectType === ProjectType.MAVEN_BLACKBOX) { + regex = this.packageNameRegexForJavaBlackbox; + } else { + regex = this.packageNameRegexForJavaKotlin; + } + break; + case ProgrammingLanguage.KOTLIN: + regex = this.packageNameRegexForJavaKotlin; + break; + case ProgrammingLanguage.SWIFT: + regex = this.appNameRegexForSwift; + break; + case ProgrammingLanguage.GO: + regex = this.packageNameRegexForGo; + break; + default: + return; } if (this.programmingExercise.packageName === undefined || this.programmingExercise.packageName === '') { @@ -1076,39 +1098,19 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest translateKey: 'artemisApp.exercise.form.packageName.undefined', translateValues: {}, }); - } 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); - 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', - translateValues: {}, - }); - } else if (this.programmingExercise.programmingLanguage === ProgrammingLanguage.SWIFT && (patternMatchSwift === null || patternMatchSwift.length === 0)) { - validationErrorReasons.push({ - translateKey: 'artemisApp.exercise.form.packageName.pattern.SWIFT', - translateValues: {}, - }); - } + return; + } + + const packageNameDoesMatch = regex.test(this.programmingExercise.packageName); + if (!packageNameDoesMatch) { + const translateKey = + this.programmingExercise.projectType === ProjectType.MAVEN_BLACKBOX + ? `artemisApp.exercise.form.packageName.pattern.${this.programmingExercise.programmingLanguage}_BLACKBOX` + : `artemisApp.exercise.form.packageName.pattern.${this.programmingExercise.programmingLanguage}`; + validationErrorReasons.push({ + translateKey, + translateValues: {}, + }); } } @@ -1244,7 +1246,6 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest exerciseCategories: this.exerciseCategories, existingCategories: this.existingCategories, updateCategories: this.categoriesChanged, - appNamePatternForSwift: this.appNamePatternForSwift, modePickerOptions: this.modePickerOptions, withDependencies: this.withDependencies, onWithDependenciesChanged: this.withDependenciesChanged, diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/language/programming-exercise-language.component.html b/src/main/webapp/app/exercises/programming/manage/update/update-components/language/programming-exercise-language.component.html index 7b60308b7091..4dd22d1e9b35 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/update-components/language/programming-exercise-language.component.html +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/language/programming-exercise-language.component.html @@ -122,7 +122,7 @@ setTimeout(() => this.calculateFormValid()))); } diff --git a/src/main/webapp/app/shared/constants/input.constants.ts b/src/main/webapp/app/shared/constants/input.constants.ts index 6748c2507b07..d220eae6bb11 100644 --- a/src/main/webapp/app/shared/constants/input.constants.ts +++ b/src/main/webapp/app/shared/constants/input.constants.ts @@ -47,3 +47,7 @@ export const PACKAGE_NAME_PATTERN_FOR_JAVA_KOTLIN = // with the restriction to a-z,A-Z as "Swift letter" and 0-9 as digits where no separators are allowed export const APP_NAME_PATTERN_FOR_SWIFT = '^(?!(?:associatedtype|class|deinit|enum|extension|fileprivate|func|import|init|inout|internal|let|open|operator|private|protocol|public|rethrows|static|struct|subscript|typealias|var|break|case|continue|default|defer|do|else|fallthrough|for|guard|if|in|repeat|return|switch|where|while|as|Any|catch|false|is|nil|super|self|Self|throw|throws|true|try|_|[sS]wift)$)[A-Za-z][0-9A-Za-z]*$'; +// Go package name Regex derived from (https://go.dev/ref/spec#Package_clause) limited to ASCII. Package names are identifiers. +// They allow letters, digits and underscore. They cannot start with a digit. The package name cannot be a keyword or "_". +export const PACKAGE_NAME_PATTERN_FOR_GO = + '^(?!(?:break|default|func|interface|select|case|defer|go|map|struct|chan|else|goto|package|switch|const|fallthrough|if|range|type|continue|for|import|return|var|_)$)[A-Za-z_][A-Za-z0-9_]*$'; diff --git a/src/main/webapp/i18n/de/exercise.json b/src/main/webapp/i18n/de/exercise.json index 00cc0421b9db..7aa36134af6f 100644 --- a/src/main/webapp/i18n/de/exercise.json +++ b/src/main/webapp/i18n/de/exercise.json @@ -239,7 +239,8 @@ "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\"!", - "JAVA_BLACKBOX": "Der Paketname muss ein gültiger Java-Bezeicher sein. Zusätzlich sind Trennpunkte im Paketnamen für den DejaGnu Aufgabentyp nicht erlaubt." + "JAVA_BLACKBOX": "Der Paketname muss ein gültiger Java-Bezeicher sein. Zusätzlich sind Trennpunkte im Paketnamen für den DejaGnu Aufgabentyp nicht erlaubt.", + "GO": "Der Package-Name muss ein gültiger Go-Identifier sein, z.B. \"exercise\"." } }, "points": { diff --git a/src/main/webapp/i18n/en/exercise.json b/src/main/webapp/i18n/en/exercise.json index 82144be9ffe5..d103ba89eb2e 100644 --- a/src/main/webapp/i18n/en/exercise.json +++ b/src/main/webapp/i18n/en/exercise.json @@ -239,7 +239,8 @@ "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\"!", - "JAVA_BLACKBOX": "The package name must be a valid Java identifier. In addition, no dots are allowed in the package name of DejaGnu projects" + "JAVA_BLACKBOX": "The package name must be a valid Java identifier. In addition, no dots are allowed in the package name of DejaGnu projects", + "GO": "The package name must be a valid Go identifier, e.g. \"exercise\"." } }, "points": { diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseFactory.java b/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseFactory.java index 2fc3bb8d8af5..2a35129aaa9c 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseFactory.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseFactory.java @@ -140,7 +140,12 @@ else if (programmingLanguage == ProgrammingLanguage.SWIFT) { else { programmingExercise.setProjectType(null); } - programmingExercise.setPackageName(programmingLanguage == ProgrammingLanguage.SWIFT ? "swiftTest" : "de.test"); + if (programmingLanguage == ProgrammingLanguage.JAVA || programmingLanguage == ProgrammingLanguage.KOTLIN) { + programmingExercise.setPackageName("de.test"); + } + else { + programmingExercise.setPackageName("testPackage"); + } final var repoName = programmingExercise.generateRepositoryName(RepositoryType.TESTS); String testRepoUri = String.format("%s/git/%s/%s.git", artemisVersionControlUrl, programmingExercise.getProjectKey(), repoName); programmingExercise.setTestRepositoryUri(testRepoUri); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseTestService.java b/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseTestService.java index 7e8e92c64162..e097a79f772f 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseTestService.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseTestService.java @@ -5,6 +5,7 @@ import static de.tum.cit.aet.artemis.exercise.domain.ExerciseMode.TEAM; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.C; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.JAVA; +import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.KOTLIN; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.SWIFT; import static de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseExportService.BUILD_PLAN_FILE_NAME; import static de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseExportService.EXPORTED_EXERCISE_DETAILS_FILE_PREFIX; @@ -526,8 +527,8 @@ public void createProgrammingExercise_mode_validExercise_created(ExerciseMode mo public void createProgrammingExercise_programmingLanguage_validExercise_created(ProgrammingLanguage language, ProgrammingLanguageFeature programmingLanguageFeature) throws Exception { exercise.setProgrammingLanguage(language); - if (language == SWIFT) { - exercise.setPackageName("swiftTest"); + if (programmingLanguageFeature.packageNameRequired() && language != JAVA && language != KOTLIN) { + exercise.setPackageName("testPackage"); } exercise.setProjectType(programmingLanguageFeature.projectTypes().isEmpty() ? null : programmingLanguageFeature.projectTypes().getFirst()); mockDelegate.mockConnectorRequestsForSetup(exercise, false, false, false); @@ -687,8 +688,8 @@ public void createProgrammingExercise_validExercise_withStaticCodeAnalysis(Progr throws Exception { exercise.setStaticCodeAnalysisEnabled(true); exercise.setProgrammingLanguage(language); - if (language == SWIFT) { - exercise.setPackageName("swiftTest"); + if (programmingLanguageFeature.packageNameRequired() && language != JAVA && language != KOTLIN) { + exercise.setPackageName("testPackage"); } // Exclude ProjectType FACT as SCA is not supported if (language == C) { 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 3e49d0bd49ff..c4cbdf912f8a 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 @@ -76,6 +76,7 @@ import { TitleChannelNameComponent } from 'app/shared/form/title-channel-name/ti import { ExerciseTitleChannelNameModule } from 'app/exercises/shared/exercise-title-channel-name/exercise-title-channel-name.module'; import { CustomNotIncludedInValidatorDirective } from '../../../../../main/webapp/app/shared/validators/custom-not-included-in-validator.directive'; import { ProgrammingExerciseDifficultyComponent } from '../../../../../main/webapp/app/exercises/programming/manage/update/update-components/difficulty/programming-exercise-difficulty.component'; +import { APP_NAME_PATTERN_FOR_SWIFT, PACKAGE_NAME_PATTERN_FOR_JAVA_KOTLIN } from '../../../../../main/webapp/app/shared/constants/input.constants'; describe('ProgrammingExerciseUpdateComponent', () => { const courseId = 1; @@ -534,7 +535,7 @@ describe('ProgrammingExerciseUpdateComponent', () => { expect(courseService.find).toHaveBeenCalledWith(courseId); expect(comp.selectedProgrammingLanguage).toBe(ProgrammingLanguage.SWIFT); expect(comp.staticCodeAnalysisAllowed).toBeTrue(); - expect(comp.packageNamePattern).toBe(comp.appNamePatternForSwift); + expect(comp.packageNamePattern).toBe(APP_NAME_PATTERN_FOR_SWIFT); })); it('should activate SCA for C', fakeAsync(() => { @@ -560,7 +561,7 @@ describe('ProgrammingExerciseUpdateComponent', () => { // THEN expect(comp.selectedProgrammingLanguage).toBe(ProgrammingLanguage.JAVA); expect(comp.staticCodeAnalysisAllowed).toBeTrue(); - expect(comp.packageNamePattern).toBe(comp.packageNamePatternForJavaKotlin); + expect(comp.packageNamePattern).toBe(PACKAGE_NAME_PATTERN_FOR_JAVA_KOTLIN); })); it('should deactivate SCA for C (FACT)', fakeAsync(() => { diff --git a/src/test/javascript/spec/component/programming-exercise/update-components/programming-exercise-creation-config-mock.ts b/src/test/javascript/spec/component/programming-exercise/update-components/programming-exercise-creation-config-mock.ts index 0b99443e2f99..d1f3ef607ed6 100644 --- a/src/test/javascript/spec/component/programming-exercise/update-components/programming-exercise-creation-config-mock.ts +++ b/src/test/javascript/spec/component/programming-exercise/update-components/programming-exercise-creation-config-mock.ts @@ -6,7 +6,6 @@ import { AuxiliaryRepository } from 'app/entities/programming/programming-exerci /* eslint-disable @typescript-eslint/no-unused-vars */ export const programmingExerciseCreationConfigMock: ProgrammingExerciseCreationConfig = { - appNamePatternForSwift: '', auxiliaryRepositoriesSupported: false, auxiliaryRepositoryDuplicateDirectories: false, auxiliaryRepositoryDuplicateNames: false, diff --git a/src/test/resources/config/application.yml b/src/test/resources/config/application.yml index f2fafb2a2021..8fce605203fa 100644 --- a/src/test/resources/config/application.yml +++ b/src/test/resources/config/application.yml @@ -78,6 +78,8 @@ artemis: default: "~~invalid~~" typescript: default: "~~invalid~~" + go: + default: "~~invalid~~" build-agent: short-name: "artemis-build-agent-test" telemetry: