Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Programming exercises: Add MATLAB programming exercise template #10039

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/user/exercises/programming-exercise-features.inc
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ Instructors can still use those templates to generate programming exercises and
+----------------------+----------+---------+
| Go | yes | yes |
+----------------------+----------+---------+
| MATLAB | yes | no |
+----------------------+----------+---------+

- 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.
Expand Down Expand Up @@ -91,6 +93,8 @@ Instructors can still use those templates to generate programming exercises and
+----------------------+----------------------+----------------------+---------------------+--------------+------------------------------------------+------------------------------+----------------------------+------------------------+
| Go | no | no | yes | yes | n/a | no | no | L: yes, J: no |
+----------------------+----------------------+----------------------+---------------------+--------------+------------------------------------------+------------------------------+----------------------------+------------------------+
| MATLAB | no | no | no | 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.
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/de/tum/cit/aet/artemis/ArtemisApp.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@
import org.springframework.boot.info.GitProperties;
import org.springframework.core.env.Environment;

import de.tum.cit.aet.artemis.core.config.LicenseConfiguration;
import de.tum.cit.aet.artemis.core.config.ProgrammingLanguageConfiguration;
import de.tum.cit.aet.artemis.core.config.TheiaConfiguration;
import tech.jhipster.config.DefaultProfileUtil;
import tech.jhipster.config.JHipsterConstants;

@SpringBootApplication
@EnableConfigurationProperties({ LiquibaseProperties.class, ProgrammingLanguageConfiguration.class, TheiaConfiguration.class })
@EnableConfigurationProperties({ LiquibaseProperties.class, ProgrammingLanguageConfiguration.class, TheiaConfiguration.class, LicenseConfiguration.class })
public class ArtemisApp {

private static final Logger log = LoggerFactory.getLogger(ArtemisApp.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public CreateContainerResponse configureContainer(String containerName, String i
// container from exiting until it finishes.
// It waits until the script that is running the tests (see below execCreateCmdResponse) is completed, and until the result files are extracted which is indicated
// by the creation of a file "stop_container.txt" in the container's root directory.
.withCmd("sh", "-c", "while [ ! -f " + LOCALCI_WORKING_DIRECTORY + "/stop_container.txt ]; do sleep 0.5; done")
.withEntrypoint().withCmd("sh", "-c", "while [ ! -f " + LOCALCI_WORKING_DIRECTORY + "/stop_container.txt ]; do sleep 0.5; done")
// .withCmd("tail", "-f", "/dev/null") // Activate for debugging purposes instead of the above command to get a running container that you can peek into using
// "docker exec -it <container-id> /bin/bash".
.exec();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package de.tum.cit.aet.artemis.core.config;

import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE;

import jakarta.annotation.Nullable;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Profile;

@Profile(PROFILE_CORE)
@ConfigurationProperties(prefix = "artemis.licenses")
public class LicenseConfiguration {

private final MatLabLicense matlab;

public record MatLabLicense(String licenseServer) {
}

public LicenseConfiguration(MatLabLicense matlab) {
this.matlab = matlab;
}

@Nullable
public String getMatlabLicenseServer() {
if (matlab == null) {
return null;
}
return matlab.licenseServer();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public enum ProgrammingLanguage {
JAVA,
JAVASCRIPT,
KOTLIN,
MATLAB,
OCAML,
PYTHON,
R,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package de.tum.cit.aet.artemis.programming.service;

import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE;

import java.util.Map;
import java.util.Objects;

import jakarta.annotation.Nullable;

import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;

import de.tum.cit.aet.artemis.core.config.LicenseConfiguration;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage;
import de.tum.cit.aet.artemis.programming.domain.ProjectType;

/**
* Provides licensing information for proprietary software to build jobs via environment variables.
*/
@Profile(PROFILE_CORE)
@Service
public class LicenseService {

private final LicenseConfiguration licenseConfiguration;

public LicenseService(LicenseConfiguration licenseConfiguration) {
this.licenseConfiguration = licenseConfiguration;
}

/**
* Checks whether a required license is configured for the specified exercise type.
* If no license is required this returns true.
*
* @param programmingLanguage the programming language of the exercise type
* @param projectType the project type of the exercise type
* @return whether a required license is configured
*/
public boolean isLicensed(ProgrammingLanguage programmingLanguage, @Nullable ProjectType projectType) {
if (programmingLanguage == ProgrammingLanguage.MATLAB && projectType == null) {
return licenseConfiguration.getMatlabLicenseServer() != null;
}

return true;
}

/**
* Returns environment variables required to run programming exercise tests.
*
* @param programmingLanguage the programming language of the exercise
* @param projectType the project type of the exercise
* @return environment variables for the specified exercise type
*/
public Map<String, String> getEnvironment(ProgrammingLanguage programmingLanguage, @Nullable ProjectType projectType) {
if (programmingLanguage == ProgrammingLanguage.MATLAB && projectType == null) {
return Map.of("MLM_LICENSE_FILE", Objects.requireNonNull(licenseConfiguration.getMatlabLicenseServer()));
}

return Map.of();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

Expand All @@ -17,7 +18,10 @@

import de.tum.cit.aet.artemis.buildagent.dto.DockerFlagsDTO;
import de.tum.cit.aet.artemis.buildagent.dto.DockerRunConfig;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseBuildConfig;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage;
import de.tum.cit.aet.artemis.programming.domain.ProjectType;

@Profile(PROFILE_CORE)
@Service
Expand All @@ -27,6 +31,12 @@ public class ProgrammingExerciseBuildConfigService {

private final ObjectMapper objectMapper = new ObjectMapper();

private final LicenseService licenseService;

public ProgrammingExerciseBuildConfigService(LicenseService licenseService) {
this.licenseService = licenseService;
}

/**
* Converts a JSON string representing Docker flags (in JSON format)
* into a {@link DockerRunConfig} instance.
Expand All @@ -46,25 +56,60 @@ public class ProgrammingExerciseBuildConfigService {
public DockerRunConfig getDockerRunConfig(ProgrammingExerciseBuildConfig buildConfig) {
DockerFlagsDTO dockerFlagsDTO = parseDockerFlags(buildConfig);

return getDockerRunConfigFromParsedFlags(dockerFlagsDTO);
String network;
Map<String, String> exerciseEnvironment;
if (dockerFlagsDTO != null) {
network = dockerFlagsDTO.network();
exerciseEnvironment = dockerFlagsDTO.env();
}
else {
network = null;
exerciseEnvironment = null;
}

ProgrammingExercise exercise = buildConfig.getProgrammingExercise();
if (exercise == null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When can this happen, i.e. what would be the reason when its null / not null?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that some of the tests related to exercise importing set this to null. Not sure if this can happen in production code.

return createDockerRunConfig(network, exerciseEnvironment);
}

ProgrammingLanguage programmingLanguage = exercise.getProgrammingLanguage();
ProjectType projectType = exercise.getProjectType();
Map<String, String> environment = addLanguageSpecificEnvironment(exerciseEnvironment, programmingLanguage, projectType);

return createDockerRunConfig(network, environment);
}

@Nullable
private Map<String, String> addLanguageSpecificEnvironment(@Nullable Map<String, String> exerciseEnvironment, ProgrammingLanguage language, ProjectType projectType) {
Map<String, String> licenseEnvironment = licenseService.getEnvironment(language, projectType);
if (licenseEnvironment.isEmpty()) {
return exerciseEnvironment;
}

Map<String, String> env = new HashMap<>(licenseEnvironment);
if (exerciseEnvironment != null) {
env.putAll(exerciseEnvironment);
}

return env;
}

DockerRunConfig getDockerRunConfigFromParsedFlags(DockerFlagsDTO dockerFlagsDTO) {
if (dockerFlagsDTO == null) {
DockerRunConfig createDockerRunConfig(String network, Map<String, String> environmentMap) {
if (network == null && environmentMap == null) {
return null;
}
List<String> env = new ArrayList<>();
boolean isNetworkDisabled = dockerFlagsDTO.network() != null && dockerFlagsDTO.network().equals("none");
List<String> environmentStrings = new ArrayList<>();
boolean isNetworkDisabled = network != null && network.equals("none");

if (dockerFlagsDTO.env() != null) {
for (Map.Entry<String, String> entry : dockerFlagsDTO.env().entrySet()) {
if (environmentMap != null) {
for (Map.Entry<String, String> entry : environmentMap.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
env.add(key + "=" + value);
environmentStrings.add(key + "=" + value);
}
}

return new DockerRunConfig(isNetworkDisabled, env);
return new DockerRunConfig(isNetworkDisabled, environmentStrings);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1119,7 +1119,7 @@ public void validateDockerFlags(ProgrammingExercise programmingExercise) {
}
}

DockerRunConfig dockerRunConfig = programmingExerciseBuildConfigService.getDockerRunConfigFromParsedFlags(dockerFlagsDTO);
DockerRunConfig dockerRunConfig = programmingExerciseBuildConfigService.createDockerRunConfig(dockerFlagsDTO.network(), dockerFlagsDTO.env());
magaupp marked this conversation as resolved.
Show resolved Hide resolved

if (List.of(ProgrammingLanguage.SWIFT, ProgrammingLanguage.HASKELL).contains(programmingExercise.getProgrammingLanguage()) && dockerRunConfig.isNetworkDisabled()) {
throw new BadRequestAlertException("This programming language does not support disabling the network access feature", "Exercise", "networkAccessNotSupported");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package de.tum.cit.aet.artemis.programming.service;

import java.util.HashMap;
import java.util.EnumMap;
import java.util.Map;

import jakarta.annotation.Nullable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoContributor;

import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage;
import de.tum.cit.aet.artemis.programming.domain.ProjectType;

/**
* This service provides information about features the different ProgrammingLanguages support.
Expand All @@ -18,7 +21,16 @@ public abstract class ProgrammingLanguageFeatureService implements InfoContribut

private static final Logger log = LoggerFactory.getLogger(ProgrammingLanguageFeatureService.class);

protected final Map<ProgrammingLanguage, ProgrammingLanguageFeature> programmingLanguageFeatures = new HashMap<>();
private final LicenseService licenseService;

private final Map<ProgrammingLanguage, ProgrammingLanguageFeature> programmingLanguageFeatures;

protected ProgrammingLanguageFeatureService(LicenseService licenseService) {
this.licenseService = licenseService;
this.programmingLanguageFeatures = getEnabledFeatures();
}

protected abstract Map<ProgrammingLanguage, ProgrammingLanguageFeature> getSupportedProgrammingLanguageFeatures();

/**
* Get the ProgrammingLanguageFeature configured for the given ProgrammingLanguage.
Expand All @@ -37,12 +49,45 @@ public ProgrammingLanguageFeature getProgrammingLanguageFeatures(ProgrammingLang
return programmingLanguageFeature;
}

public Map<ProgrammingLanguage, ProgrammingLanguageFeature> getProgrammingLanguageFeatures() {
return programmingLanguageFeatures;
}

@Override
public void contribute(Info.Builder builder) {
builder.withDetail("programmingLanguageFeatures", getProgrammingLanguageFeatures().values());
builder.withDetail("programmingLanguageFeatures", programmingLanguageFeatures.values());
}

private Map<ProgrammingLanguage, ProgrammingLanguageFeature> getEnabledFeatures() {
var features = new EnumMap<ProgrammingLanguage, ProgrammingLanguageFeature>(ProgrammingLanguage.class);
for (var programmingLanguageFeatureEntry : getSupportedProgrammingLanguageFeatures().entrySet()) {
var language = programmingLanguageFeatureEntry.getKey();
var feature = programmingLanguageFeatureEntry.getValue();
if (feature.projectTypes().isEmpty()) {
if (isProjectTypeUsable(language, null)) {
features.put(language, feature);
}
}
else {
var filteredProjectTypes = feature.projectTypes().stream().filter((projectType -> isProjectTypeUsable(language, projectType))).toList();
if (!filteredProjectTypes.isEmpty()) {
// @formatter:off
var filteredFeature = new ProgrammingLanguageFeature(
feature.programmingLanguage(),
feature.sequentialTestRuns(),
feature.staticCodeAnalysis(),
feature.plagiarismCheckSupported(),
feature.packageNameRequired(),
feature.checkoutSolutionRepositoryAllowed(),
filteredProjectTypes,
feature.testwiseCoverageAnalysisSupported(),
feature.auxiliaryRepositoriesSupported()
);
// @formatter:on
features.put(language, filteredFeature);
}
}
}
return features;
}

private boolean isProjectTypeUsable(ProgrammingLanguage programmingLanguage, @Nullable ProjectType projectType) {
return licenseService.isLicensed(programmingLanguage, projectType);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ 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, GO -> defaultRepositoryUpgradeService;
case SQL, 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, MATLAB ->
defaultRepositoryUpgradeService;
case SQL, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + programmingLanguage);
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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, GO -> "assignment";
case SQL, 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, MATLAB -> "assignment";
case SQL, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + language);
};
}
},
Expand All @@ -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, GO -> "tests";
case SQL, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + language);
case C, VHDL, ASSEMBLER, OCAML, C_SHARP, GO, MATLAB -> "tests";
case SQL, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + language);
};
}
},
Expand Down
Loading
Loading