Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
bb6c5cc
Reused ProgrammingExerciseTestService cases in ProgrammingExerciseLoc…
enverkayandan Apr 29, 2025
8d7a6a0
Merge branch 'develop' into chore/improve-programming-exercise-tests
ekayandan Apr 29, 2025
d7150cc
Added some more tests from ProgrammingExerciseTestService
enverkayandan Apr 29, 2025
03399ff
Merge remote-tracking branch 'origin/chore/improve-programming-exerci…
enverkayandan Apr 29, 2025
80a5737
Merge branch 'develop' into chore/improve-programming-exercise-tests
ekayandan Apr 30, 2025
86c3cbf
removed failing tests
enverkayandan Apr 30, 2025
2d2d89c
Merge branch 'chore/improve-programming-exercise-tests' of github.com…
enverkayandan Apr 30, 2025
63a52ba
Created test for title change during Programming Exercise import
enverkayandan May 5, 2025
f72c324
Merge branch 'develop' into chore/improve-programming-exercise-tests
ekayandan May 5, 2025
7266050
Cleaned up importFromFile_validImportZip_changeTitle_success()
enverkayandan May 5, 2025
9770cab
Merge branch 'chore/improve-programming-exercise-tests' of github.com…
enverkayandan May 5, 2025
e9ead85
Merge branch 'develop' into chore/improve-programming-exercise-tests
ekayandan May 10, 2025
cbfa88d
Merge branch 'develop' into chore/improve-programming-exercise-tests
ekayandan May 11, 2025
7debd72
mocked aeolus for importFromFile_validImportZip_changeTitle_success
enverkayandan May 11, 2025
8fe8320
Re-enabled passing tests
enverkayandan May 11, 2025
e8e8eed
Converted zip reading logic to function for reuse
enverkayandan May 11, 2025
dab0a7b
Added test case: importFromFile_verifyBuildPlansCreated()
enverkayandan May 11, 2025
fcaedf7
Added test case: testCreateProgrammingExerciseWithSequentialTestRuns()
enverkayandan May 11, 2025
6567223
Merge branch 'develop' into chore/improve-programming-exercise-tests
ekayandan May 11, 2025
3b50812
small fix
enverkayandan May 11, 2025
5b0d281
Merge remote-tracking branch 'origin/chore/improve-programming-exerci…
enverkayandan May 11, 2025
e3753b7
Revert unsuccessful fix attempt
enverkayandan May 12, 2025
283a123
Merge branch 'develop' into chore/improve-programming-exercise-tests
ekayandan May 12, 2025
651d450
Removed unnecessary wait logic
enverkayandan May 12, 2025
61da91f
spotlessApply
enverkayandan May 12, 2025
589b656
Applied improvements from PR comments
enverkayandan May 12, 2025
01ced8b
Merge branch 'develop' into chore/improve-programming-exercise-tests
ekayandan May 13, 2025
fd17e25
Merge branch 'develop' into chore/improve-programming-exercise-tests
krusche May 13, 2025
3b6eeea
Fix localci working directory
enverkayandan May 14, 2025
7180112
spotlessApply
enverkayandan May 14, 2025
b46b3cb
Fixed flaky test case
enverkayandan May 14, 2025
8186130
Applied improvements from PR comments
enverkayandan May 14, 2025
376cb39
Merge branch 'develop' into chore/improve-programming-exercise-tests
ekayandan May 15, 2025
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 @@ -7,7 +7,8 @@
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.ZonedDateTime;
import java.util.Map;
import java.util.Optional;
Expand All @@ -25,13 +26,18 @@
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ArgumentsSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.util.LinkedMultiValueMap;

import de.tum.cit.aet.artemis.atlas.domain.competency.Competency;
import de.tum.cit.aet.artemis.atlas.domain.competency.CompetencyExerciseLink;
import de.tum.cit.aet.artemis.core.domain.Course;
import de.tum.cit.aet.artemis.exam.util.InvalidExamExerciseDatesArgumentProvider;
import de.tum.cit.aet.artemis.exam.util.InvalidExamExerciseDatesArgumentProvider.InvalidExamExerciseDateConfiguration;
import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationLocalCILocalVCTestBase;
import de.tum.cit.aet.artemis.programming.domain.AeolusTarget;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise;
Expand All @@ -43,6 +49,9 @@
import de.tum.cit.aet.artemis.programming.service.localvc.LocalVCRepositoryUri;
import de.tum.cit.aet.artemis.programming.util.LocalRepository;
import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseFactory;
import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseImportTestService;
import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseImportTestService.ImportFileResult;
import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseTestService;

// TestInstance.Lifecycle.PER_CLASS allows all test methods in this class to share the same instance of the test class.
// This reduces the overhead of repeatedly creating and tearing down a new Spring application context for each test method.
Expand Down Expand Up @@ -73,6 +82,12 @@ class ProgrammingExerciseLocalVCLocalCIIntegrationTest extends AbstractProgrammi

private Competency competency;

@Autowired
private ProgrammingExerciseTestService programmingExerciseTestService;

@Autowired
private ProgrammingExerciseImportTestService programmingExerciseImportTestService;

@BeforeAll
void setupAll() {
CredentialsProvider.setDefault(new UsernamePasswordCredentialsProvider(localVCUsername, localVCPassword));
Expand Down Expand Up @@ -124,6 +139,9 @@ void setup() throws Exception {
localVCLocalCITestService.verifyRepositoryFoldersExist(programmingExercise, localVCBasePath);

competency = competencyUtilService.createCompetency(course);

programmingExerciseTestService.setupTestUsers(TEST_PREFIX, 0, 0, 0, 0);
programmingExerciseTestService.setup(this, versionControlService, localVCGitBranchService);
}

@Override
Expand All @@ -132,11 +150,12 @@ protected String getTestPrefix() {
}

@AfterEach
void tearDown() throws IOException {
void tearDown() throws Exception {
templateRepository.resetLocalRepo();
solutionRepository.resetLocalRepo();
testsRepository.resetLocalRepo();
assignmentRepository.resetLocalRepo();
programmingExerciseTestService.tearDown();
}

@Disabled
Expand Down Expand Up @@ -217,7 +236,6 @@ void testUpdateProgrammingExercise_templateRepositoryUriIsInvalid() throws Excep
request.put("/api/programming/programming-exercises", programmingExercise, HttpStatus.BAD_REQUEST);
}

@Disabled
@Test
@WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR")
void testDeleteProgrammingExercise() throws Exception {
Expand Down Expand Up @@ -290,6 +308,196 @@ void testImportProgrammingExercise() throws Exception {
verify(competencyProgressApi).updateProgressByLearningObjectAsync(eq(importedExercise));
}

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

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

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

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

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

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

@ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}")
@ArgumentsSource(InvalidExamExerciseDatesArgumentProvider.class)
@WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR")
void createProgrammingExerciseForExam_invalidExercise_dates(InvalidExamExerciseDateConfiguration dates) throws Exception {
programmingExerciseTestService.createProgrammingExerciseForExam_invalidExercise_dates(dates);
}

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

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

/**
* Ensures <a href="https://github.com/ls1intum/Artemis/issues/7188">issue #7188</a> does not occur again
*
*/
@Test
@WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR")
void importFromFile_validImportZip_changeTitle_success() throws Exception {
aeolusRequestMockProvider.enableMockingOfRequests();
aeolusRequestMockProvider.mockFailedGenerateBuildPlan(AeolusTarget.CLI);

String uniqueSuffix = java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 20).toUpperCase();
String newTitle = "TITLE" + uniqueSuffix;
String newShortName = "SHORT" + uniqueSuffix;

ImportFileResult importResult = programmingExerciseImportTestService.prepareExerciseImport("test-data/import-from-file/valid-import.zip", exercise -> {
String oldTitle = exercise.getTitle();
exercise.setTitle(newTitle);
exercise.setShortName(newShortName);
return oldTitle;
}, course);

ProgrammingExercise importedExercise = importResult.importedExercise();
String oldTitle = (String) importResult.additionalData();

assertThat(importedExercise).isNotNull();
assertThat(importedExercise.getTitle()).isEqualTo(newTitle);
assertThat(importedExercise.getProgrammingLanguage()).isEqualTo(importResult.parsedExercise().getProgrammingLanguage());
assertThat(importedExercise.getCourseViaExerciseGroupOrCourseMember()).isEqualTo(course);

String repoClonePath = System.getProperty("artemis.repo-clone-path", "repos");
String projectKey = importResult.parsedExercise().getProjectKey();
Path exercisePath = Paths.get(repoClonePath, projectKey);
int newTitleCount = programmingExerciseImportTestService.countOccurrencesInDirectory(exercisePath, newTitle);
int oldTitleCount = programmingExerciseImportTestService.countOccurrencesInDirectory(exercisePath, oldTitle);

assertThat(newTitleCount).isEqualTo(programmingExerciseImportTestService.countOccurrencesInZip(importResult.resource(), oldTitle));
assertThat(oldTitleCount).isZero();
}

@Test
@WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR")
void importFromFile_validImportZip() throws Exception {
aeolusRequestMockProvider.enableMockingOfRequests();
aeolusRequestMockProvider.mockFailedGenerateBuildPlan(AeolusTarget.CLI);

ImportFileResult importResult = programmingExerciseImportTestService.prepareExerciseImport("test-data/import-from-file/valid-import.zip", exercise -> null, course);
ProgrammingExercise importedExercise = importResult.importedExercise();

assertThat(importedExercise).isNotNull();
assertThat(importedExercise.getTitle()).isEqualTo(importResult.parsedExercise().getTitle());
assertThat(importedExercise.getProgrammingLanguage()).isEqualTo(importResult.parsedExercise().getProgrammingLanguage());
assertThat(importedExercise.getCourseViaExerciseGroupOrCourseMember()).isEqualTo(course);
}

/**
* Ensures <a href="https://github.com/ls1intum/Artemis/issues/8562">issue #8562</a> does not occur again
*
*/
@Test
@WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR")
void importFromFile_verifyBuildPlansCreated() throws Exception {
aeolusRequestMockProvider.enableMockingOfRequests();
aeolusRequestMockProvider.mockFailedGenerateBuildPlan(AeolusTarget.CLI);

// Mock commit hash retrieval
dockerClientTestService.mockInputStreamReturnedFromContainer(dockerClient, LOCAL_CI_DOCKER_CONTAINER_WORKING_DIRECTORY + "/testing-dir/assignment/.git/refs/heads/[^/]+",
Map.of("assignmentCommitHash", DUMMY_COMMIT_HASH), Map.of("assignmentCommitHash", DUMMY_COMMIT_HASH));
dockerClientTestService.mockInputStreamReturnedFromContainer(dockerClient, LOCAL_CI_DOCKER_CONTAINER_WORKING_DIRECTORY + "/testing-dir/.git/refs/heads/[^/]+",
Map.of("testsCommitHash", DUMMY_COMMIT_HASH), Map.of("testsCommitHash", DUMMY_COMMIT_HASH));

// Mock image inspection
dockerClientTestService.mockInspectImage(dockerClient);

ImportFileResult importResult = programmingExerciseImportTestService.prepareExerciseImport("test-data/import-from-file/valid-import.zip", exercise -> null, course);
ProgrammingExercise importedExercise = importResult.importedExercise();

assertThat(importedExercise).isNotNull();

// Mock test results for builds
Map<String, String> templateBuildTestResults = dockerClientTestService.createMapFromTestResultsFolder(ALL_FAIL_TEST_RESULTS_PATH);
Map<String, String> solutionBuildTestResults = dockerClientTestService.createMapFromTestResultsFolder(ALL_SUCCEED_TEST_RESULTS_PATH);
dockerClientTestService.mockInputStreamReturnedFromContainer(dockerClient, LOCAL_CI_DOCKER_CONTAINER_WORKING_DIRECTORY + LOCAL_CI_RESULTS_DIRECTORY,
templateBuildTestResults, solutionBuildTestResults);

try {
// Refresh the exercise to get latest participation data
ProgrammingExercise refreshedExercise = programmingExerciseRepository.findWithAllParticipationsAndBuildConfigById(importedExercise.getId()).orElseThrow();

// Verify template build plan
TemplateProgrammingExerciseParticipation templateParticipation = templateProgrammingExerciseParticipationRepository
.findByProgrammingExerciseId(refreshedExercise.getId()).orElseThrow();

localVCLocalCITestService.testLatestSubmission(templateParticipation.getId(), null, 0, false, 30);

// Verify solution build plan
SolutionProgrammingExerciseParticipation solutionParticipation = solutionProgrammingExerciseParticipationRepository
.findByProgrammingExerciseId(refreshedExercise.getId()).orElseThrow();

localVCLocalCITestService.testLatestSubmission(solutionParticipation.getId(), null, 13, false, 30);
}
catch (Exception e) {
throw new AssertionError("Failed to verify build plans", e);
}
}

@Test
@WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR")
void testCreateProgrammingExerciseWithSequentialTestRuns() throws Exception {
ProgrammingExercise newExercise = ProgrammingExerciseFactory.generateProgrammingExercise(ZonedDateTime.now().minusDays(1), ZonedDateTime.now().plusDays(7), course);
newExercise.setProjectType(ProjectType.PLAIN_GRADLE);
// Enable sequential test runs
newExercise.getBuildConfig().setSequentialTestRuns(true);

// Mock dockerClient.copyArchiveFromContainerCmd() such that it returns a dummy commitHash for both the assignment and the test repository.
dockerClientTestService.mockInputStreamReturnedFromContainer(dockerClient, LOCAL_CI_DOCKER_CONTAINER_WORKING_DIRECTORY + "/testing-dir/assignment/.git/refs/heads/[^/]+",
Map.of("assignmentCommitHash", DUMMY_COMMIT_HASH), Map.of("assignmentCommitHash", DUMMY_COMMIT_HASH));
dockerClientTestService.mockInputStreamReturnedFromContainer(dockerClient, LOCAL_CI_DOCKER_CONTAINER_WORKING_DIRECTORY + "/testing-dir/.git/refs/heads/[^/]+",
Map.of("testsCommitHash", DUMMY_COMMIT_HASH), Map.of("testsCommitHash", DUMMY_COMMIT_HASH));

dockerClientTestService.mockInspectImage(dockerClient);

// Mock dockerClient.copyArchiveFromContainerCmd() such that it returns the XMLs containing the test results.
// Mock the results for the template repository build and for the solution repository build that will both be triggered as a result of creating the exercise.
Map<String, String> templateBuildTestResults = dockerClientTestService.createMapFromTestResultsFolder(ALL_FAIL_TEST_RESULTS_PATH);
Map<String, String> solutionBuildTestResults = dockerClientTestService.createMapFromTestResultsFolder(ALL_SUCCEED_TEST_RESULTS_PATH);
dockerClientTestService.mockInputStreamReturnedFromContainer(dockerClient, LOCAL_CI_DOCKER_CONTAINER_WORKING_DIRECTORY + LOCAL_CI_RESULTS_DIRECTORY,
templateBuildTestResults, solutionBuildTestResults);
newExercise.setChannelName("testchannelname-pe-sequential");
aeolusRequestMockProvider.enableMockingOfRequests();
aeolusRequestMockProvider.mockFailedGenerateBuildPlan(AeolusTarget.CLI);

// Create the exercise and verify status code 201
request.postWithResponseBody("/api/programming/programming-exercises/setup", newExercise, ProgrammingExercise.class, HttpStatus.CREATED);
}

@Nested
class TestGetCheckoutDirectories {

Expand Down
Loading
Loading