Skip to content

Commit

Permalink
[#11003] LnP tests for batch processing (#11027)
Browse files Browse the repository at this point in the history
* Add LnP test for enrollment update

* Change number of students

* Break the enrollment request into chunks of 50 students each

* Change access modifer
  • Loading branch information
daongochieu2810 authored Apr 4, 2021
1 parent 72bafbf commit 1420af9
Show file tree
Hide file tree
Showing 5 changed files with 386 additions and 6 deletions.
8 changes: 4 additions & 4 deletions src/lnp/java/teammates/lnp/cases/BaseLNPTestCase.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ public abstract class BaseLNPTestCase extends BaseTestCase {
protected static final String POST = HttpPost.METHOD_NAME;
protected static final String PUT = HttpPut.METHOD_NAME;
protected static final String DELETE = HttpDelete.METHOD_NAME;
protected static final Logger log = Logger.getLogger();

private static final BackDoor BACKDOOR = BackDoor.getInstance();
private static final Logger log = Logger.getLogger();

private static final int RESULT_COUNT = 3;

Expand Down Expand Up @@ -115,7 +115,7 @@ private String getPathToTestStatisticsResultsFile() {
this.getClass().getSimpleName(), this.timeStamp);
}

private String createFileAndDirectory(String directory, String fileName) throws IOException {
protected String createFileAndDirectory(String directory, String fileName) throws IOException {
File dir = new File(directory);
if (!dir.exists()) {
dir.mkdir();
Expand All @@ -135,7 +135,7 @@ private String createFileAndDirectory(String directory, String fileName) throws
/**
* Creates the JSON data and writes it to the file specified by {@link #getJsonDataPath()}.
*/
private void createJsonDataFile(LNPTestData testData) throws IOException {
void createJsonDataFile(LNPTestData testData) throws IOException {
DataBundle jsonData = testData.generateJsonData();

String pathToResultFile = createFileAndDirectory(TestProperties.LNP_TEST_DATA_FOLDER, getJsonDataPath());
Expand Down Expand Up @@ -169,7 +169,7 @@ private void createCsvConfigDataFile(LNPTestData testData) throws IOException {
* Converts the list of {@code values} to a CSV row.
* @return A single string containing {@code values} separated by pipelines and ending with newline.
*/
private String convertToCsv(List<String> values) {
String convertToCsv(List<String> values) {
StringJoiner csvRow = new StringJoiner("|", "", "\n");
for (String value : values) {
csvRow.add(value);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,360 @@
package teammates.lnp.cases;

import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.jmeter.protocol.http.control.HeaderManager;
import org.apache.jorphan.collections.HashTree;
import org.apache.jorphan.collections.ListedHashTree;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

import teammates.common.datatransfer.DataBundle;
import teammates.common.datatransfer.InstructorPrivileges;
import teammates.common.datatransfer.attributes.AccountAttributes;
import teammates.common.datatransfer.attributes.CourseAttributes;
import teammates.common.datatransfer.attributes.FeedbackResponseAttributes;
import teammates.common.datatransfer.attributes.FeedbackResponseCommentAttributes;
import teammates.common.datatransfer.attributes.InstructorAttributes;
import teammates.common.datatransfer.attributes.StudentAttributes;
import teammates.common.datatransfer.questions.FeedbackTextResponseDetails;
import teammates.common.exception.HttpRequestFailedException;
import teammates.common.exception.TeammatesException;
import teammates.common.util.Const;
import teammates.common.util.JsonUtils;
import teammates.lnp.util.JMeterElements;
import teammates.lnp.util.LNPSpecification;
import teammates.lnp.util.LNPTestData;
import teammates.lnp.util.TestProperties;
import teammates.ui.request.StudentsEnrollRequest;

/**
* L&P Test Case for cascading batch updating students.
*/
public class InstructorStudentCascadingUpdateLNPTest extends BaseLNPTestCase {
private static final int NUM_INSTRUCTORS = 1;
private static final int RAMP_UP_PERIOD = NUM_INSTRUCTORS * 2;

private static final int NUM_STUDENTS = 1000;
private static final int NUM_STUDENTS_PER_SECTION = 50;
private static final int NUMBER_OF_FEEDBACK_QUESTIONS = 20;

private static final String INSTRUCTOR_NAME = "LnPInstructor";
private static final String INSTRUCTOR_ID = "LnPInstructor_id";
private static final String INSTRUCTOR_EMAIL = "[email protected]";
private static final String COURSE_NAME = "tmms.test.gma-demo";
private static final String COURSE_ID = "tmms.test.gma-demo";

private static final String STUDENT_NAME_PREFIX = "LnPStudent";
private static final String STUDENT_ID_PREFIX = "LnPStudent.tmms";
private static final String STUDENT_EMAIL_SUBFIX = "@gmail.tmt";

private static final String FEEDBACK_RESPONSE_PREFIX = "LnPResponse";
private static final String FEEDBACK_SESSION_NAME = "LnPSession";

private static final double ERROR_RATE_LIMIT = 0.01;
private static final double MEAN_RESP_TIME_LIMIT = 60;

// To generate multiple csv files for multiple sections
private static int csvTestDataIndex;
private static LNPTestData testData;

@Override
protected LNPTestData getTestData() {
if (testData != null) {
return testData;
}
testData = new LNPTestData() {
@Override
protected Map<String, AccountAttributes> generateAccounts() {
return new HashMap<>();
}

@Override
protected Map<String, CourseAttributes> generateCourses() {
Map<String, CourseAttributes> courses = new HashMap<>();

courses.put(COURSE_ID, CourseAttributes.builder(COURSE_ID)
.withName(COURSE_NAME)
.withTimezone(ZoneId.of("UTC"))
.build()
);

return courses;
}

@Override
protected Map<String, StudentAttributes> generateStudents() {
Map<String, StudentAttributes> students = new HashMap<>();

for (int i = 0; i < NUM_STUDENTS; i++) {
students.put(STUDENT_NAME_PREFIX + i, StudentAttributes.builder(COURSE_ID,
STUDENT_NAME_PREFIX + i + STUDENT_EMAIL_SUBFIX)
.withGoogleId(STUDENT_ID_PREFIX + i)
.withName(STUDENT_NAME_PREFIX + i)
.withComment("This student's name is " + STUDENT_NAME_PREFIX + i)
.withSectionName(String.valueOf(i / NUM_STUDENTS_PER_SECTION))
.withTeamName(String.valueOf(i / NUM_STUDENTS_PER_SECTION))
.build());
}

return students;
}

@Override
protected Map<String, FeedbackResponseAttributes> generateFeedbackResponses() {
Map<String, FeedbackResponseAttributes> feedbackResponses = new HashMap<>();

for (int i = 1; i <= NUMBER_OF_FEEDBACK_QUESTIONS; i++) {
for (int j = 0; j <= NUM_STUDENTS; j++) {
String responseText = FEEDBACK_RESPONSE_PREFIX
+ " some random text to make the response has a reasonable length " + j;
FeedbackTextResponseDetails details =
new FeedbackTextResponseDetails(responseText);

feedbackResponses.put(responseText,
FeedbackResponseAttributes.builder(String.valueOf(i),
STUDENT_ID_PREFIX + j,
STUDENT_ID_PREFIX + j)
.withCourseId(COURSE_ID)
.withFeedbackSessionName(FEEDBACK_SESSION_NAME)
.withGiverSection(String.valueOf(j / NUM_STUDENTS_PER_SECTION))
.withRecipientSection(String.valueOf(j / NUM_STUDENTS_PER_SECTION))
.withResponseDetails(details)
.build());
}
}

return feedbackResponses;
}

@Override
protected Map<String, FeedbackResponseCommentAttributes> generateFeedbackResponseComments() {
Map<String, FeedbackResponseCommentAttributes> feedbackResponseComments = new HashMap<>();

for (int i = 1; i <= NUMBER_OF_FEEDBACK_QUESTIONS; i++) {
for (int j = 0; j <= NUM_STUDENTS; j++) {
String responseText = "This is a comment " + j;

feedbackResponseComments.put(responseText,
FeedbackResponseCommentAttributes.builder()
.withCourseId(COURSE_ID)
.withFeedbackResponseId(String.valueOf(i))
.withFeedbackSessionName(FEEDBACK_SESSION_NAME)
.withGiverSection(String.valueOf(j / NUM_STUDENTS_PER_SECTION))
.withCommentGiver(String.valueOf(j / NUM_STUDENTS_PER_SECTION))
.withCommentText(responseText)
.build());
}
}

return feedbackResponseComments;
}

@Override
protected Map<String, InstructorAttributes> generateInstructors() {
Map<String, InstructorAttributes> instructors = new HashMap<>();

instructors.put(INSTRUCTOR_ID,
InstructorAttributes.builder(COURSE_ID, INSTRUCTOR_EMAIL)
.withGoogleId(INSTRUCTOR_ID)
.withName(INSTRUCTOR_NAME)
.withRole("Co-owner")
.withIsDisplayedToStudents(true)
.withDisplayedName("Co-owner")
.withPrivileges(new InstructorPrivileges(
Const.InstructorPermissionRoleNames.INSTRUCTOR_PERMISSION_ROLE_COOWNER))
.build()
);

return instructors;
}

@Override
public List<String> generateCsvHeaders() {
List<String> headers = new ArrayList<>();

headers.add("loginId");
headers.add("isAdmin");
headers.add("courseId");
headers.add("enrollData");

return headers;
}

@Override
public List<List<String>> generateCsvData() {
DataBundle dataBundle = loadDataBundle(getJsonDataPath());
List<List<String>> csvData = new ArrayList<>();

dataBundle.instructors.forEach((key, instructor) -> {
List<String> csvRow = new ArrayList<>();

csvRow.add(instructor.googleId);
csvRow.add("yes");
csvRow.add(instructor.courseId);

// Create and add student enrollment data with a team number corresponding to each section number
List<StudentsEnrollRequest.StudentEnrollRequest> enrollRequests = new ArrayList<>();
int startIndex = csvTestDataIndex * NUM_STUDENTS_PER_SECTION;

for (int i = startIndex; i < startIndex + NUM_STUDENTS_PER_SECTION; i++) {
String name = instructor.name + ".Student" + (NUM_STUDENTS - i);
String email = STUDENT_NAME_PREFIX + i + STUDENT_EMAIL_SUBFIX;
String team = String.valueOf((NUM_STUDENTS - i) / NUM_STUDENTS_PER_SECTION);
String section = String.valueOf((NUM_STUDENTS - i) / NUM_STUDENTS_PER_SECTION);
String comment = "no comment";

enrollRequests.add(
new StudentsEnrollRequest.StudentEnrollRequest(name, email, team, section, comment)
);
}
String enrollData = sanitizeForCsv(JsonUtils.toJson(new StudentsEnrollRequest(enrollRequests)));
csvRow.add(enrollData);

csvData.add(csvRow);
});

return csvData;
}
};

return testData;
}

private Map<String, String> getRequestHeaders() {
Map<String, String> headers = new HashMap<>();

headers.put("X-CSRF-TOKEN", "${csrfToken}");
headers.put("Content-Type", "text/csv");

return headers;
}

private String getTestEndpoint() {
return Const.ResourceURIs.STUDENTS + "?courseid=${courseId}";
}

@Override
protected void createTestData() {
LNPTestData testData = getTestData();
try {
createJsonDataFile(testData);
persistTestData();
} catch (IOException | HttpRequestFailedException ex) {
log.severe(TeammatesException.toStringWithStackTrace(ex));
}
}

@Override
protected String getCsvConfigPath() {
return "/" + getClass().getSimpleName() + "Config_" + csvTestDataIndex + timeStamp + ".csv";
}

/**
* Generates csv data for each request, distinguished by csvTestDataIndex.
*/
protected void createCsvConfigDataFile() throws IOException {
List<String> headers = testData.generateCsvHeaders();
List<List<String>> valuesList = testData.generateCsvData();

String pathToCsvFile = createFileAndDirectory(TestProperties.LNP_TEST_DATA_FOLDER, getCsvConfigPath());
try (BufferedWriter bw = Files.newBufferedWriter(Paths.get(pathToCsvFile))) {
// Write headers and data to the CSV file
bw.write(convertToCsv(headers));

for (List<String> values : valuesList) {
bw.write(convertToCsv(values));
}

bw.flush();
}
}

@Override
protected ListedHashTree getLnpTestPlan() {
ListedHashTree testPlan = new ListedHashTree(JMeterElements.testPlan());
HashTree threadGroup = testPlan.add(
JMeterElements.threadGroup(NUM_INSTRUCTORS, RAMP_UP_PERIOD, 1));

threadGroup.add(JMeterElements.cookieManager());
threadGroup.add(JMeterElements.defaultSampler());

threadGroup.add(JMeterElements.onceOnlyController())
.add(JMeterElements.loginSampler())
.add(JMeterElements.csrfExtractor("csrfToken"));

// Add HTTP sampler for test endpoint
HeaderManager headerManager = JMeterElements.headerManager(getRequestHeaders());
// Mocks paginated calls from FE
for (int i = 0; i < NUM_STUDENTS / NUM_STUDENTS_PER_SECTION; i++) {
try {
createCsvConfigDataFile();
} catch (IOException e) {
e.printStackTrace();
break;
}
threadGroup.add(JMeterElements.csvDataSet(getPathToTestDataFile(getCsvConfigPath())));
threadGroup.add(JMeterElements.httpSampler(getTestEndpoint(), PUT, "${enrollData}"))
.add(headerManager);
csvTestDataIndex++;
}

return testPlan;
}

@Override
protected void setupSpecification() {
this.specification = LNPSpecification.builder()
.withErrorRateLimit(ERROR_RATE_LIMIT)
.withMeanRespTimeLimit(MEAN_RESP_TIME_LIMIT)
.build();
}

@BeforeClass
public void classSetup() {
generateTimeStamp();
createTestData();
setupSpecification();
}

@Test
public void runLnpTest() throws IOException {
runJmeter(false);
displayLnpResults();
}

@Override
protected void deleteDataFiles() throws IOException {
String pathToJsonFile = getPathToTestDataFile(getJsonDataPath());

csvTestDataIndex = 0;
for (int i = 0; i < NUM_STUDENTS / NUM_STUDENTS_PER_SECTION; i++) {
String pathToCsvFile = getPathToTestDataFile(getCsvConfigPath());
Files.delete(Paths.get(pathToCsvFile));
csvTestDataIndex++;
}

Files.delete(Paths.get(pathToJsonFile));
}

/**
* Removes the entities added for the instructors' student enrollment L&P test.
*/
@AfterClass
public void classTearDown() throws IOException {
// There is no need to add the newly enrolled students to the JSON DataBundle#students. This is because the new
// CourseStudent entities that were created are automatically deleted when the corresponding course is deleted.
deleteTestData();
deleteDataFiles();
cleanupResults();
}
}
Loading

0 comments on commit 1420af9

Please sign in to comment.