Skip to content

Commit

Permalink
Programming exercises: Support python exercises for local continuous …
Browse files Browse the repository at this point in the history
…integration (#7369)
  • Loading branch information
laurenzfb authored Oct 15, 2023
1 parent 20b910e commit f2ad596
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@

/**
* This service contains the logic to execute a build job for a programming exercise participation in the local CI system.
* The {@link #runBuildJob(ProgrammingExerciseParticipation, String, String)} method is wrapped into a Callable by the {@link LocalCIBuildJobManagementService} and submitted to the
* executor service.
* The {@link #runBuildJob(ProgrammingExerciseParticipation, String, String, String)} method is wrapped into a Callable by the {@link LocalCIBuildJobManagementService} and
* submitted to the executor service.
*/
@Service
@Profile("localci")
Expand All @@ -68,8 +68,8 @@ public class LocalCIBuildJobExecutionService {
@Value("${artemis.version-control.url}")
private URL localVCBaseUrl;

@Value("${artemis.version-control.local-vcs-repo-path}")
private String localVCBasePath;
@Value("${artemis.repo-clone-path}")
private String repoClonePath;

public LocalCIBuildJobExecutionService(LocalCIBuildPlanService localCIBuildPlanService, Optional<VersionControlService> versionControlService,
LocalCIContainerService localCIContainerService, AuxiliaryRepositoryRepository auxiliaryRepositoryRepository, XMLInputFactory localCIXMLInputFactory) {
Expand Down Expand Up @@ -104,10 +104,11 @@ public String toString() {
* @param commitHash The commit hash of the commit that should be built. If it is null, the latest commit of the default branch will be built.
* @param containerName The name of the Docker container that will be used to run the build job.
* It needs to be prepared beforehand to stop and remove the container if something goes wrong here.
* @param dockerImage The Docker image that will be used to run the build job.
* @return The build result.
* @throws LocalCIException If some error occurs while preparing or running the build job.
*/
public LocalCIBuildResult runBuildJob(ProgrammingExerciseParticipation participation, String commitHash, String containerName) {
public LocalCIBuildResult runBuildJob(ProgrammingExerciseParticipation participation, String commitHash, String containerName, String dockerImage) {
// Update the build plan status to "BUILDING".
localCIBuildPlanService.updateBuildPlanStatus(participation, ContinuousIntegrationService.BuildStatus.BUILDING);

Expand Down Expand Up @@ -144,7 +145,7 @@ public LocalCIBuildResult runBuildJob(ProgrammingExerciseParticipation participa

for (int i = 0; i < auxiliaryRepositories.size(); i++) {
auxiliaryRepositoriesUrls[i] = new LocalVCRepositoryUrl(auxiliaryRepositories.get(i).getRepositoryUrl(), localVCBaseUrl);
auxiliaryRepositoriesPaths[i] = auxiliaryRepositoriesUrls[i].getLocalRepositoryPath(localVCBasePath).toAbsolutePath();
auxiliaryRepositoriesPaths[i] = auxiliaryRepositoriesUrls[i].getRepoClonePath(repoClonePath).toAbsolutePath();
auxiliaryRepositoryNames[i] = auxiliaryRepositories.get(i).getName();
}
}
Expand All @@ -157,8 +158,8 @@ public LocalCIBuildResult runBuildJob(ProgrammingExerciseParticipation participa
throw new LocalCIException("Error while creating LocalVCRepositoryUrl", e);
}

Path assignmentRepositoryPath = assignmentRepositoryUrl.getLocalRepositoryPath(localVCBasePath).toAbsolutePath();
Path testsRepositoryPath = testsRepositoryUrl.getLocalRepositoryPath(localVCBasePath).toAbsolutePath();
Path assignmentRepositoryPath = assignmentRepositoryUrl.getRepoClonePath(repoClonePath).toAbsolutePath();
Path testsRepositoryPath = testsRepositoryUrl.getRepoClonePath(repoClonePath).toAbsolutePath();

String branch;
try {
Expand All @@ -171,7 +172,7 @@ public LocalCIBuildResult runBuildJob(ProgrammingExerciseParticipation participa
// Create the container from the "ls1tum/artemis-maven-template" image with the local paths to the Git repositories and the shell script bound to it. Also give the
// container information about the branch and commit hash to be used.
// This does not start the container yet.
CreateContainerResponse container = localCIContainerService.configureContainer(containerName, branch, commitHash);
CreateContainerResponse container = localCIContainerService.configureContainer(containerName, branch, commitHash, dockerImage);

return runScriptAndParseResults(participation, containerName, container.getId(), branch, commitHash, assignmentRepositoryPath, testsRepositoryPath,
auxiliaryRepositoriesPaths, auxiliaryRepositoryNames, buildScriptPath);
Expand Down Expand Up @@ -227,7 +228,7 @@ private LocalCIBuildResult runScriptAndParseResults(ProgrammingExerciseParticipa
return constructFailedBuildResult(branch, assignmentRepoCommitHash, testRepoCommitHash, buildCompletedDate);
}

List<String> testResultsPaths = getTestResultPath(participation.getProgrammingExercise());
List<String> testResultsPaths = getTestResultPaths(participation.getProgrammingExercise());

// Get an input stream of the test result files.
List<TarArchiveInputStream> testResultsTarInputStreams = new ArrayList<>();
Expand Down Expand Up @@ -269,34 +270,47 @@ private LocalCIBuildResult runScriptAndParseResults(ProgrammingExerciseParticipa

// --- Helper methods ----

private List<String> getTestResultPath(ProgrammingExercise programmingExercise) {
List<String> testResultPaths = new ArrayList<>();
private List<String> getTestResultPaths(ProgrammingExercise programmingExercise) {
switch (programmingExercise.getProgrammingLanguage()) {
case JAVA, KOTLIN -> {
if (ProjectType.isMavenProject(programmingExercise.getProjectType())) {
if (programmingExercise.hasSequentialTestRuns()) {
testResultPaths.add("/repositories/test-repository/structural/target/surefire-reports");
testResultPaths.add("/repositories/test-repository/behavior/target/surefire-reports");
}
else {
testResultPaths.add("/repositories/test-repository/target/surefire-reports");
}
}
else {
if (programmingExercise.hasSequentialTestRuns()) {
testResultPaths.add("/repositories/test-repository/build/test-results/behaviorTests");
testResultPaths.add("/repositories/test-repository/build/test-results/structuralTests");
}
else {
testResultPaths.add("/repositories/test-repository/build/test-results/test");
}
}
return testResultPaths;
return getJavaKotlinTestResultPaths(programmingExercise);
}
case PYTHON -> {
return getPythonTestResultPaths();
}
default -> throw new IllegalArgumentException("Programming language " + programmingExercise.getProgrammingLanguage() + " is not supported");
}
}

private List<String> getJavaKotlinTestResultPaths(ProgrammingExercise programmingExercise) {
List<String> testResultPaths = new ArrayList<>();
if (ProjectType.isMavenProject(programmingExercise.getProjectType())) {
if (programmingExercise.hasSequentialTestRuns()) {
testResultPaths.add("/repositories/test-repository/structural/target/surefire-reports");
testResultPaths.add("/repositories/test-repository/behavior/target/surefire-reports");
}
else {
testResultPaths.add("/repositories/test-repository/target/surefire-reports");
}
}
else {
if (programmingExercise.hasSequentialTestRuns()) {
testResultPaths.add("/repositories/test-repository/build/test-results/behaviorTests");
testResultPaths.add("/repositories/test-repository/build/test-results/structuralTests");
}
else {
testResultPaths.add("/repositories/test-repository/build/test-results/test");
}
}
return testResultPaths;
}

private List<String> getPythonTestResultPaths() {
List<String> testResultPaths = new ArrayList<>();
testResultPaths.add("/repositories/test-repository/test-reports");
return testResultPaths;
}

private LocalCIBuildResult parseTestResults(List<TarArchiveInputStream> testResultsTarInputStreams, String assignmentRepoBranchName, String assignmentRepoCommitHash,
String testsRepoCommitHash, ZonedDateTime buildCompletedDate) throws IOException, XMLStreamException {

Expand Down Expand Up @@ -330,7 +344,8 @@ private boolean isValidTestResultFile(TarArchiveEntry tarArchiveEntry) {
int lastIndexOfSlash = name.lastIndexOf('/');
String result = (lastIndexOfSlash != -1 && lastIndexOfSlash + 1 < name.length()) ? name.substring(lastIndexOfSlash + 1) : name;

return !tarArchiveEntry.isDirectory() && result.endsWith(".xml") && result.startsWith("TEST-");
// Java test result files are named "TEST-*.xml", Python test result files are named "*results.xml".
return !tarArchiveEntry.isDirectory() && ((result.endsWith(".xml") && result.startsWith("TEST-")) || result.endsWith("results.xml"));
}

private String readTarEntryContent(TarArchiveInputStream tarArchiveInputStream) throws IOException {
Expand All @@ -356,6 +371,10 @@ private void processTestResultFile(String testResultFileString, List<LocalCIBuil
xmlStreamReader.next();
}

if ("testsuites".equals(xmlStreamReader.getLocalName())) {
xmlStreamReader.next();
}

// Check if the start element is the "testsuite" node.
if (!("testsuite".equals(xmlStreamReader.getLocalName()))) {
throw new IllegalStateException("Expected testsuite element, but got " + xmlStreamReader.getLocalName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.*;
import java.util.function.Supplier;

Expand All @@ -12,7 +13,9 @@
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;

import de.tum.in.www1.artemis.config.ProgrammingLanguageConfiguration;
import de.tum.in.www1.artemis.domain.ProgrammingExercise;
import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage;
import de.tum.in.www1.artemis.domain.enumeration.ProjectType;
import de.tum.in.www1.artemis.domain.participation.Participation;
import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation;
Expand Down Expand Up @@ -46,25 +49,26 @@ public class LocalCIBuildJobManagementService {

private final LocalCIProgrammingLanguageFeatureService localCIProgrammingLanguageFeatureService;

private final ProgrammingLanguageConfiguration programmingLanguageConfiguration;

@Value("${artemis.continuous-integration.timeout-seconds:120}")
private int timeoutSeconds;

@Value("${artemis.continuous-integration.asynchronous:true}")
private boolean runBuildJobsAsynchronously;

@Value("${artemis.continuous-integration.build.images.java.default}")
private String dockerImage;

public LocalCIBuildJobManagementService(LocalCIBuildJobExecutionService localCIBuildJobExecutionService, ExecutorService localCIBuildExecutorService,
ProgrammingMessagingService programmingMessagingService, LocalCIBuildPlanService localCIBuildPlanService, LocalCIContainerService localCIContainerService,
LocalCIDockerService localCIDockerService, LocalCIProgrammingLanguageFeatureService localCIProgrammingLanguageFeatureService) {
LocalCIDockerService localCIDockerService, LocalCIProgrammingLanguageFeatureService localCIProgrammingLanguageFeatureService,
ProgrammingLanguageConfiguration programmingLanguageConfiguration) {
this.localCIBuildJobExecutionService = localCIBuildJobExecutionService;
this.localCIBuildExecutorService = localCIBuildExecutorService;
this.programmingMessagingService = programmingMessagingService;
this.localCIBuildPlanService = localCIBuildPlanService;
this.localCIContainerService = localCIContainerService;
this.localCIDockerService = localCIDockerService;
this.localCIProgrammingLanguageFeatureService = localCIProgrammingLanguageFeatureService;
this.programmingLanguageConfiguration = programmingLanguageConfiguration;
}

/**
Expand All @@ -79,10 +83,13 @@ public CompletableFuture<LocalCIBuildResult> addBuildJobToQueue(ProgrammingExerc

ProgrammingExercise programmingExercise = participation.getProgrammingExercise();

List<ProjectType> supportedProjectTypes = localCIProgrammingLanguageFeatureService.getProgrammingLanguageFeatures(programmingExercise.getProgrammingLanguage())
.projectTypes();
ProgrammingLanguage programmingLanguage = programmingExercise.getProgrammingLanguage();

ProjectType projectType = programmingExercise.getProjectType();

String dockerImage = programmingLanguageConfiguration.getImage(programmingLanguage, Optional.ofNullable(projectType));

var projectType = programmingExercise.getProjectType();
List<ProjectType> supportedProjectTypes = localCIProgrammingLanguageFeatureService.getProgrammingLanguageFeatures(programmingLanguage).projectTypes();

if (projectType != null && !supportedProjectTypes.contains(programmingExercise.getProjectType())) {
throw new LocalCIException("The project type " + programmingExercise.getProjectType() + " is not supported by the local CI.");
Expand All @@ -95,7 +102,7 @@ public CompletableFuture<LocalCIBuildResult> addBuildJobToQueue(ProgrammingExerc
String containerName = "artemis-local-ci-" + participation.getId() + "-" + ZonedDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"));

// Prepare a Callable that will later be called. It contains the actual steps needed to execute the build job.
Callable<LocalCIBuildResult> buildJob = () -> localCIBuildJobExecutionService.runBuildJob(participation, commitHash, containerName);
Callable<LocalCIBuildResult> buildJob = () -> localCIBuildJobExecutionService.runBuildJob(participation, commitHash, containerName, dockerImage);

// Wrap the buildJob Callable in a BuildJobTimeoutCallable, so that the build job is cancelled if it takes too long.
BuildJobTimeoutCallable<LocalCIBuildResult> timedBuildJob = new BuildJobTimeoutCallable<>(buildJob, timeoutSeconds);
Expand Down
Loading

0 comments on commit f2ad596

Please sign in to comment.