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: Export and import build plan from file #7624

Merged
merged 21 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6dcdf7d
`Programming exercises`: Export and import build plan from file
tobias-lippert Nov 18, 2023
9106612
remove superfluous empty line
tobias-lippert Nov 18, 2023
5fc19ca
add javadoc
tobias-lippert Nov 18, 2023
cec9057
code review comments and test improvement
tobias-lippert Nov 20, 2023
cb43e75
Merge branch 'develop' into enhancement/export-import-buildplan
tobias-lippert Nov 20, 2023
2182490
Merge branch 'develop' into enhancement/export-import-buildplan
tobias-lippert Nov 25, 2023
c9567a0
Merge branch 'develop' into enhancement/export-import-buildplan
tobias-lippert Nov 25, 2023
f34c1a3
Merge branch 'develop' into enhancement/export-import-buildplan
tobias-lippert Nov 26, 2023
2489f18
Merge branch 'develop' into enhancement/export-import-buildplan
tobias-lippert Nov 27, 2023
38de650
Update BuildPlanRepository.java
tobias-lippert Nov 28, 2023
14aa9d6
Merge branch 'develop' into enhancement/export-import-buildplan
b-fein Nov 29, 2023
5b226fc
Merge branch 'develop' into enhancement/export-import-buildplan
tobias-lippert Nov 30, 2023
9b39fef
fix path to build plan for import and test the import of the build plan
tobias-lippert Dec 1, 2023
72c4b47
Merge remote-tracking branch 'origin/enhancement/export-import-buildp…
tobias-lippert Dec 1, 2023
41ff521
Merge branch 'develop' into enhancement/export-import-buildplan
tobias-lippert Dec 1, 2023
204ac97
Merge branch 'develop' into enhancement/export-import-buildplan
b-fein Dec 12, 2023
1c781e9
Merge branch 'develop' into enhancement/export-import-buildplan
tobias-lippert Dec 14, 2023
c91c4a1
Merge branch 'develop' into enhancement/export-import-buildplan
tobias-lippert Dec 19, 2023
74b752a
Merge branch 'develop' into enhancement/export-import-buildplan
b-fein Dec 27, 2023
b29a3a8
Merge branch 'develop' into enhancement/export-import-buildplan
tobias-lippert Jan 2, 2024
3d3adc0
Merge branch 'develop' into enhancement/export-import-buildplan
b-fein Jan 8, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ public interface BuildPlanRepository extends JpaRepository<BuildPlan, Long> {
""")
Optional<BuildPlan> findByProgrammingExercises_IdWithProgrammingExercises(@Param("exerciseId") long exerciseId);

@Query("""
SELECT buildPlan
FROM BuildPlan buildPlan
JOIN buildPlan.programmingExercises programmingExercises
WHERE programmingExercises.id = :exerciseId
""")
Optional<BuildPlan> findByProgrammingExercises_Id(long exerciseId);
b-fein marked this conversation as resolved.
Show resolved Hide resolved

default BuildPlan findByProgrammingExercises_IdWithProgrammingExercisesElseThrow(final long exerciseId) {
return findByProgrammingExercises_IdWithProgrammingExercises(exerciseId)
.orElseThrow(() -> new EntityNotFoundException("Could not find a build plan for exercise " + exerciseId));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ public boolean isBamboo() {
return isProfileActive("bamboo");
}

public boolean isGitlabCiOrJenkins() {
return isProfileActive("gitlabci") || isProfileActive("jenkins");
}
b-fein marked this conversation as resolved.
Show resolved Hide resolved

private boolean isProfileActive(String profile) {
return Set.of(this.environment.getActiveProfiles()).contains(profile);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,12 @@
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.*;
b-fein marked this conversation as resolved.
Show resolved Hide resolved
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.function.Predicate;
Expand All @@ -31,10 +27,7 @@
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathException;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.*;
b-fein marked this conversation as resolved.
Show resolved Hide resolved

import org.apache.commons.io.FileUtils;
import org.eclipse.jgit.api.errors.GitAPIException;
Expand All @@ -54,9 +47,7 @@
import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation;
import de.tum.in.www1.artemis.domain.participation.StudentParticipation;
import de.tum.in.www1.artemis.exception.GitException;
import de.tum.in.www1.artemis.repository.AuxiliaryRepositoryRepository;
import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository;
import de.tum.in.www1.artemis.repository.StudentParticipationRepository;
import de.tum.in.www1.artemis.repository.*;
b-fein marked this conversation as resolved.
Show resolved Hide resolved
import de.tum.in.www1.artemis.service.ExerciseDateService;
import de.tum.in.www1.artemis.service.FileService;
import de.tum.in.www1.artemis.service.ZipFileService;
Expand Down Expand Up @@ -91,13 +82,17 @@ public class ProgrammingExerciseExportService extends ExerciseWithSubmissionsExp

private final ZipFileService zipFileService;

private final BuildPlanRepository buildPlanRepository;

public static final String EXPORTED_EXERCISE_DETAILS_FILE_PREFIX = "Exercise-Details";

public static final String EXPORTED_EXERCISE_PROBLEM_STATEMENT_FILE_PREFIX = "Problem-Statement";

public static final String BUILD_PLAN_FILE_NAME = "buildPlan.txt";

public ProgrammingExerciseExportService(ProgrammingExerciseRepository programmingExerciseRepository, ProgrammingExerciseTaskService programmingExerciseTaskService,
StudentParticipationRepository studentParticipationRepository, FileService fileService, GitService gitService, ZipFileService zipFileService,
MappingJackson2HttpMessageConverter springMvcJacksonConverter, AuxiliaryRepositoryRepository auxiliaryRepositoryRepository) {
MappingJackson2HttpMessageConverter springMvcJacksonConverter, AuxiliaryRepositoryRepository auxiliaryRepositoryRepository, BuildPlanRepository buildPlanRepository) {
// Programming exercises do not have a submission export service
super(fileService, springMvcJacksonConverter, null);
this.programmingExerciseRepository = programmingExerciseRepository;
Expand All @@ -107,6 +102,7 @@ public ProgrammingExerciseExportService(ProgrammingExerciseRepository programmin
this.gitService = gitService;
this.zipFileService = zipFileService;
this.auxiliaryRepositoryRepository = auxiliaryRepositoryRepository;
this.buildPlanRepository = buildPlanRepository;
}

/**
Expand All @@ -132,8 +128,8 @@ private Path exportProgrammingExerciseMaterialWithStudentReposOptional(Programmi
exportDir = Optional.of(fileService.getTemporaryUniquePathWithoutPathCreation(repoDownloadClonePath, 5));
}

// Add the exported zip folder containing template, solution, and tests repositories
// wrap this in a try catch block to prevent the problem statement and exercise details not being exported if the repositories fail to export
// Add the exported zip folder containing template, solution, and tests repositories. Also export the build plan if one exists.
// Wrap this in a try catch block to prevent the problem statement and exercise details not being exported if the repositories fail to export
try {
var repoExportsPaths = exportProgrammingExerciseRepositories(exercise, includeStudentRepos, shouldZipZipFiles, repoDownloadClonePath, exportDir.orElseThrow(),
exportErrors, archivalReportEntries);
Expand All @@ -142,6 +138,15 @@ private Path exportProgrammingExerciseMaterialWithStudentReposOptional(Programmi
pathsToBeZipped.add(path);
}
});

// Export the build plan of a programming exercise, if one exists. Only relevant for Gitlab/Jenkins or Gitlab/GitlabCI setups.
var buildPlan = buildPlanRepository.findByProgrammingExercises_Id(exercise.getId());
if (buildPlan.isPresent()) {
Path buildPlanPath = exportDir.orElseThrow().resolve(BUILD_PLAN_FILE_NAME);
FileUtils.writeStringToFile(buildPlanPath.toFile(), buildPlan.orElseThrow().getBuildPlan(), StandardCharsets.UTF_8);
pathsToBeZipped.add(buildPlanPath);
}

}
catch (Exception e) {
exportErrors.add("Failed to export programming exercise repositories: " + e.getMessage());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package de.tum.in.www1.artemis.service.programming;

import static de.tum.in.www1.artemis.service.export.ProgrammingExerciseExportService.BUILD_PLAN_FILE_NAME;

import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
Expand All @@ -13,6 +16,8 @@
import org.apache.commons.io.filefilter.NameFileFilter;
import org.apache.commons.io.filefilter.NotFileFilter;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

Expand All @@ -21,13 +26,16 @@

import de.tum.in.www1.artemis.domain.*;
import de.tum.in.www1.artemis.domain.enumeration.RepositoryType;
import de.tum.in.www1.artemis.repository.BuildPlanRepository;
import de.tum.in.www1.artemis.service.*;
import de.tum.in.www1.artemis.service.connectors.GitService;
import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException;

@Service
public class ProgrammingExerciseImportFromFileService {

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

private final ProgrammingExerciseService programmingExerciseService;

private final ZipFileService zipFileService;
Expand All @@ -40,16 +48,23 @@ public class ProgrammingExerciseImportFromFileService {

private final FileService fileService;

private final ProfileService profileService;

private final BuildPlanRepository buildPlanRepository;

private static final List<String> SHORT_NAME_REPLACEMENT_EXCLUSIONS = List.of("gradle-wrapper.jar");

public ProgrammingExerciseImportFromFileService(ProgrammingExerciseService programmingExerciseService, ZipFileService zipFileService,
StaticCodeAnalysisService staticCodeAnalysisService, RepositoryService repositoryService, GitService gitService, FileService fileService) {
StaticCodeAnalysisService staticCodeAnalysisService, RepositoryService repositoryService, GitService gitService, FileService fileService, ProfileService profileService,
BuildPlanRepository buildPlanRepository) {
this.programmingExerciseService = programmingExerciseService;
this.zipFileService = zipFileService;
this.staticCodeAnalysisService = staticCodeAnalysisService;
this.repositoryService = repositoryService;
this.gitService = gitService;
this.fileService = fileService;
this.profileService = profileService;
this.buildPlanRepository = buildPlanRepository;
}

/**
Expand Down Expand Up @@ -84,9 +99,14 @@ public ProgrammingExercise importProgrammingExerciseFromFile(ProgrammingExercise
if (Boolean.TRUE.equals(programmingExerciseForImport.isStaticCodeAnalysisEnabled())) {
staticCodeAnalysisService.createDefaultCategories(importedProgrammingExercise);
}
copyEmbeddedFiles(exerciseFilePath.toAbsolutePath().getParent().resolve(FileNameUtils.getBaseName(exerciseFilePath.toString())));
Path pathToDirectoryWithImportedContent = exerciseFilePath.toAbsolutePath().getParent().resolve(FileNameUtils.getBaseName(exerciseFilePath.toString()));
copyEmbeddedFiles(pathToDirectoryWithImportedContent);
importRepositoriesFromFile(importedProgrammingExercise, importExerciseDir, oldShortName, user);
importedProgrammingExercise.setCourse(course);
// It doesn't make sense to import a build plan on a bamboo or local CI setup.
if (profileService.isGitlabCiOrJenkins()) {
importBuildPlanIfExisting(importedProgrammingExercise, pathToDirectoryWithImportedContent);
}
}
finally {
// want to make sure the directories are deleted, even if an exception is thrown
Expand All @@ -95,6 +115,25 @@ public ProgrammingExercise importProgrammingExerciseFromFile(ProgrammingExercise
return importedProgrammingExercise;
}

/**
* Imports a build plan if it exists in the extracted zip file
* If the file cannot be read, the build plan is skipped
*
* @param programmingExercise the programming exercise for which the build plan should be imported
* @param importExerciseDir the directory where the extracted zip file is located
*/
private void importBuildPlanIfExisting(ProgrammingExercise programmingExercise, Path importExerciseDir) {
Path buildPlanPath = importExerciseDir.resolve(BUILD_PLAN_FILE_NAME);
if (Files.exists(buildPlanPath)) {
try {
buildPlanRepository.setBuildPlanForExercise(FileUtils.readFileToString(buildPlanPath.toFile(), StandardCharsets.UTF_8), programmingExercise);
}
catch (IOException e) {
log.warn("Could not read build plan file. Continue importing the exercise but skipping the build plan.", e);
}
}
}
b-fein marked this conversation as resolved.
Show resolved Hide resolved

/**
* Copy embedded files from the extracted zip file to the markdown folder, so they can be used in the problem statement
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ void exportInstructorRepositories_forbidden() throws Exception {
@Test
@WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR")
void exportProgrammingExerciseInstructorMaterial() throws Exception {
programmingExerciseTestService.exportProgrammingExerciseInstructorMaterial_shouldReturnFile(true);
programmingExerciseTestService.exportProgrammingExerciseInstructorMaterial_shouldReturnFileWithoutBuildplan(true);
// we have a working directory and one directory for each repository
verify(fileService, times(4)).scheduleDirectoryPathForRecursiveDeletion(any(Path.class), eq(5L));
verify(fileService).schedulePathForDeletion(any(Path.class), eq(5L));
Expand Down Expand Up @@ -435,29 +435,29 @@ void testArchiveCourseWithProgrammingExercise() throws Exception {
@WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR")
void testExportProgrammingExerciseInstructorMaterial_failToCreateZip() throws Exception {
doThrow(IOException.class).when(zipFileService).createZipFile(any(Path.class), any());
programmingExerciseTestService.exportProgrammingExerciseInstructorMaterial(HttpStatus.INTERNAL_SERVER_ERROR, true, true, true);
programmingExerciseTestService.exportProgrammingExerciseInstructorMaterial(HttpStatus.INTERNAL_SERVER_ERROR, true, true, true, false);
}

@Test
@WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR")
void testExportProgrammingExerciseInstructorMaterial_failToCreateTempDir() throws Exception {
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
mockedFiles.when(() -> Files.createTempDirectory(any(Path.class), any(String.class))).thenThrow(IOException.class);
programmingExerciseTestService.exportProgrammingExerciseInstructorMaterial(HttpStatus.INTERNAL_SERVER_ERROR, true, false, false);
programmingExerciseTestService.exportProgrammingExerciseInstructorMaterial(HttpStatus.INTERNAL_SERVER_ERROR, true, false, false, false);
}
}

@Test
@WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR")
void testExportProgrammingExerciseInstructorMaterial_embeddedFilesDontExist() throws Exception {
programmingExerciseTestService.exportProgrammingExerciseInstructorMaterial_shouldReturnFile(false);
programmingExerciseTestService.exportProgrammingExerciseInstructorMaterial_shouldReturnFileWithoutBuildplan(false);
}

@Test
@WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR")
void testExportProgrammingExerciseInstructorMaterial_failToExportRepository() throws Exception {
doThrow(GitException.class).when(fileService).getTemporaryUniquePathWithoutPathCreation(any(Path.class), anyLong());
programmingExerciseTestService.exportProgrammingExerciseInstructorMaterial(HttpStatus.INTERNAL_SERVER_ERROR, false, true, true);
programmingExerciseTestService.exportProgrammingExerciseInstructorMaterial(HttpStatus.INTERNAL_SERVER_ERROR, false, true, true, false);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,12 @@ void importExerciseFromFile_embeddedFiles_filesCopied() throws Exception {
programmingExerciseTestService.importFromFile_embeddedFiles_embeddedFilesCopied();
}

@Test
@WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR")
void importExerciseFromFile_buildPlanPresent_buildPlanSet() throws Exception {
programmingExerciseTestService.importFromFile_buildPlanPresent_buildPlanUsed();
}

@WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR")
@ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}")
@EnumSource(value = ProgrammingLanguage.class, names = { "HASKELL", "PYTHON" }, mode = EnumSource.Mode.INCLUDE)
Expand Down Expand Up @@ -404,7 +410,7 @@ void exportInstructorRepositories_forbidden() throws Exception {
@Test
@WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR")
void exportProgrammingExerciseInstructorMaterial() throws Exception {
programmingExerciseTestService.exportProgrammingExerciseInstructorMaterial_shouldReturnFile(true);
programmingExerciseTestService.exportProgrammingExerciseInstructorMaterial_shouldReturnFileWithBuildplan();
// we have a working directory and one directory for each repository
verify(fileService, times(4)).scheduleDirectoryPathForRecursiveDeletion(any(Path.class), eq(5L));
verify(fileService).schedulePathForDeletion(any(Path.class), eq(5L));
Expand All @@ -427,14 +433,14 @@ void exportProgrammingExerciseInstructorMaterial_problemStatementShouldContainTe
void testExportProgrammingExerciseInstructorMaterial_failToCreateTempDir() throws Exception {
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
mockedFiles.when(() -> Files.createTempDirectory(any(Path.class), any(String.class))).thenThrow(IOException.class);
programmingExerciseTestService.exportProgrammingExerciseInstructorMaterial(HttpStatus.INTERNAL_SERVER_ERROR, true, false, false);
programmingExerciseTestService.exportProgrammingExerciseInstructorMaterial(HttpStatus.INTERNAL_SERVER_ERROR, true, false, false, false);
}
}

@Test
@WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR")
void testExportProgrammingExerciseInstructorMaterial_embeddedFilesDontExist() throws Exception {
programmingExerciseTestService.exportProgrammingExerciseInstructorMaterial_shouldReturnFile(false);
programmingExerciseTestService.exportProgrammingExerciseInstructorMaterial_shouldReturnFile(false, false);
}

@Test
Expand Down
Loading
Loading