diff --git a/build.gradle b/build.gradle index 163b2517748f..5a5a4c014e61 100644 --- a/build.gradle +++ b/build.gradle @@ -248,6 +248,7 @@ dependencies { implementation "de.jplag:jplag:${jplag_version}" implementation "de.jplag:c:${jplag_version}" + implementation "de.jplag:cpp:${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 660e2bd4bf02..e1ce98df80f1 100644 --- a/docs/user/exercises/programming-exercise-features.inc +++ b/docs/user/exercises/programming-exercise-features.inc @@ -39,6 +39,8 @@ Instructors can still use those templates to generate programming exercises and +----------------------+----------+---------+ | R | yes | yes | +----------------------+----------+---------+ + | C++ | 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. @@ -75,6 +77,8 @@ Instructors can still use those templates to generate programming exercises and +----------------------+----------------------+----------------------+---------------------+--------------+------------------------------------------+------------------------------+----------------------------+------------------------+ | R | no | no | yes | no | n/a | no | no | L: yes, J: no | +----------------------+----------------------+----------------------+---------------------+--------------+------------------------------------------+------------------------------+----------------------------+------------------------+ + | C++ | no | no | yes | no | 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 ea6ebfdf3b81..30e568ac85a5 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 @@ -30,6 +30,7 @@ import de.jplag.Language; import de.jplag.c.CLanguage; import de.jplag.clustering.ClusteringOptions; +import de.jplag.cpp.CPPLanguage; import de.jplag.exceptions.ExitException; import de.jplag.java.JavaLanguage; import de.jplag.javascript.JavaScriptLanguage; @@ -312,6 +313,7 @@ public void deleteTempLocalRepository(Repository repository) { private Language getJPlagProgrammingLanguage(ProgrammingExercise programmingExercise) { return switch (programmingExercise.getProgrammingLanguage()) { case C -> new CLanguage(); + case C_PLUS_PLUS -> new CPPLanguage(); case JAVA -> new JavaLanguage(); case JAVASCRIPT -> new JavaScriptLanguage(); case KOTLIN -> new KotlinLanguage(); @@ -319,9 +321,8 @@ private Language getJPlagProgrammingLanguage(ProgrammingExercise programmingExer case R -> new RLanguage(); case RUST -> new RustLanguage(); case SWIFT -> new SwiftLanguage(); - case EMPTY, PHP, DART, HASKELL, ASSEMBLER, OCAML, C_SHARP, C_PLUS_PLUS, SQL, TYPESCRIPT, GO, MATLAB, BASH, VHDL, RUBY, POWERSHELL, ADA -> - throw new BadRequestAlertException("Programming language " + programmingExercise.getProgrammingLanguage() + " not supported for plagiarism check.", - "ProgrammingExercise", "notSupported"); + case EMPTY, PHP, DART, HASKELL, ASSEMBLER, OCAML, C_SHARP, SQL, TYPESCRIPT, GO, 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 781ad04f98c6..0fdc216122b9 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 @@ -40,6 +40,7 @@ public enum ProgrammingLanguage { private static final Set ENABLED_LANGUAGES = Set.of( ASSEMBLER, C, + C_PLUS_PLUS, HASKELL, JAVA, JAVASCRIPT, 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 4c73046b1ab0..cc018ae1eb61 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 -> defaultRepositoryUpgradeService; - case C_SHARP, C_PLUS_PLUS, SQL, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> + case KOTLIN, PYTHON, C, HASKELL, VHDL, ASSEMBLER, SWIFT, OCAML, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS -> defaultRepositoryUpgradeService; + case C_SHARP, SQL, TYPESCRIPT, GO, 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 b9050501c67a..f2f53467d261 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 -> "assignment"; - case C_SHARP, C_PLUS_PLUS, SQL, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> + case JAVA, PYTHON, C, HASKELL, KOTLIN, VHDL, ASSEMBLER, SWIFT, OCAML, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS -> "assignment"; + case C_SHARP, SQL, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + language); }; } @@ -230,9 +230,9 @@ public String forProgrammingLanguage(ProgrammingLanguage language) { @Override public String forProgrammingLanguage(ProgrammingLanguage language) { return switch (language) { - case JAVA, PYTHON, HASKELL, KOTLIN, SWIFT, EMPTY, RUST, JAVASCRIPT, R -> ""; + case JAVA, PYTHON, HASKELL, KOTLIN, SWIFT, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS -> ""; case C, VHDL, ASSEMBLER, OCAML -> "tests"; - case C_SHARP, C_PLUS_PLUS, SQL, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> + case C_SHARP, SQL, TYPESCRIPT, GO, 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 45a473da9148..8b948e8f0073 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 @@ -1,6 +1,7 @@ package de.tum.cit.aet.artemis.programming.service.jenkins; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.C; +import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.C_PLUS_PLUS; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.EMPTY; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.HASKELL; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.JAVA; @@ -35,6 +36,7 @@ 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, false)); 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(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 f900cc0f6dd1..dc4ff7a8178a 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 -> jenkinsBuildPlanCreator; - case VHDL, ASSEMBLER, OCAML, C_SHARP, C_PLUS_PLUS, SQL, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> + case JAVA, KOTLIN, PYTHON, C, HASKELL, SWIFT, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS -> jenkinsBuildPlanCreator; + case VHDL, ASSEMBLER, OCAML, C_SHARP, SQL, TYPESCRIPT, GO, 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 bc8292d407bb..704755528d3c 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 @@ -3,6 +3,7 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_LOCALCI; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.ASSEMBLER; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.C; +import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.C_PLUS_PLUS; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.EMPTY; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.HASKELL; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.JAVA; @@ -42,6 +43,7 @@ public LocalCIProgrammingLanguageFeatureService() { programmingLanguageFeatures.put(EMPTY, new ProgrammingLanguageFeature(EMPTY, false, false, false, false, false, List.of(), false, true)); programmingLanguageFeatures.put(ASSEMBLER, new ProgrammingLanguageFeature(ASSEMBLER, false, false, false, false, false, List.of(), false, true)); 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(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 3924e2d804f9..6645696acc2e 100644 --- a/src/main/resources/config/application.yml +++ b/src/main/resources/config/application.yml @@ -93,6 +93,8 @@ artemis: default: "ghcr.io/ls1intum/artemis-javascript-docker:v1.0.0" r: default: "ghcr.io/ls1intum/artemis-r-docker:v1.0.0" + c_plus_plus: + default: "ghcr.io/ls1intum/artemis-cpp-docker:v1.0.0" management: endpoints: diff --git a/src/main/resources/templates/aeolus/c_plus_plus/default.sh b/src/main/resources/templates/aeolus/c_plus_plus/default.sh new file mode 100644 index 000000000000..aa91d2f607d0 --- /dev/null +++ b/src/main/resources/templates/aeolus/c_plus_plus/default.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +set -e +export AEOLUS_INITIAL_DIRECTORY=${PWD} +setup_the_build_environment () { + echo '⚙️ executing setup_the_build_environment' + #!/usr/bin/env bash + + # ------------------------------ + # Task Description: + # Setup the build environment + # ------------------------------ + + mkdir test-reports + + # Updating ownership... + chown -R artemis_user:artemis_user . + + REQ_FILE=requirements.txt + if [ -f "$REQ_FILE" ]; then + python3 -m venv /venv + /venv/bin/pip3 install -r "$REQ_FILE" + else + echo "$REQ_FILE does not exist" + fi +} + +build_and_run_all_tests () { + echo '⚙️ executing build_and_run_all_tests' + #!/usr/bin/env bash + + # ------------------------------ + # Task Description: + # Build and run all tests + # ------------------------------ + + if [ -d /venv ]; then + . /venv/bin/activate + fi + + # Run tests as unprivileged user + runuser -u artemis_user python3 Tests.py +} + +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; setup_the_build_environment" + cd "${AEOLUS_INITIAL_DIRECTORY}" + bash -c "source ${_script_name} aeolus_sourcing; build_and_run_all_tests" +} + +main "${@}" diff --git a/src/main/resources/templates/aeolus/c_plus_plus/default.yaml b/src/main/resources/templates/aeolus/c_plus_plus/default.yaml new file mode 100644 index 000000000000..ac71eb79779f --- /dev/null +++ b/src/main/resources/templates/aeolus/c_plus_plus/default.yaml @@ -0,0 +1,48 @@ +api: v0.0.1 +metadata: + name: C++ + id: c_plus_plus + description: Test using the GBS Tester +actions: + - name: setup_the_build_environment + script: |- + #!/usr/bin/env bash + + # ------------------------------ + # Task Description: + # Setup the build environment + # ------------------------------ + + mkdir test-reports + + # Updating ownership... + chown -R artemis_user:artemis_user . + + REQ_FILE=requirements.txt + if [ -f "$REQ_FILE" ]; then + python3 -m venv /venv + /venv/bin/pip3 install -r "$REQ_FILE" + else + echo "$REQ_FILE does not exist" + fi + runAlways: false + - name: build_and_run_all_tests + script: |- + #!/usr/bin/env bash + + # ------------------------------ + # Task Description: + # Build and run all tests + # ------------------------------ + + if [ -d /venv ]; then + . /venv/bin/activate + fi + + # Run tests as unprivileged user + runuser -u artemis_user python3 Tests.py + runAlways: false + results: + - name: junit_test-reports/tests-results.xml + path: 'test-reports/*.xml' + type: junit diff --git a/src/main/resources/templates/c_plus_plus/exercise/.clang-format b/src/main/resources/templates/c_plus_plus/exercise/.clang-format new file mode 100644 index 000000000000..541e6dca6c50 --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/exercise/.clang-format @@ -0,0 +1,4 @@ +--- +Language: Cpp +BasedOnStyle: Google +IncludeBlocks: Preserve diff --git a/src/main/resources/templates/c_plus_plus/exercise/.gitattributes b/src/main/resources/templates/c_plus_plus/exercise/.gitattributes new file mode 100644 index 000000000000..0e4a9089ed6e --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/exercise/.gitattributes @@ -0,0 +1,39 @@ +# Source: https://github.com/gitattributes/gitattributes/blob/master/C%2B%2B.gitattributes (01.09.2024) + +# Sources +*.c text diff=cpp +*.cc text diff=cpp +*.cxx text diff=cpp +*.cpp text diff=cpp +*.cpi text diff=cpp +*.c++ text diff=cpp +*.hpp text diff=cpp +*.h text diff=cpp +*.h++ text diff=cpp +*.hh text diff=cpp + +# Compiled Object files +*.slo binary +*.lo binary +*.o binary +*.obj binary + +# Precompiled Headers +*.gch binary +*.pch binary + +# Compiled Dynamic libraries +*.so binary +*.dylib binary +*.dll binary + +# Compiled Static libraries +*.lai binary +*.la binary +*.a binary +*.lib binary + +# Executables +*.exe binary +*.out binary +*.app binary diff --git a/src/main/resources/templates/c_plus_plus/exercise/.gitignore b/src/main/resources/templates/c_plus_plus/exercise/.gitignore new file mode 100644 index 000000000000..62f60adbb914 --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/exercise/.gitignore @@ -0,0 +1,4 @@ +cmake-build-*/ + +.vscode/ +.idea/ diff --git a/src/main/resources/templates/c_plus_plus/exercise/CMakeLists.txt b/src/main/resources/templates/c_plus_plus/exercise/CMakeLists.txt new file mode 100644 index 000000000000..1a01a44252aa --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/exercise/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.13) +project(ArtemisExercise) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_library(assignment src/sort.cpp) +target_include_directories(assignment PUBLIC include) + +add_executable(assignment_main src/main.cpp) +target_link_libraries(assignment_main assignment) diff --git a/src/main/resources/templates/c_plus_plus/exercise/include/sort.hpp b/src/main/resources/templates/c_plus_plus/exercise/include/sort.hpp new file mode 100644 index 000000000000..886fc4aeba92 --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/exercise/include/sort.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include + +void selection_sort(std::vector::iterator begin, + std::vector::iterator end); + +void insertion_sort(std::vector::iterator begin, + std::vector::iterator end); + +void quicksort(std::vector::iterator begin, + std::vector::iterator end); + +void mergesort(std::vector::iterator begin, + std::vector::iterator end); + +void mergesort_inplace(std::vector::iterator begin, + std::vector::iterator end); + +void heapsort(std::vector::iterator begin, std::vector::iterator end); + +void heapsort_explicit(std::vector::iterator begin, + std::vector::iterator end); + +void bogosort(std::vector::iterator begin, std::vector::iterator end); diff --git a/src/main/resources/templates/c_plus_plus/exercise/src/main.cpp b/src/main/resources/templates/c_plus_plus/exercise/src/main.cpp new file mode 100644 index 000000000000..16b651caade9 --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/exercise/src/main.cpp @@ -0,0 +1,5 @@ +#include "sort.hpp" + +int main() { + // Test your implementation here +} diff --git a/src/main/resources/templates/c_plus_plus/exercise/src/sort.cpp b/src/main/resources/templates/c_plus_plus/exercise/src/sort.cpp new file mode 100644 index 000000000000..5966af03e21f --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/exercise/src/sort.cpp @@ -0,0 +1,43 @@ +#include "sort.hpp" + +#include + +void selection_sort(std::vector::iterator begin, + std::vector::iterator end) { + throw std::logic_error("not implemented"); +} + +void insertion_sort(std::vector::iterator begin, + std::vector::iterator end) { + throw std::logic_error("not implemented"); +} + +void quicksort(std::vector::iterator begin, + std::vector::iterator end) { + throw std::logic_error("not implemented"); +} + +void mergesort(std::vector::iterator begin, + std::vector::iterator end) { + throw std::logic_error("not implemented"); +} + +void mergesort_inplace(std::vector::iterator begin, + std::vector::iterator end) { + throw std::logic_error("not implemented"); +} + +void heapsort(std::vector::iterator begin, + std::vector::iterator end) { + throw std::logic_error("not implemented"); +} + +void heapsort_explicit(std::vector::iterator begin, + std::vector::iterator end) { + throw std::logic_error("not implemented"); +} + +void bogosort(std::vector::iterator begin, + std::vector::iterator end) { + throw std::logic_error("not implemented"); +} diff --git a/src/main/resources/templates/c_plus_plus/readme b/src/main/resources/templates/c_plus_plus/readme new file mode 100644 index 000000000000..5aefda9d7606 --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/readme @@ -0,0 +1,91 @@ +# Sorting using `` building blocks + +An array `v` is considered _sorted_ if each pair of neighboring elements fulfills `v[i] <= v[i + 1]`. +Sorting an array means rearranging the elements such that it is sorted. This also means that +sorting may not add or remove elements. This is usually achieved by swapping elements. + +1. [task][CMake runs correctly](TestConfigure) +2. [task][Your code compiles](CompileSort) + + +# Sorting Algorithms +[task][All algorithms sort correctly](TestCatch2(sort-test)) +1. [task][Selection Sort](sorting_algorithms/selection_sort,sorting_algorithms/all_elements_equal/selection_sort,sorting_algorithms/reverse-sorted_values/selection_sort,sorting_algorithms/single_values/selection_sort,sorting_algorithms/empty_input/selection_sort,sorting_algorithms/large_input/selection_sort) + Find the correct value for the next position, one position at a time. + + Implement the following function using suitable C++ standard library algorithms. + ```c++ + void selection_sort(std::vector::iterator begin, + std::vector::iterator end) { /* ... */ } + ``` + +2. [task][Insertion Sort](sorting_algorithms/insertion_sort,sorting_algorithms/all_elements_equal/insertion_sort,sorting_algorithms/reverse-sorted_values/insertion_sort,sorting_algorithms/single_values/insertion_sort,sorting_algorithms/empty_input/insertion_sort,sorting_algorithms/large_input/insertion_sort) + Find the correct position in the sorted sequence for the next value, one value at a time. + + Implement the following function using suitable C++ standard library algorithms: + ```c++ + void insertion_sort(std::vector::iterator begin, + std::vector::iterator end) { /* ... */ } + ``` + +3. [task][Quicksort](sorting_algorithms/quicksort,sorting_algorithms/all_elements_equal/quicksort,sorting_algorithms/reverse-sorted_values/quicksort,sorting_algorithms/single_values/quicksort,sorting_algorithms/empty_input/quicksort,sorting_algorithms/large_input/quicksort) + Quicksort chooses a single element (called _pivot_ `p`) from the input and partitions the + remaining elements into $$ \le p $$ and $$ \gt p $$, with the pivot placed between them. + + ```c++ + void quicksort(std::vector::iterator begin, + std::vector::iterator end) { /* .. */ } + ``` + + Don't forget to catch the base case (sorting 0 or 1 elements)! + +4. [task][Mergesort](sorting_algorithms/mergesort,sorting_algorithms/all_elements_equal/mergesort,sorting_algorithms/reverse-sorted_values/mergesort,sorting_algorithms/single_values/mergesort,sorting_algorithms/empty_input/mergesort,sorting_algorithms/large_input/mergesort) + Split the input into 2 equal-sized halves, call `mergesort` on them and then merge/interleave + the two sorted halves using an appropriate algorithm with linear time complexity. + + ```c++ + void mergesort(std::vector::iterator begin, + std::vector::iterator end) { /* ... */ } + ``` + + Don't forget to catch the base case (sorting 0 or 1 elements)! + +5. [task][Mergesort Inplace](sorting_algorithms/mergesort_inplace,sorting_algorithms/all_elements_equal/mergesort_inplace,sorting_algorithms/reverse-sorted_values/mergesort_inplace,sorting_algorithms/single_values/mergesort_inplace,sorting_algorithms/empty_input/mergesort_inplace,sorting_algorithms/large_input/mergesort_inplace) + Split the input into 2 equal-sized halves, call `mergesort_inplace` on them and then merge/interleave + the two sorted halves using an appropriate algorithm without allocating additional memory. + + ```c++ + void mergesort_inplace(std::vector::iterator begin, + std::vector::iterator end) { /* ... */ } + ``` + + Don't forget to catch the base case (sorting 0 or 1 elements)! + +6. [task][Heapsort](sorting_algorithms/heapsort,sorting_algorithms/all_elements_equal/heapsort,sorting_algorithms/reverse-sorted_values/heapsort,sorting_algorithms/single_values/heapsort,sorting_algorithms/empty_input/heapsort,sorting_algorithms/large_input/heapsort) + Construct a heap from the input. Then sort it with an appropriate algorithm. + + ```c++ + void heapsort(std::vector::iterator begin, + std::vector::iterator end) { /* ... */ } + ``` + +7. [task][Explicit Heapsort](sorting_algorithms/heapsort_explicit,sorting_algorithms/all_elements_equal/heapsort_explicit,sorting_algorithms/reverse-sorted_values/heapsort_explicit,sorting_algorithms/single_values/heapsort_explicit,sorting_algorithms/empty_input/heapsort_explicit,sorting_algorithms/large_input/heapsort_explicit) + Implement Heapsort without using `sort_heap`. + You do not need to understand the details of the algorithm. You should only look at the + documentation for heap algorithms. For example, refer to + [cppreference.com: `sort_heap`](https://en.cppreference.com/w/cpp/algorithm/sort_heap) + and figure out which algorithms need to be called. + + ```c++ + void heapsort_explicit(std::vector::iterator begin, + std::vector::iterator end) { /* ... */ } + ``` + +8. [task][Bogosort](bogosort,bogosort/empty_input,bogosort/single_value) + As long as the vector is not sorted, randomly shuffle the entire vector. + Alternatively, you can deterministically try all permutations until the vector is sorted. + + ```c++ + void bogosort(std::vector::iterator begin, + std::vector::iterator end) { /* ... */ } + ``` diff --git a/src/main/resources/templates/c_plus_plus/solution/.clang-format b/src/main/resources/templates/c_plus_plus/solution/.clang-format new file mode 100644 index 000000000000..541e6dca6c50 --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/solution/.clang-format @@ -0,0 +1,4 @@ +--- +Language: Cpp +BasedOnStyle: Google +IncludeBlocks: Preserve diff --git a/src/main/resources/templates/c_plus_plus/solution/.gitattributes b/src/main/resources/templates/c_plus_plus/solution/.gitattributes new file mode 100644 index 000000000000..0e4a9089ed6e --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/solution/.gitattributes @@ -0,0 +1,39 @@ +# Source: https://github.com/gitattributes/gitattributes/blob/master/C%2B%2B.gitattributes (01.09.2024) + +# Sources +*.c text diff=cpp +*.cc text diff=cpp +*.cxx text diff=cpp +*.cpp text diff=cpp +*.cpi text diff=cpp +*.c++ text diff=cpp +*.hpp text diff=cpp +*.h text diff=cpp +*.h++ text diff=cpp +*.hh text diff=cpp + +# Compiled Object files +*.slo binary +*.lo binary +*.o binary +*.obj binary + +# Precompiled Headers +*.gch binary +*.pch binary + +# Compiled Dynamic libraries +*.so binary +*.dylib binary +*.dll binary + +# Compiled Static libraries +*.lai binary +*.la binary +*.a binary +*.lib binary + +# Executables +*.exe binary +*.out binary +*.app binary diff --git a/src/main/resources/templates/c_plus_plus/solution/.gitignore b/src/main/resources/templates/c_plus_plus/solution/.gitignore new file mode 100644 index 000000000000..62f60adbb914 --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/solution/.gitignore @@ -0,0 +1,4 @@ +cmake-build-*/ + +.vscode/ +.idea/ diff --git a/src/main/resources/templates/c_plus_plus/solution/CMakeLists.txt b/src/main/resources/templates/c_plus_plus/solution/CMakeLists.txt new file mode 100644 index 000000000000..1a01a44252aa --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/solution/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.13) +project(ArtemisExercise) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_library(assignment src/sort.cpp) +target_include_directories(assignment PUBLIC include) + +add_executable(assignment_main src/main.cpp) +target_link_libraries(assignment_main assignment) diff --git a/src/main/resources/templates/c_plus_plus/solution/include/sort.hpp b/src/main/resources/templates/c_plus_plus/solution/include/sort.hpp new file mode 100644 index 000000000000..886fc4aeba92 --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/solution/include/sort.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include + +void selection_sort(std::vector::iterator begin, + std::vector::iterator end); + +void insertion_sort(std::vector::iterator begin, + std::vector::iterator end); + +void quicksort(std::vector::iterator begin, + std::vector::iterator end); + +void mergesort(std::vector::iterator begin, + std::vector::iterator end); + +void mergesort_inplace(std::vector::iterator begin, + std::vector::iterator end); + +void heapsort(std::vector::iterator begin, std::vector::iterator end); + +void heapsort_explicit(std::vector::iterator begin, + std::vector::iterator end); + +void bogosort(std::vector::iterator begin, std::vector::iterator end); diff --git a/src/main/resources/templates/c_plus_plus/solution/src/main.cpp b/src/main/resources/templates/c_plus_plus/solution/src/main.cpp new file mode 100644 index 000000000000..16b651caade9 --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/solution/src/main.cpp @@ -0,0 +1,5 @@ +#include "sort.hpp" + +int main() { + // Test your implementation here +} diff --git a/src/main/resources/templates/c_plus_plus/solution/src/sort.cpp b/src/main/resources/templates/c_plus_plus/solution/src/sort.cpp new file mode 100644 index 000000000000..2e091fae5c4d --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/solution/src/sort.cpp @@ -0,0 +1,82 @@ +#include "sort.hpp" + +#include +#include +#include + +void selection_sort(std::vector::iterator begin, + std::vector::iterator end) { + for (auto it = begin; it != end; ++it) { + auto min = std::min_element(it, end); + // std::iter_swap(min, it); // unstable + std::rotate(it, min, min + 1); + } +} + +void insertion_sort(std::vector::iterator begin, + std::vector::iterator end) { + for (auto it = begin; it != end; ++it) { + auto insertion_pos = std::upper_bound(begin, it, *it); + std::rotate(insertion_pos, it, it + 1); + } +} + +void quicksort(std::vector::iterator begin, + std::vector::iterator end) { + if (end - begin <= 1) { + return; + } + auto pivot = *begin; + auto middle = + std::partition(begin + 1, end, [pivot](int i) { return i < pivot; }); + auto new_middle = std::rotate(begin, begin + 1, middle); + quicksort(begin, new_middle); + quicksort(new_middle + 1, end); +} + +void mergesort(std::vector::iterator begin, + std::vector::iterator end) { + auto length = end - begin; + if (length <= 1) { + return; + } + std::vector tmp(begin, end); + auto middle = tmp.begin() + length / 2; + mergesort(tmp.begin(), middle); + mergesort(middle, tmp.end()); + std::merge(tmp.begin(), middle, middle, tmp.end(), begin); +} + +void mergesort_inplace(std::vector::iterator begin, + std::vector::iterator end) { + auto length = end - begin; + if (length <= 1) { + return; + } + auto middle = begin + length / 2; + mergesort_inplace(begin, middle); + mergesort_inplace(middle, end); + std::inplace_merge(begin, middle, end); +} + +void heapsort(std::vector::iterator begin, + std::vector::iterator end) { + std::make_heap(begin, end); + std::sort_heap(begin, end); +} + +void heapsort_explicit(std::vector::iterator begin, + std::vector::iterator end) { + std::make_heap(begin, end); + while (end != begin) { + std::pop_heap(begin, end); + --end; + } +} + +void bogosort(std::vector::iterator begin, + std::vector::iterator end) { + while (!std::is_sorted(begin, end)) { + std::next_permutation(begin, end); + } +} diff --git a/src/main/resources/templates/c_plus_plus/test/.clang-format b/src/main/resources/templates/c_plus_plus/test/.clang-format new file mode 100644 index 000000000000..541e6dca6c50 --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/test/.clang-format @@ -0,0 +1,4 @@ +--- +Language: Cpp +BasedOnStyle: Google +IncludeBlocks: Preserve diff --git a/src/main/resources/templates/c_plus_plus/test/.gitattributes b/src/main/resources/templates/c_plus_plus/test/.gitattributes new file mode 100644 index 000000000000..0e4a9089ed6e --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/test/.gitattributes @@ -0,0 +1,39 @@ +# Source: https://github.com/gitattributes/gitattributes/blob/master/C%2B%2B.gitattributes (01.09.2024) + +# Sources +*.c text diff=cpp +*.cc text diff=cpp +*.cxx text diff=cpp +*.cpp text diff=cpp +*.cpi text diff=cpp +*.c++ text diff=cpp +*.hpp text diff=cpp +*.h text diff=cpp +*.h++ text diff=cpp +*.hh text diff=cpp + +# Compiled Object files +*.slo binary +*.lo binary +*.o binary +*.obj binary + +# Precompiled Headers +*.gch binary +*.pch binary + +# Compiled Dynamic libraries +*.so binary +*.dylib binary +*.dll binary + +# Compiled Static libraries +*.lai binary +*.la binary +*.a binary +*.lib binary + +# Executables +*.exe binary +*.out binary +*.app binary diff --git a/src/main/resources/templates/c_plus_plus/test/.gitignore b/src/main/resources/templates/c_plus_plus/test/.gitignore new file mode 100644 index 000000000000..d779962e6971 --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/test/.gitignore @@ -0,0 +1,10 @@ +/${studentParentWorkingDirectoryName}/ +/test-reports/ +/build/ + +cmake-build-*/ + +.vscode/ +.idea/ + +__pycache__/ diff --git a/src/main/resources/templates/c_plus_plus/test/CMakeLists.txt b/src/main/resources/templates/c_plus_plus/test/CMakeLists.txt new file mode 100644 index 000000000000..fe378768d813 --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/test/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.13) +project(ArtemisTest) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +include(CTest) + +find_package(Catch2 3.0 REQUIRED) + +add_subdirectory("${studentParentWorkingDirectoryName}") + +add_executable(sort-test src/sort-test.cpp) +target_link_libraries(sort-test assignment Catch2::Catch2WithMain) +add_test(NAME sort-test COMMAND sort-test) diff --git a/src/main/resources/templates/c_plus_plus/test/Tests.py b/src/main/resources/templates/c_plus_plus/test/Tests.py new file mode 100755 index 000000000000..1c062fef90fe --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/test/Tests.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 + +from tests.TestCompile import TestCompile +from tests.TestConfigure import TestConfigure +from tests.TestCatch2 import TestCatch2 +from testUtils.Tester import Tester + + +def main() -> None: + # Create a new instance of the tester: + tester: Tester = Tester() + + buildDir = "./build" + + # Register all test cases: + # Configure: + testConfigure: TestConfigure = TestConfigure(".", buildDir, + ["-DCMAKE_BUILD_TYPE=Debug", + "-DCMAKE_CXX_FLAGS=-fsanitize=address", + "-DCMAKE_EXE_LINKER_FLAGS=-fsanitize=address"]) + tester.addTest(testConfigure) + tester.addTest(TestCompile(buildDir, "sort-test", requirements=[testConfigure.name], name="CompileSort")) + tester.addTest(TestCatch2(buildDir, "sort-test", ["CompileSort"])) + + # Run the actual tests: + tester.run() + # Export the results into the JUnit XML format: + tester.exportResult("./test-reports/tests-results.xml") + + +if __name__ == "__main__": + main() diff --git a/src/main/resources/templates/c_plus_plus/test/src/sort-test.cpp b/src/main/resources/templates/c_plus_plus/test/src/sort-test.cpp new file mode 100644 index 000000000000..6216ce9aa745 --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/test/src/sort-test.cpp @@ -0,0 +1,122 @@ +#include "sort.hpp" + +#include +#include +#include +#include + +#include + +void run_all_algorithms(std::vector& values, + const std::vector& expected) { + SECTION("selection_sort") { + selection_sort(values.begin(), values.end()); + REQUIRE(values == expected); + } + SECTION("insertion_sort") { + insertion_sort(values.begin(), values.end()); + REQUIRE(values == expected); + } + SECTION("quicksort") { + quicksort(values.begin(), values.end()); + REQUIRE(values == expected); + } + SECTION("mergesort") { + mergesort(values.begin(), values.end()); + REQUIRE(values == expected); + } + SECTION("mergesort_inplace") { + mergesort_inplace(values.begin(), values.end()); + REQUIRE(values == expected); + } + SECTION("heapsort") { + heapsort(values.begin(), values.end()); + REQUIRE(values == expected); + } + SECTION("heapsort_explicit") { + heapsort_explicit(values.begin(), values.end()); + REQUIRE(values == expected); + } +} + +TEST_CASE("sorting_algorithms") { + std::vector values{6, 2, 4, 2, 1, 7, 0, 2, 3, 4, 8}; + std::vector expected{0, 1, 2, 2, 2, 3, 4, 4, 6, 7, 8}; + + run_all_algorithms(values, expected); +} + +TEST_CASE("sorting_algorithms/all_elements_equal") { + std::vector values(20, 1); + auto expected = values; + + // just to make sure your code doesn't crash on repeated values + run_all_algorithms(values, expected); +} + +TEST_CASE("sorting_algorithms/reverse-sorted_values") { + std::vector values(20, 1); + std::iota(values.begin(), values.end(), 0); + auto expected = values; + std::reverse(values.begin(), values.end()); + + run_all_algorithms(values, expected); +} + +TEST_CASE("sorting_algorithms/single_values") { + std::vector values{4}; + std::vector expected{4}; + + // just to make sure your code doesn't crash on single values + run_all_algorithms(values, expected); +} + +TEST_CASE("sorting_algorithms/empty_input") { + std::vector values; + std::vector expected; + + // just to make sure your code doesn't crash on empty inputs + run_all_algorithms(values, expected); +} + +TEST_CASE("sorting_algorithms/large_input") { + std::vector values; + std::uniform_int_distribution dist{0, 50}; + std::default_random_engine rng; // default seed + for (int i = 0; i < 100; ++i) { + values.push_back(dist(rng)); + } + auto expected = values; + std::sort(expected.begin(), expected.end()); + + run_all_algorithms(values, expected); +} + +TEST_CASE("bogosort") { + // bogosort only works for very small inputs, + // large inputs take forever + std::vector values{6, 2, 4, 2}; + std::vector expected{2, 2, 4, 6}; + + bogosort(values.begin(), values.end()); + + REQUIRE(values == expected); +} + +TEST_CASE("bogosort/empty_input") { + std::vector values{}; + std::vector expected{}; + + bogosort(values.begin(), values.end()); + + REQUIRE(values == expected); +} + +TEST_CASE("bogosort/single_value") { + std::vector values{3}; + std::vector expected{3}; + + bogosort(values.begin(), values.end()); + + REQUIRE(values == expected); +} diff --git a/src/main/resources/templates/c_plus_plus/test/testUtils/AbstractProgramTest.py b/src/main/resources/templates/c_plus_plus/test/testUtils/AbstractProgramTest.py new file mode 100644 index 000000000000..5c080913c17c --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/test/testUtils/AbstractProgramTest.py @@ -0,0 +1,42 @@ +from typing import List, Optional + +from testUtils.AbstractTest import AbstractTest +from testUtils.Utils import PWrap + + +class AbstractProgramTest(AbstractTest): + """ + A abstract test that every test executing an external program has to inherit from. + How to: + 1. Inherit from AbstractProgramTest + 2. Override the "_run()" method. + 3. Done + """ + + # Our process wrapper instance: + pWrap: Optional[PWrap] + # The location of the executable: + executionDirectory: str + # The name of the executable that should get executed: + executable: str + + def __init__(self, name: str, executionDirectory: str, executable: str, requirements: List[str] = None, timeoutSec: int = -1): + super(AbstractProgramTest, self).__init__(name, requirements, timeoutSec) + self.executionDirectory: str = executionDirectory + self.executable: str = executable + self.pWrap: Optional[PWrap] = None + + def _onTimeout(self): + self._terminateProgramm() + + def _onFailed(self): + self._terminateProgramm() + + def _terminateProgramm(self): + if self.pWrap: + if not self.pWrap.hasTerminated(): + self.pWrap.kill() + self.pWrap.cleanup() + + def _progTerminatedUnexpectedly(self): + self._failWith("Program terminated unexpectedly.") diff --git a/src/main/resources/templates/c_plus_plus/test/testUtils/AbstractTest.py b/src/main/resources/templates/c_plus_plus/test/testUtils/AbstractTest.py new file mode 100644 index 000000000000..18040680de98 --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/test/testUtils/AbstractTest.py @@ -0,0 +1,269 @@ +from abc import ABC, abstractmethod +from contextlib import contextmanager, suppress +from datetime import datetime, timedelta +from os import makedirs, path +from signal import alarm, SIG_IGN, SIGALRM, signal +from traceback import print_exc +from typing import Dict, List, Optional, NoReturn +from xml.etree import ElementTree as Et + +from testUtils.junit.TestCase import Result, TestCase +from testUtils.junit.TestSuite import TestSuite +from testUtils.TestFailedError import TestFailedError +from testUtils.Utils import printTester, PWrap + + +# Timeout handler based on: https://www.jujens.eu/posts/en/2018/Jun/02/python-timeout-function/ +class AbstractTest(ABC): + """ + An abstract test that every test has to inherit from. + How to: + 1. Inherit from AbstractTest + 2. Override the "_run()" method. + 3. Override the "_onTimeout()" method. + 4. Override the "_onFailed()" method. + 5. Done + """ + + name: str + requirements: List[str] + timeoutSec: int + case: Optional[TestCase] + suite: Optional[TestSuite] + additionalSuites: List[Et.Element] + + def __init__(self, name: str, requirements: Optional[List[str]] = None, timeoutSec: int = -1) -> None: + """ + name: str + An unique test case name. + + requirements: List[str] + A list of test cases names that have to finish successfully for this test to run. + Usually an execution test should have the compile test as its requirement. + + timeoutSec: int + The test case timeout in seconds, + """ + + self.name = name + self.timeoutSec = timeoutSec + self.requirements = [] if requirements is None else requirements + + self.case: Optional[TestCase] = None + self.suite: Optional[TestSuite] = None + + def start(self, testResults: Dict[str, Result], suite: TestSuite, additionalSuites: List[TestSuite]) -> None: + """ + Starts the test run. + + --- + + testResults: Dict[str, Result] + All test results up to this point. + + suite: TestSuite + The test suite where this test should get added to. + """ + + self.suite = suite + self.additionalSuites = additionalSuites + self.case = TestCase(self.name) + + # Check if all test requirements (other tests) are fulfilled: + if not self.__checkTestRequirements(testResults): + printTester(f"Skipping test case '{self.name}' not all requirements ({self.requirements!s}) are fulfilled") + self.case.message = f"Test requires other test cases to succeed first ({self.requirements!s})" + self.case.result = Result.SKIPPED + self.case.stdout = "" + self.case.stderr = "" + self.case.time = timedelta() + self.suite.addCase(self.case) + return + + startTime: datetime = datetime.now() + + self._initOutputDirectory() + + if self.timeoutSec > 0: + # Run with timeout: + with self.__timeout(self.timeoutSec): + try: + self._run() + except TestFailedError: + printTester(f"'{self.name}' failed.") + except TimeoutError: + self._timeout() + except Exception as e: + self.__markAsFailed(f"'{self.name}' had an internal error. {e}.\nPlease report this to an instructor!") + print_exc() + self._onFailed() + else: + # Run without timeout: + try: + self._run() + except TestFailedError: + printTester(f"'{self.name}' failed.") + except Exception as e: + self.__markAsFailed(f"'{self.name}' had an internal error. {e}.\nPlease report this to an instructor!") + print_exc() + self._onFailed() + + self.case.time = datetime.now() - startTime + self.suite.addCase(self.case) + + def __checkTestRequirements(self, testResults: Dict[str, Result]) -> bool: + """ + Checks if all requirements (i.e. other test cases were successful) are fulfilled. + """ + + return all(testResults.get(req) == Result.SUCCESS for req in self.requirements) + + @contextmanager + def __timeout(self, timeoutSec: int): + # Register a function to raise a TimeoutError on the signal. + signal(SIGALRM, self.__raiseTimeout) + # Schedule the signal to be sent after ``time``. + alarm(timeoutSec) + + with suppress(TimeoutError): + yield + # Unregister the signal so it won't be triggered + # if the timeout is not reached. + signal(SIGALRM, SIG_IGN) + + def __raiseTimeout(self, _sigNum: int, _frame) -> NoReturn: + self._onTimeout() + raise TimeoutError + + def _failWith(self, msg: str) -> NoReturn: + """ + Marks the current test as failed with the given message. + Stores the complete stderr and stdout output from the run. + """ + + self.__markAsFailed(msg) + self._onFailed() + raise TestFailedError(f"{self.name} failed.") + + def __markAsFailed(self, msg: str) -> None: + """ + Marks the current test case as failed and loads all stdout and stderr. + """ + + self.case.message = msg + self.case.result = Result.FAILURE + self.case.stdout = self._loadFullStdout() + self.case.stderr = self._loadFullStderr() + printTester(f"Test {self.name} failed with: {msg}") + + def _timeout(self, msg: str = "") -> None: + """ + Marks the current test as failed with the given optional message. + Stores the complete stderr and stdout output from the run. + Should be called once a test timeout occurred. + """ + + if msg: + self.__markAsFailed(f"timeout ({msg})") + else: + self.__markAsFailed("timeout") + + def __loadFileContent(self, filePath: str) -> str: + """ + Returns the content of a file specified by filePath as string. + """ + if path.exists(filePath) and path.isfile(filePath): + with open(filePath, "r") as file: + content: str = file.read() + return content + return "" + + def _loadFullStdout(self) -> str: + """ + Returns the stdout output of the executable. + """ + filePath: str = self._getStdoutFilePath() + return self.__loadFileContent(filePath) + + def _loadFullStderr(self) -> str: + """ + Returns the stderr output of the executable. + """ + + filePath: str = self._getStderrFilePath() + return self.__loadFileContent(filePath) + + def _initOutputDirectory(self) -> None: + """ + Prepares the output directory for the stderr and stdout files. + """ + outDir: str = self._getOutputPath() + if path.exists(outDir) and path.isdir(outDir): + return + makedirs(outDir) + + def _getOutputPath(self) -> str: + """ + Returns the output path for temporary stuff like the stderr and stdout files. + """ + + return path.join("/tmp", self.suite.name, self.name) + + def _getStdoutFilePath(self) -> str: + """ + Returns the path of the stdout cache file. + """ + + return path.join(self._getOutputPath(), "stdout.txt") + + def _getStderrFilePath(self) -> str: + """ + Returns the path of the stderr cache file. + """ + + return path.join(self._getOutputPath(), "stderr.txt") + + def _createPWrap(self, cmd: List[str], cwd: Optional[str] = None) -> PWrap: + """ + Creates a new PWrap instance from the given command. + """ + + return PWrap(cmd, self._getStdoutFilePath(), self._getStderrFilePath(), cwd=cwd) + + def _startPWrap(self, pWrap: PWrap) -> None: + """ + Starts the PWrap execution. + Handles FileNotFoundError if, for example, the executable was not found or does not exist. + """ + + try: + pWrap.start() + except FileNotFoundError as fe: + printTester(str(fe)) + self._failWith("File not found for execution. Did compiling fail?") + except NotADirectoryError as de: + printTester(str(de)) + self._failWith(f"Directory '{pWrap.cwd}' does not exist.") + except PermissionError as pe: + printTester(str(pe)) + self._failWith("Missing file execution permission. Make sure it has execute rights (chmod +x ).") + + @abstractmethod + def _run(self): + """ + Implement your test run here. + """ + + @abstractmethod + def _onTimeout(self): + """ + Called once a timeout occurs. + Should cancel all outstanding actions and free all resources. + """ + + @abstractmethod + def _onFailed(self): + """ + Called once the test failed via "_failWith(msg: str)". + Should cancel all outstanding actions and free all allocated resources. + """ diff --git a/src/main/resources/templates/c_plus_plus/test/testUtils/TestFailedError.py b/src/main/resources/templates/c_plus_plus/test/testUtils/TestFailedError.py new file mode 100644 index 000000000000..00ac65a80ce8 --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/test/testUtils/TestFailedError.py @@ -0,0 +1,6 @@ +class TestFailedError(Exception): + """ + Raised when a test failed. + """ + + pass diff --git a/src/main/resources/templates/c_plus_plus/test/testUtils/Tester.py b/src/main/resources/templates/c_plus_plus/test/testUtils/Tester.py new file mode 100644 index 000000000000..eae56143c44e --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/test/testUtils/Tester.py @@ -0,0 +1,81 @@ +from typing import Dict, List + +from testUtils.AbstractTest import AbstractTest +from testUtils.junit.Junit import Junit +from testUtils.junit.TestCase import Result +from testUtils.junit.TestSuite import TestSuite +from xml.etree import ElementTree as Et +from testUtils.Utils import clearTesterOutputCache, getTesterOutput, printTester, resetStdoutLimit, setStdoutLimitEnabled + + +class Tester: + name: str + suite: TestSuite + additionalSuites: List[Et.Element] + tests: Dict[str, AbstractTest] + + def __init__(self, name: str = "GBS-Tester-1.36") -> None: + self.name = name + self.suite = TestSuite(name) + self.additionalSuites = [] + self.tests = {} + + def run(self) -> None: + """ + Starts the tester and runs all tests added via "addTest(test: AbstractTest)". + """ + + setStdoutLimitEnabled(False) + printTester(f"Running: {self.name}") + + # A dictionary of test results: + # Test name -> result + testResults: Dict[str, Result] = {} + + for name, test in self.tests.items(): + if test.timeoutSec >= 0: + printTester(f"Running test case '{name}' with a {test.timeoutSec} second timeout...") + else: + printTester(f"Running test case '{name}' with no timeout...") + + # Reset the tester output cache: + resetStdoutLimit() + setStdoutLimitEnabled(True) + clearTesterOutputCache() + + test.start(testResults, self.suite, self.additionalSuites) + + setStdoutLimitEnabled(False) + printTester(f"Finished test case '{name}' in {test.case.time.total_seconds()} seconds.") + + # Store the tester output in the test case: + test.case.testerOutput = self.name + "\n" + getTesterOutput() + # Update test results: + testResults[name] = test.case.result + self.__printResult() + + def addTest(self, test: AbstractTest) -> None: + """ + Adds a new test that will be run once "run()" is invoked. + """ + + if test.name in self.tests: + raise ValueError(f"Test '{test.name}' already registered. Test names should be unique!") + self.tests[test.name] = test + + def __printResult(self) -> None: + print("Result".center(50, "=")) + print(f"{self.name} finished {len(self.tests)} test cases in {self.suite.time.total_seconds()} seconds.") + print(f"SUCCESS: {self.suite.successful}") + print(f"FAILED: {self.suite.failures}") + print(f"ERROR: {self.suite.errors}") + print(f"SKIPPED: {self.suite.skipped}") + print("".center(50, "=")) + + def exportResult(self, outputPath: str) -> None: + """ + Exports the test results into a JUnit format and stores it at the given outputPath. + """ + + junit: Junit = Junit(self.suite, self.additionalSuites) + junit.toXml(outputPath) diff --git a/src/main/resources/templates/c_plus_plus/test/testUtils/Utils.py b/src/main/resources/templates/c_plus_plus/test/testUtils/Utils.py new file mode 100644 index 000000000000..25c28bb4553e --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/test/testUtils/Utils.py @@ -0,0 +1,532 @@ +import os +import select +import signal +from datetime import datetime +from io import TextIOWrapper +from pty import openpty +from pwd import getpwnam, struct_passwd +from subprocess import Popen +from termios import ONLCR, tcgetattr, TCSANOW, tcsetattr +from threading import Thread +from time import sleep +from typing import Any, Dict, List, Optional + + +def studSaveStrComp(ref: str, other: str, strip: bool = True, ignoreCase: bool = True, ignoreNonAlNum=True): + """ + Student save compare between strings. + Converts both to lower, strips them and removes all non alphanumeric chars + before comparison. + """ + # Strip: + if strip: + ref = ref.strip() + other = other.strip() + + # Convert to lower + if ignoreCase: + ref = ref.lower() + other = other.lower() + + # Remove all non alphanumeric chars: + if ignoreNonAlNum: + ref = "".join(c for c in ref if c.isalnum()) + other = "".join(c for c in other if c.isalnum()) + + # print("Ref: {}\nOther:{}".format(ref, other)) + return ref == other + + +def recursive_chmod(path: str, mode: int): + """ + Recursively changes file permissions. + """ + os.chmod(path, mode) + # print("CHMOD: {}".format(path)) + f: str + for f in os.listdir(path): + f = os.path.join(path, f) + if os.path.isdir(f): + recursive_chmod(f, mode) + else: + os.chmod(f, mode) + # print("CHMOD: {}".format(f)) + + +# Limit for stdout in chars. +# Should prevent to much output on artemis if for example there is a loop in a tree. +# By default the stdout limit is disabled: +__stdoutLimitEnabled: bool = False + + +def resetStdoutLimit(limit: int = 15000): + """ + Resets the stout limit to the given limit (default = 15.000 chars). + """ + global stdoutCharsLeft # Required since we want to modify stdoutCharsLeft + stdoutCharsLeft = limit + + +def setStdoutLimitEnabled(enabled: bool): + """ + Enables or disables the stdout limit. + Does not restet the chars left! + """ + global __stdoutLimitEnabled + __stdoutLimitEnabled = enabled + + +def __printStdout(text: str): + """ + Prints the given text to stdout. + Only if there are still enough chars in stdoutCharsLeft left. + Else will not print anything. + """ + global stdoutCharsLeft # Required since we want to modify stdoutCharsLeft + + if not __stdoutLimitEnabled: + print(text) + elif stdoutCharsLeft > 0: + if stdoutCharsLeft >= len(text): + print(text) + else: + print(text[:stdoutCharsLeft] + "...") + stdoutCharsLeft -= len(text) + if stdoutCharsLeft <= 0: + print("[STDOUT LIMIT REACHED]".center(50, "=")) + + +# A cache of all that the tester has been writing to stdout: +testerOutputCache: List[str] = list() + + +def clearTesterOutputCache(): + """ + Clears the testerOutputCache. + """ + testerOutputCache.clear() + + +def getTesterOutput(): + """ + Returns the complete tester output as a single string. + """ + return "\n".join(testerOutputCache) + + +startTime: datetime = datetime.now() + + +def __getCurSeconds(): + """ + Returns the total seconds passed, since the tester started as a string with a precision of two digits. + """ + seconds: float = (datetime.now() - startTime).total_seconds() + return str(round(seconds, 2)) + + +def __getCurDateTimeStr(): + """ + Returns the current date and time string (e.g. 11.10.2019_17:02:33) + """ + return datetime.now().strftime("%d.%m.%Y_%H:%M:%S") + + +def printTester(text: str, addToCache: bool = True): + """ + Prints the given string with the '[T]: ' tag in front. + Should be used instead of print() to make it easier for students + to determine what came from the tester and what from their program. + """ + msg: str = f"[{__getCurSeconds()}][T]: {text}" + __printStdout(msg) + if addToCache: + testerOutputCache.append(msg) + + +def printProg(text: str, addToCache: bool = True): + """ + Prints the given string with the '[P]: ' tag in front. + Should be used instead of print() to make it easier for students + to determine what came from the tester and what from their program. + """ + msg: str = f"[{__getCurSeconds()}][P]: {text.rstrip()}" + __printStdout(msg) + if addToCache: + testerOutputCache.append(msg) + + +def shortenText(text: str, maxNumChars: int): + """ + Shortens the given text to a maximum number of chars. + If there are more chars than specified in maxNumChars, + it will append: "\n[And {} chars more...]". + """ + + if len(text) > maxNumChars: + s: str = f"\n[And {len(text) - maxNumChars} chars more...]" + l: int = maxNumChars - len(s) + if l > 0: + return f"{text[:l]}{s}" + else: + printTester(f"Unable to limit output to {maxNumChars} chars! Not enough space.", False) + return "" + return text + + +class ReadCache(Thread): + """ + Helper class that makes sure we only get one line (separated by '\n') + if we read multiple lines at once. + """ + + __cacheList: List[str] + __cacheFile: TextIOWrapper + + __outFd: int + __outSlaveFd: int + + def __init__(self, filePath: str): + Thread.__init__(self) + self.__cacheList = [] + self.__cacheFile = open(filePath, "w") + + # Emulate a terminal: + self.__outFd, self.__outSlaveFd = openpty() + + self.start() + + def fileno(self): + return self.__outFd + + def join(self, timeout: float = None): + try: + os.close(self.__outFd) + except OSError as e: + printTester(f"Closing stdout FD failed with: {e}") + try: + os.close(self.__outSlaveFd) + except OSError as e: + printTester(f"Closing stdout slave FD failed with: {e}") + Thread.join(self, timeout) + + @staticmethod + def __isFdValid(fd: int): + try: + os.stat(fd) + except OSError: + return False + return True + + @staticmethod + def __decode(data: bytes): + """ + Tries to decode the given string as UTF8. + In case this fails, it will fall back to ASCII encoding. + Returns the decoded result. + + --- + + data: bytes + The data that should be decoded. + """ + try: + return data.decode("utf8", "replace") + except UnicodeDecodeError as e: + printTester(f"Failed to decode line as utf8. Using ascii ecoding - {e}") + return data.decode("ascii", "replace") + + def run(self): + pollObj = select.poll() + pollObj.register(self.__outSlaveFd, select.POLLIN) + while self.__isFdValid(self.__outSlaveFd): + try: + for fd, mask in pollObj.poll(100): + if fd != self.__outSlaveFd: + continue + if mask & (select.POLLHUP | select.POLLERR | select.POLLNVAL): + return + if mask & select.POLLIN: + data: bytes = os.read(self.__outSlaveFd, 4096) + dataStr: str = self.__decode(data) + try: + self.__cacheFile.write(dataStr) + except UnicodeEncodeError: + printTester("Invalid ASCII character read. Skipping line...") + continue + self.__cacheFile.flush() + self.__cache(dataStr) + printProg(dataStr) + except OSError: + break + + def canReadLine(self): + return len(self.__cacheList) > 0 + + def __cache(self, data: str): + self.__cacheList.extend(data.splitlines(True)) + + def readLine(self): + if self.canReadLine(): + return self.__cacheList.pop(0) + return "" + + +class PWrap: + """ + A wrapper for "Popen". + """ + + cmd: List[str] + prog: Optional[Popen] + cwd: str + + __stdinFd: int + __stdinMasterFd: int + + __stdOutLineCache: ReadCache + __stdErrLineCache: ReadCache + + __terminatedTime: Optional[datetime] + + def __init__(self, cmd: List[str], stdoutFilePath: str = "/tmp/stdout.txt", stderrFilePath: str = "/tmp/stderr.txt", cwd: Optional[str] = None): + self.cmd = cmd + self.prog = None + self.cwd: str = os.getcwd() if cwd is None else cwd + self.stdout = open(stdoutFilePath, "wb") + self.stderr = open(stderrFilePath, "wb") + + self.__stdOutLineCache = ReadCache(stdoutFilePath) + self.__stdErrLineCache = ReadCache(stderrFilePath) + + self.__terminatedTime = None + + def __del__(self): + try: + os.close(self.__stdinFd) + except OSError as e: + printTester(f"Closing stdin FD failed with: {e}") + except AttributeError: + pass + try: + os.close(self.__stdinMasterFd) + except OSError as e: + printTester(f"Closing stdin master FD failed with: {e}") + except AttributeError: + pass + + def start(self, userName: Optional[str] = None): + """ + Starts the process and sets all file descriptors to nonblocking. + + --- + + userName: Optional[str] = None + In case the userName is not None, the process will be executed as the given userName. + This requires root privileges and you have to ensure the user has the required rights to access all resources (files). + """ + # Emulate a terminal for stdin: + self.__stdinMasterFd, self.__stdinFd = openpty() + + # Transform "\r\n" to '\n' for data send to stdin: + tsettings: List[Any] = tcgetattr(self.__stdinFd) + tsettings[1] &= ~ONLCR + tcsetattr(self.__stdinFd, TCSANOW, tsettings) + + if userName is not None: + # Check for root privileges: + self.__checkForRootPrivileges() + + # Prepare environment: + pwRecord: struct_passwd = getpwnam(userName) + env: Dict[str, str] = os.environ.copy() + env["HOME"] = pwRecord.pw_dir + env["LOGNAME"] = pwRecord.pw_name + env["USER"] = pwRecord.pw_name + env["PWD"] = self.cwd + printTester(f"Starting process as: {pwRecord.pw_name}") + + # Start the actual process: + self.prog = Popen( + self.cmd, + stdout=self.__stdOutLineCache.fileno(), + stdin=self.__stdinMasterFd, + stderr=self.__stdErrLineCache.fileno(), + universal_newlines=True, + cwd=self.cwd, + env=env, + preexec_fn=self.__demote(pwRecord.pw_uid, pwRecord.pw_gid, pwRecord.pw_name), + ) + else: + # Start the actual process: + self.prog = Popen( + self.cmd, + stdout=self.__stdOutLineCache.fileno(), + stdin=self.__stdinMasterFd, + stderr=self.__stdErrLineCache.fileno(), + universal_newlines=True, + cwd=self.cwd, + preexec_fn=os.setsid, + ) # Make sure we store the process group id + + def __demote(self, userUid: int, userGid: int, userName: str): + """ + Returns a call, demoting the calling process to the given user, UID and GID. + """ + + def result(): + # self.__printIds("Starting demotion...") # Will print inside the new process and reports via the __stdOutLineCache + os.initgroups(userName, userGid) + os.setuid(userUid) + # self.__printIds("Finished demotion.") # Will print inside the new process and reports via the __stdOutLineCache + + return result + + @staticmethod + def __checkForRootPrivileges(): + """ + Checks if the current process has root permissions. + Fails if not. + """ + if os.geteuid() != 0: + raise PermissionError("The tester has to be executed as root to be able to switch users!") + + def __printIds(self, msg: str): + printTester(f"uid, gid = {os.getuid()}, {os.getgid()}; {msg}") + + def __readLine(self, lineCache: ReadCache, blocking: bool): + """ + Reads a single line from the given ReadCache and returns it. + + --- + + blocking: + When set to True will only return if the process terminated or we read a non empty string. + """ + while blocking: + if not lineCache.canReadLine(): + if not self.hasTerminated(): + sleep(0.1) + else: + break + else: + line: str = lineCache.readLine() + return line + return "" + + def readLineStdout(self, blocking: bool = True): + """ + Reads a single line from the processes stdout and returns it. + + --- + + blocking: + When set to True will only return if the process terminated or we read a non empty string. + """ + return self.__readLine(self.__stdOutLineCache, blocking) + + def canReadLineStdout(self): + """ + Returns whether there is a line from the processes stdout that can be read. + """ + return self.__stdOutLineCache.canReadLine() + + def readLineStderr(self, blocking: bool = True): + """ + Reads a single line from the processes stderr and returns it. + + --- + + blocking: + When set to True will only return if the process terminated or we read a non empty string. + """ + return self.__readLine(self.__stdErrLineCache, blocking) + + def canReadLineStderr(self): + """ + Returns whether there is a line from the processes stderr that can be read. + """ + return self.__stdErrLineCache.canReadLine() + + def writeStdin(self, data: str): + """ + Writes the given data string to the processes stdin. + """ + os.write(self.__stdinFd, data.encode()) + printTester(f"Wrote: {data}") + + def hasTerminated(self): + """ + Returns whether the process has terminated. + """ + if self.prog is None: + return True + + # Make sure we wait 1.0 seconds after the process has terminated to + # make sure all the output arrived: + elif self.prog.poll() is not None: + if self.__terminatedTime: + if (datetime.now() - self.__terminatedTime).total_seconds() > 1.0: + return True + else: + self.__terminatedTime = datetime.now() + return False + + def getReturnCode(self): + """ + Returns the returncode of the terminated process else None. + """ + return self.prog.returncode + + def waitUntilTerminationReading(self, secs: float = -1): + """ + Waits until termination of the process and tries to read until either + the process terminated or the timeout occurred. + + Returns True if the process terminated before the timeout occurred, + else False. + + --- + + secs: + The timeout in seconds. Values < 0 result in infinity. + """ + start: datetime = datetime.now() + while True: + if self.hasTerminated(): + return True + elif 0 <= secs <= (datetime.now() - start).total_seconds(): + return False + self.readLineStdout(False) + sleep(0.1) + + def kill(self, signal: int = signal.SIGKILL): + """ + Sends the given signal to the complete process group started by the process. + + Returns True if the process existed and had to be killed. Else False. + + --- + + signal: + The signal that should be sent to the process group started by the process. + """ + # Send a signal to the complete process group: + try: + os.killpg(os.getpgid(self.prog.pid), signal) + return True + except ProcessLookupError: + printTester("No need to kill process. Process does not exist any more.") + return False + + def cleanup(self): + """ + Should be called once the execution has terminated. + Will join the stdout and stderr reader threads. + """ + + self.__stdOutLineCache.join() + self.__stdErrLineCache.join() + + def getPID(self): + return self.prog.pid diff --git a/src/main/resources/templates/c_plus_plus/test/testUtils/junit/Junit.py b/src/main/resources/templates/c_plus_plus/test/testUtils/junit/Junit.py new file mode 100644 index 000000000000..12aaa288889b --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/test/testUtils/junit/Junit.py @@ -0,0 +1,33 @@ +from os import chmod, makedirs, path +from typing import Tuple, List +from xml.etree import ElementTree as Et + +from testUtils.junit.TestSuite import TestSuite + + +# JUnit format: https://github.com/junit-team/junit5/blob/master/platform-tests/src/test/resources/jenkins-junit.xsd +class Junit: + suite: TestSuite + additionalSuites: List[Et.Element] + + def __init__(self, suite: TestSuite, additionalSuites: List[Et.Element]) -> None: + self.suite = suite + self.additionalSuites = additionalSuites + + def toXml(self, outputPath: str) -> None: + suiteXml: Et.Element = self.suite.toXml() + root: Et.Element = Et.Element("testsuites") + root.append(suiteXml) + root.extend(self.additionalSuites) + tree: Et.ElementTree = Et.ElementTree(root) + self.createOutputPath(outputPath) + tree.write(outputPath, xml_declaration=True) + # Ensure nobody can edit our results: + chmod(outputPath, 0o644) + + @staticmethod + def createOutputPath(outputPath: str) -> None: + paths: Tuple[str, str] = path.split(outputPath) + if paths[0] and not path.exists(paths[0]): + # Prevent others from writing in this folder: + makedirs(paths[0], mode=0o755) diff --git a/src/main/resources/templates/c_plus_plus/test/testUtils/junit/TestCase.py b/src/main/resources/templates/c_plus_plus/test/testUtils/junit/TestCase.py new file mode 100644 index 000000000000..8bb7e1392bc7 --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/test/testUtils/junit/TestCase.py @@ -0,0 +1,75 @@ +from datetime import timedelta +from enum import Enum +from xml.etree import ElementTree as Et + +from testUtils.Utils import shortenText + + +class Result(Enum): + SKIPPED = "skipped" + ERROR = "error" + FAILURE = "failure" + SUCCESS = "success" + + +class TestCase: + stdout: str + stderr: str + testerOutput: str + + name: str + time: timedelta + result: Result + message: str + + def __init__(self, name: str) -> None: + self.name = name + + self.stdout: str = "" + self.stderr: str = "" + self.testerOutput: str = "" + self.time: timedelta = timedelta() + self.result: Result = Result.SUCCESS + self.message: str = "" + + def toXml(self, suite: Et.Element, maxCharsPerOutput: int = 2500) -> None: + case: Et.Element = Et.SubElement(suite, "testcase") + case.set("name", self.name) + case.set("time", str(self.time.total_seconds())) + + if self.result != Result.SUCCESS: + result: Et.Element = Et.SubElement(case, self.result.value) + result.set("message", self.message) + result.text = self.genErrFailureMessage() + + if self.stdout: + stdout: Et.Element = Et.SubElement(case, "system-out") + stdout.text = shortenText(self.stdout, maxCharsPerOutput) + "\n" + if self.stderr: + stderr: Et.Element = Et.SubElement(case, "system-err") + stderr.text = shortenText(self.stderr, maxCharsPerOutput) + "\n" + + def genErrFailureMessage(self, maxChars: int = 5000) -> str: + oneThird: int = maxChars // 3 + + # Limit the stderr output to one third of the available chars: + stderrMsg: str = "\n" + "stderr".center(50, "=") + "\n" + if self.stderr: + stderrMsg += shortenText(self.stderr, oneThird) + "\n" + else: + stderrMsg += "No output on stderr found!\n" + + # Limit the stdout output to one third + the unused chars from the stderr output: + stdoutMsg: str = "\n" + "stdout".center(50, "=") + "\n" + if self.stdout: + stdoutMsg += shortenText(self.stdout, oneThird + (oneThird - len(stderrMsg))) + "\n" + else: + stdoutMsg += "No output on stdout found!\n" + + # Limit the tester output to one third + the left overs from stderr and stdout: + testerMsg: str = "\n" + "Tester".center(50, "=") + "\n" + if self.testerOutput: + testerMsg += shortenText(self.testerOutput, maxChars - len(testerMsg) - len(stderrMsg) - len(stdoutMsg)) + "\n" + else: + testerMsg += "No tester output found!\n" + return self.message + stdoutMsg + stderrMsg + testerMsg diff --git a/src/main/resources/templates/c_plus_plus/test/testUtils/junit/TestSuite.py b/src/main/resources/templates/c_plus_plus/test/testUtils/junit/TestSuite.py new file mode 100644 index 000000000000..0acc513c744b --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/test/testUtils/junit/TestSuite.py @@ -0,0 +1,55 @@ +from datetime import timedelta +from typing import Dict +from xml.etree import ElementTree as Et + +from testUtils.junit.TestCase import Result, TestCase + + +class TestSuite: + __cases: Dict[str, TestCase] + + name: str + tests: int + failures: int + errors: int + skipped: int + successful: int + time: timedelta + + def __init__(self, name: str): + self.name = name + + self.__cases: Dict[str, TestCase] = dict() + self.tests: int = 0 + self.failures: int = 0 + self.errors: int = 0 + self.skipped: int = 0 + self.successful: int = 0 + self.time: timedelta = timedelta() + + def addCase(self, case: TestCase): + self.__cases[case.name] = case + self.tests += 1 + self.time += case.time + + if case.result == Result.ERROR: + self.errors += 1 + elif case.result == Result.FAILURE: + self.failures += 1 + elif case.result == Result.SKIPPED: + self.skipped += 1 + else: + self.successful += 1 + + def toXml(self): + suite: Et.Element = Et.Element("testsuite") + suite.set("name", self.name) + suite.set("tests", str(self.tests)) + suite.set("failures", str(self.failures)) + suite.set("errors", str(self.errors)) + suite.set("skipped", str(self.skipped)) + suite.set("time", str(self.time.total_seconds())) + + for _name, case in self.__cases.items(): + case.toXml(suite) + return suite diff --git a/src/main/resources/templates/c_plus_plus/test/tests/TestCatch2.py b/src/main/resources/templates/c_plus_plus/test/tests/TestCatch2.py new file mode 100644 index 000000000000..4b2de70d2564 --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/test/tests/TestCatch2.py @@ -0,0 +1,36 @@ +from os.path import join +from typing import List +from xml.etree import ElementTree as Et + +from testUtils.AbstractProgramTest import AbstractProgramTest +from testUtils.Utils import printTester + + +class TestCatch2(AbstractProgramTest): + def __init__(self, location: str, executable: str, requirements: List[str] | None = None, name: str | None = None) -> None: + super().__init__(name or f"TestCatch2({executable})", location, executable, requirements, timeoutSec=10) + + def _run(self) -> None: + # Start the program: + outputFilename = f"result-{self.executable}.xml" + self.pWrap = self._createPWrap([join(".", self.executable), "--success", "--reporter", f"JUnit::out={outputFilename}", "--reporter", "console::out=-::colour-mode=none"], self.executionDirectory) + self._startPWrap(self.pWrap) + self.pWrap.waitUntilTerminationReading() + + retCode: int = self.pWrap.getReturnCode() + # parse XML output and append it to the results + try: + catchXmlRoot: Et = Et.parse(join(self.executionDirectory, outputFilename)) + catchXmlSuite: Et.Element = catchXmlRoot.find("testsuite") + self.additionalSuites.append(catchXmlSuite) + printTester(f"Appended {catchXmlSuite}") + except Exception as e: + printTester(f"Exception {e}") + + if retCode != 0: + self._failWith( + f"Test for {self.executable} failed." + ) + + # Always cleanup to make sure all threads get joined: + self.pWrap.cleanup() diff --git a/src/main/resources/templates/c_plus_plus/test/tests/TestCompile.py b/src/main/resources/templates/c_plus_plus/test/tests/TestCompile.py new file mode 100644 index 000000000000..612380af0872 --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/test/tests/TestCompile.py @@ -0,0 +1,40 @@ +from typing import List + +from testUtils.AbstractProgramTest import AbstractProgramTest + + +class TestCompile(AbstractProgramTest): + """ + Test case that tries to compile the given program with any compiler optimization disabled. + Most compiler warnings are enabled but aren't treated as errors. + """ + + target: str + + def __init__( + self, + buildDir: str, + target: str = "all", + requirements: List[str] | None = None, + name: str = "TestCompile", + ) -> None: + super().__init__( + name, buildDir, "cmake", requirements, timeoutSec=10 + ) + self.target = target + + def _run(self) -> None: + # Build all targets: + self.pWrap = self._createPWrap([self.executable, "--build", self.executionDirectory, "--target", self.target]) + self._startPWrap(self.pWrap) + + self.pWrap.waitUntilTerminationReading() + + retCode: int = self.pWrap.getReturnCode() + if retCode != 0: + self._failWith( + f"Build for directory {self.executionDirectory} failed. Returncode is {retCode}." + ) + + # Always cleanup to make sure all threads get joined: + self.pWrap.cleanup() diff --git a/src/main/resources/templates/c_plus_plus/test/tests/TestConfigure.py b/src/main/resources/templates/c_plus_plus/test/tests/TestConfigure.py new file mode 100644 index 000000000000..6a74ed566b0b --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/test/tests/TestConfigure.py @@ -0,0 +1,49 @@ +from typing import List +import shutil +import os.path + +from testUtils.AbstractProgramTest import AbstractProgramTest + + +class TestConfigure(AbstractProgramTest): + """ + Test case that runs CMake to configure the build + """ + + buildDir: str + extraFlags: List[str] + + def __init__( + self, + location: str, + buildDir: str, + extraFlags: List[str] | None = None, + requirements: List[str] | None = None, + name: str = "TestConfigure", + ) -> None: + super().__init__( + name, location, "cmake", requirements, timeoutSec=10 + ) + self.buildDir = buildDir + self.extraFlags = extraFlags or [] + + def _run(self) -> None: + if os.path.exists(self.buildDir): + shutil.rmtree(self.buildDir) + # Call CMake to configure the project: + self.pWrap = self._createPWrap( + [self.executable, "-S", self.executionDirectory, "-B", self.buildDir, + *self.extraFlags] + ) + self._startPWrap(self.pWrap) + + self.pWrap.waitUntilTerminationReading() + + retCode: int = self.pWrap.getReturnCode() + if retCode != 0: + self._failWith( + f"CMake for directory {self.executionDirectory} failed. Returncode is {retCode}." + ) + + # Always cleanup to make sure all threads get joined: + self.pWrap.cleanup() diff --git a/src/main/resources/templates/c_plus_plus/test/tests/TestOutput.py b/src/main/resources/templates/c_plus_plus/test/tests/TestOutput.py new file mode 100644 index 000000000000..dfbc60fe28fa --- /dev/null +++ b/src/main/resources/templates/c_plus_plus/test/tests/TestOutput.py @@ -0,0 +1,39 @@ +from os.path import join +from typing import List + +from testUtils.AbstractProgramTest import AbstractProgramTest +from testUtils.Utils import printTester, studSaveStrComp + + +class TestOutput(AbstractProgramTest): + def __init__(self, makefileLocation: str, requirements: List[str] = None, name: str = "TestOutput", executable: str = "helloWorld.out"): + super(TestOutput, self).__init__(name, makefileLocation, executable, requirements, timeoutSec=10) + + def _run(self): + # Start the program: + self.pWrap = self._createPWrap([join(".", self.executionDirectory, self.executable)]) + self._startPWrap(self.pWrap) + + # Wait for child being ready: + printTester("Waiting for: 'Hello world!'") + expected: str = "Hello world!" + while True: + if self.pWrap.hasTerminated() and not self.pWrap.canReadLineStdout(): + self._progTerminatedUnexpectedly() + # Read a single line form the program output: + line: str = self.pWrap.readLineStdout() + # Perform a "student save" compare: + if studSaveStrComp(expected, line): + break + else: + printTester(f"Expected '{expected}' but received read '{line}'") + + # Wait reading until the program terminates: + printTester("Waiting for the program to terminate...") + if not self.pWrap.waitUntilTerminationReading(3): + printTester("Program did not terminate - killing it!") + self.pWrap.kill() + self._failWith("Program did not terminate at the end.") + + # Always cleanup to make sure all threads get joined: + self.pWrap.cleanup() diff --git a/src/main/resources/templates/jenkins/c_plus_plus/regularRuns/pipeline.groovy b/src/main/resources/templates/jenkins/c_plus_plus/regularRuns/pipeline.groovy new file mode 100644 index 000000000000..d9bc3bbf4a27 --- /dev/null +++ b/src/main/resources/templates/jenkins/c_plus_plus/regularRuns/pipeline.groovy @@ -0,0 +1,81 @@ +/* + * 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('Setup') { + sh ''' + mkdir test-reports + + # Updating ownership... + chown -R artemis_user:artemis_user . + + REQ_FILE=requirements.txt + if [ -f "$REQ_FILE" ]; then + python3 -m venv /venv + /venv/bin/pip3 install -r "$REQ_FILE" + else + echo "$REQ_FILE does not exist" + fi + ''' + } + + stage('Compile and Test') { + sh ''' + if [ -d /venv ]; then + . /venv/bin/activate + fi + + # Run tests as unprivileged user + runuser -u artemis_user python3 Tests.py + ''' + } +} + +/** + * Script of the post build tasks aggregating all JUnit files in $WORKSPACE/results. + * + * Called by Jenkins. + */ +void postBuildTasks() { + sh ''' + if [ -e test-reports/tests-results.xml ] + then + sed -i 's/[^[:print:]\t]/�/g' test-reports/tests-results.xml + sed -i 's//<\\/error>/g' test-reports/tests-results.xml + sed -i 's/]*>//g ; s/<\\/testsuites>/<\\/testsuite>/g' test-reports/tests-results.xml + fi + rm -rf results + mv test-reports results + ''' +} + +// 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 8dde59469c03..d1d039f7cd38 100644 --- a/src/main/webapp/app/entities/programming/programming-exercise.model.ts +++ b/src/main/webapp/app/entities/programming/programming-exercise.model.ts @@ -16,6 +16,7 @@ export enum ProgrammingLanguage { EMPTY = 'EMPTY', ASSEMBLER = 'ASSEMBLER', C = 'C', + C_PLUS_PLUS = 'C_PLUS_PLUS', HASKELL = 'HASKELL', JAVA = 'JAVA', JAVASCRIPT = 'JAVASCRIPT', diff --git a/src/test/resources/config/application.yml b/src/test/resources/config/application.yml index ee94c5be7573..670ef33f823f 100644 --- a/src/test/resources/config/application.yml +++ b/src/test/resources/config/application.yml @@ -72,6 +72,8 @@ artemis: default: "~~invalid~~" r: default: "~~invalid~~" + c_plus_plus: + default: "~~invalid~~" spring: application: