iterator = lines.iterator();
+ while (iterator.hasNext()) {
+ String line = iterator.next();
+ lineNumber++;
+ if (score == null && totalPoints == null) {
+ Matcher matcher = scorePattern.matcher(line);
+ if (matcher.find()) {
+ score = matcher.group(1);
+ totalPoints = matcher.group(2);
+ scoreLineNumber = lineNumber;
+ }
+ }
+
+ if (projectName == null && line.contains("The Joy of Coding")) {
+ Matcher matcher = projectNamePattern.matcher(line);
+ if (matcher.find()) {
+ projectName = "Project" + matcher.group(1);
+
+ } else {
+ matcher = koansProjectNamePattern.matcher(line);
+ if (matcher.find()) {
+ projectName = "koans";
+ }
+ }
+ }
+
+ if (submissionTime == null && line.contains("Submitted on")) {
+ submissionTime = parseSubmissionTime(line);
+ }
+ }
+
+ if (totalPoints == null) {
+ throw new TestedProjectSubmissionOutputParsingException("Could not find score line in project report");
+ }
+
+ if (projectName == null) {
+ throw new TestedProjectSubmissionOutputParsingException("Could not find project name in project report");
+ }
+
+ if (submissionTime == null) {
+ throw new TestedProjectSubmissionOutputParsingException("Could not find submission time in project report");
+ }
+
+ return new ProjectScore(score, scoreLineNumber, totalPoints, projectName, submissionTime);
+ }
+
+ static LocalDateTime parseSubmissionTime(String line) {
+ Matcher matcher = submissionTimePattern.matcher(line);
+ String timeString;
+ if (matcher.matches()) {
+ timeString = matcher.group(1).trim();
+
+ } else {
+ throw new IllegalArgumentException("Could not parse submission time from line: " + line);
+ }
+
+ try {
+ ZonedDateTime zoned;
+ try {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("E MMM d hh:mm:ss a z yyyy");
+ zoned = ZonedDateTime.parse(timeString, formatter);
+
+ } catch (DateTimeParseException ex) {
+ // Single-digit day format
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("E MMM d hh:mm:ss a z yyyy");
+ zoned = ZonedDateTime.parse(timeString, formatter);
+ }
+ return zoned.toLocalDateTime();
+
+ } catch (DateTimeParseException ex) {
+ return LocalDateTime.parse(timeString);
+ }
+ }
+
+ static class ProjectScore {
+ static final int UNREVIEWED_SCORE_LINE_NUMBER = 7;
+ private final double score;
+ private final double totalPoints;
+ private final String projectName;
+ private final boolean reviewed;
+ private final LocalDateTime submissionTime;
+
+ ProjectScore(String score, int scoreLineNumber, String totalPoints, String projectName, LocalDateTime submissionTime) {
+ this.score = score == null ? Double.NaN : Double.parseDouble(score);
+ this.totalPoints = Double.parseDouble(totalPoints);
+ this.projectName = projectName;
+ this.reviewed = !Double.isNaN(this.score) || scoreLineNumber > UNREVIEWED_SCORE_LINE_NUMBER;
+ this.submissionTime = submissionTime;
+ }
+
+ public double getScore() {
+ return this.score;
+ }
+
+ public double getTotalPoints() {
+ return this.totalPoints;
+ }
+
+ public String getProjectName() {
+ return projectName;
+ }
+
+ /**
+ * Determines if a submission has been reviewed by a grader.
+ * A submission is considered reviewed if:
+ * 1. It has a numeric score, OR
+ * 2. The score line appears after line 7 (indicating grader comments are present)
+ *
+ * When a score line like " out of X.X" appears after line 7, it indicates
+ * the grader has left feedback for the student to fix issues and resubmit.
+ */
+ public boolean isReviewed() {
+ return reviewed;
+ }
+
+ public LocalDateTime getSubmissionTime() {
+ return submissionTime;
+ }
+ }
+
+ @VisibleForTesting
+ static class TestedProjectSubmissionOutputParsingException extends Exception {
+
+ public TestedProjectSubmissionOutputParsingException(String message) {
+ super(message);
+ }
+ }
+}
diff --git a/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java b/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java
index 283dba292..7491c4fe8 100644
--- a/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java
+++ b/grader/src/test/java/edu/pdx/cs/joy/grader/FindUngradedSubmissionsTest.java
@@ -1,6 +1,9 @@
package edu.pdx.cs.joy.grader;
import edu.pdx.cs.joy.grader.FindUngradedSubmissions.TestOutputDetailsProviderFromTestOutputFile;
+import edu.pdx.cs.joy.grader.gradebook.Grade;
+import edu.pdx.cs.joy.grader.gradebook.GradeBook;
+import edu.pdx.cs.joy.grader.gradebook.Student;
import org.junit.jupiter.api.Test;
import java.nio.file.FileSystem;
@@ -8,6 +11,7 @@
import java.nio.file.Path;
import java.nio.file.spi.FileSystemProvider;
import java.time.LocalDateTime;
+import java.util.Optional;
import java.util.stream.Stream;
import static org.hamcrest.MatcherAssert.assertThat;
@@ -60,7 +64,8 @@ void submissionWithNoTestOutputNeedsToBeTested() {
FindUngradedSubmissions.TestOutputPathProvider testOutputProvider = mock(FindUngradedSubmissions.TestOutputPathProvider.class);
when(testOutputProvider.getTestOutput(any(Path.class), eq(studentId))).thenReturn(testOutput);
- FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, mock(FindUngradedSubmissions.TestOutputDetailsProvider.class));
+ FindUngradedSubmissions.GradeBookProvider gradeBookProvider = mock(FindUngradedSubmissions.GradeBookProvider.class);
+ FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, mock(FindUngradedSubmissions.TestOutputDetailsProvider.class), gradeBookProvider);
FindUngradedSubmissions.SubmissionAnalysis analysis = finder.analyzeSubmission(submission);
assertThat(analysis.needsToBeTested(), equalTo(true));
assertThat(analysis.needsToBeGraded(), equalTo(true));
@@ -83,10 +88,11 @@ void submissionWithTestOutputOlderThanSubmissionNeedsToBeTested() {
when(testOutputProvider.getTestOutput(any(Path.class), eq(studentId))).thenReturn(testOutput);
FindUngradedSubmissions.TestOutputDetailsProvider testOutputDetailsProvider = mock(FindUngradedSubmissions.TestOutputDetailsProvider.class);
- LocalDateTime testedSubmissionTime = submissionTime.minusDays(1); // Simulate test output older than submission
- when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(new FindUngradedSubmissions.TestOutputDetails(testedSubmissionTime, true));
+ LocalDateTime testedSubmissionTime = submissionTime.minusDays(1); // Old test output
+ when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(new FindUngradedSubmissions.TestOutputDetails(testOutput, testedSubmissionTime, true, null, null, true));
- FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, testOutputDetailsProvider);
+ FindUngradedSubmissions.GradeBookProvider gradeBookProvider = mock(FindUngradedSubmissions.GradeBookProvider.class);
+ FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, testOutputDetailsProvider, gradeBookProvider);
FindUngradedSubmissions.SubmissionAnalysis analysis = finder.analyzeSubmission(submission);
assertThat(analysis.needsToBeTested(), equalTo(true));
assertThat(analysis.needsToBeGraded(), equalTo(true));
@@ -108,10 +114,11 @@ void submissionWithTestOutputLessThanAMinuteOlderThanSubmissionDoesNotNeedToBeTe
when(testOutputProvider.getTestOutput(any(Path.class), eq(studentId))).thenReturn(testOutput);
FindUngradedSubmissions.TestOutputDetailsProvider testOutputDetailsProvider = mock(FindUngradedSubmissions.TestOutputDetailsProvider.class);
- LocalDateTime testedSubmissionTime = submissionTime.minusSeconds(10); // Simulate test output older than submission
- when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(new FindUngradedSubmissions.TestOutputDetails(testedSubmissionTime, true));
+ LocalDateTime testedSubmissionTime = submissionTime.plusMinutes(30); // Still within 1 minute tolerance
+ when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(new FindUngradedSubmissions.TestOutputDetails(testOutput, testedSubmissionTime, true, null, null, true));
- FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, testOutputDetailsProvider);
+ FindUngradedSubmissions.GradeBookProvider gradeBookProvider = mock(FindUngradedSubmissions.GradeBookProvider.class);
+ FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, testOutputDetailsProvider, gradeBookProvider);
FindUngradedSubmissions.SubmissionAnalysis analysis = finder.analyzeSubmission(submission);
assertThat(analysis.needsToBeTested(), equalTo(false));
}
@@ -133,12 +140,15 @@ void submissionWithNoGradeNeedsToBeGraded() {
FindUngradedSubmissions.TestOutputDetailsProvider testOutputDetailsProvider = mock(FindUngradedSubmissions.TestOutputDetailsProvider.class);
LocalDateTime gradedTime = submissionTime.plusDays(1); // Simulate test output newer than submission
- when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(new FindUngradedSubmissions.TestOutputDetails(gradedTime, false));
+ boolean hasBeenReviewed = false;
+ when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(new FindUngradedSubmissions.TestOutputDetails(testOutput, gradedTime, false, null, null, hasBeenReviewed));
- FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, testOutputDetailsProvider);
+ FindUngradedSubmissions.GradeBookProvider gradeBookProvider = mock(FindUngradedSubmissions.GradeBookProvider.class);
+ FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, testOutputDetailsProvider, gradeBookProvider);
FindUngradedSubmissions.SubmissionAnalysis analysis = finder.analyzeSubmission(submission);
assertThat(analysis.needsToBeTested(), equalTo(false));
assertThat(analysis.needsToBeGraded(), equalTo(true));
+ assertThat(analysis.testOutput(), equalTo(testOutput));
}
@Test
@@ -158,33 +168,75 @@ void submissionWithGradeIsGraded() {
FindUngradedSubmissions.TestOutputDetailsProvider testOutputDetailsProvider = mock(FindUngradedSubmissions.TestOutputDetailsProvider.class);
LocalDateTime gradedTime = submissionTime.plusDays(1);
- when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(new FindUngradedSubmissions.TestOutputDetails(gradedTime, true));
+ when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(new FindUngradedSubmissions.TestOutputDetails(testOutput, gradedTime, true, null, null, true));
- FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, testOutputDetailsProvider);
+ FindUngradedSubmissions.GradeBookProvider gradeBookProvider = mock(FindUngradedSubmissions.GradeBookProvider.class);
+ FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, testOutputDetailsProvider, gradeBookProvider);
FindUngradedSubmissions.SubmissionAnalysis analysis = finder.analyzeSubmission(submission);
assertThat(analysis.needsToBeTested(), equalTo(false));
assertThat(analysis.needsToBeGraded(), equalTo(false));
}
@Test
- void parseSubmissionTimeFromAndroidProjectTestOutputLine() {
- LocalDateTime submissionTime = TestOutputDetailsProviderFromTestOutputFile.parseSubmissionTime(" Submitted on 2025-08-18T11:34:19.017953486");
- LocalDateTime expectedTime = LocalDateTime.of(2025, 8, 18, 11, 34, 19, 17953486);
- assertThat(submissionTime, equalTo(expectedTime));
+ void gradedSubmissionForStudentNotInGradebookDoesNotNeedToBeRecorded() {
+ FindUngradedSubmissions.SubmissionDetailsProvider submissionDetailsProvider = mock(FindUngradedSubmissions.SubmissionDetailsProvider.class);
+
+ String studentId = "student123";
+ LocalDateTime submissionTime = LocalDateTime.now();
+ FindUngradedSubmissions.SubmissionDetails submissionDetails = new FindUngradedSubmissions.SubmissionDetails(studentId, submissionTime);
+ Path submission = getPathToExistingFile();
+ when(submissionDetailsProvider.getSubmissionDetails(submission)).thenReturn(submissionDetails);
+
+ Path testOutput = getPathToExistingFile();
+
+ FindUngradedSubmissions.TestOutputPathProvider testOutputProvider = mock(FindUngradedSubmissions.TestOutputPathProvider.class);
+ when(testOutputProvider.getTestOutput(any(Path.class), eq(studentId))).thenReturn(testOutput);
+
+ FindUngradedSubmissions.TestOutputDetailsProvider testOutputDetailsProvider = mock(FindUngradedSubmissions.TestOutputDetailsProvider.class);
+ LocalDateTime gradedTime = submissionTime.plusDays(1);
+ when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(
+ new FindUngradedSubmissions.TestOutputDetails(testOutput, gradedTime, true, "Project1", 12.5, true));
+
+ // Student does not exist in the gradebook
+ GradeBook gradeBook = mock(GradeBook.class);
+ when(gradeBook.getStudent(studentId)).thenReturn(Optional.empty());
+
+ FindUngradedSubmissions.GradeBookProvider gradeBookProvider = mock(FindUngradedSubmissions.GradeBookProvider.class);
+ when(gradeBookProvider.getGradeBook()).thenReturn(Optional.of(gradeBook));
+
+ FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, testOutputDetailsProvider, gradeBookProvider);
+ FindUngradedSubmissions.SubmissionAnalysis analysis = finder.analyzeSubmission(submission);
+ assertThat(analysis.needsToBeTested(), equalTo(false));
+ assertThat(analysis.needsToBeGraded(), equalTo(false));
+ assertThat(analysis.gradeNeedsToBeRecorded(), equalTo(false));
+ }
+
+ @Test
+ void parseProjectNameFromTestOutputLine() {
+ String line = " The Joy of Coding Project 1: edu.pdx.cs410J.studentId.Project1";
+ String projectName = TestOutputDetailsProviderFromTestOutputFile.parseProjectName(line);
+ assertThat(projectName, equalTo("Project1"));
}
@Test
- void parseSubmissionTimeFromTestOutputLine() {
- LocalDateTime submissionTime = TestOutputDetailsProviderFromTestOutputFile.parseSubmissionTime(" Submitted on Wed Aug 6 01:13:59 PM PDT 2025");
- LocalDateTime expectedTime = LocalDateTime.of(2025, 8, 6, 13, 13, 59);
- assertThat(submissionTime, equalTo(expectedTime));
+ void parseProjectNameFromTestOutputLineWithDifferentProject() {
+ String line = " The Joy of Coding Project 3: edu.pdx.cs.joy.student.Project3";
+ String projectName = TestOutputDetailsProviderFromTestOutputFile.parseProjectName(line);
+ assertThat(projectName, equalTo("Project3"));
}
@Test
- void parseSubmissionTimeFromTestOutputLineWithTwoDigitDay() {
- LocalDateTime submissionTime = TestOutputDetailsProviderFromTestOutputFile.parseSubmissionTime(" Submitted on Wed Jul 23 12:59:13 PM PDT 2025");
- LocalDateTime expectedTime = LocalDateTime.of(2025, 7, 23, 12, 59, 13);
- assertThat(submissionTime, equalTo(expectedTime));
+ void lineWithoutProjectNameReturnsNull() {
+ String line = "Some random line without a project";
+ String projectName = TestOutputDetailsProviderFromTestOutputFile.parseProjectName(line);
+ assertThat(projectName, equalTo(null));
+ }
+
+ @Test
+ void parseProject6NameFromTestOutputLine() {
+ String line = " The Joy of Coding Project 6: Android";
+ String projectName = TestOutputDetailsProviderFromTestOutputFile.parseProjectName(line);
+ assertThat(projectName, equalTo("Project6"));
}
@Test
@@ -209,20 +261,43 @@ void lineWithMissingGradeHasNaNGrade() {
}
@Test
- void parseTestOutputDetails() {
+ void parseTestOutputDetails() throws TestedProjectSubmissionOutputParser.TestedProjectSubmissionOutputParsingException {
+ Stream lines = Stream.of(
+ " The Joy of Coding Project 2: edu.pdx.cs410J.whitlock.Project2",
+ " Submitted on Wed Aug 6 01:13:59 PM PDT 2025",
+ "",
+ "12.5 out of 13.0"
+ );
+ Path testOutput = mock(Path.class);
+ FindUngradedSubmissions.TestOutputDetails details = TestOutputDetailsProviderFromTestOutputFile.parseTestOutputDetails(testOutput, lines);
+ LocalDateTime submissionTime = LocalDateTime.of(2025, 8, 6, 13, 13, 59);
+ assertThat(details.testOutput(), equalTo(testOutput));
+ assertThat(details.testedSubmissionTime(), equalTo(submissionTime));
+ assertThat(details.hasGrade(), equalTo(true));
+ }
+
+ @Test
+ void testOutputDetailsIncludesProjectNameAndGrade() throws TestedProjectSubmissionOutputParser.TestedProjectSubmissionOutputParsingException {
Stream lines = Stream.of(
+ " The Joy of Coding Project 2: edu.pdx.cs410J.whitlock.Project2",
+ " Submitted by Dave Whitlock",
" Submitted on Wed Aug 6 01:13:59 PM PDT 2025",
+ " Graded on Wed Aug 6 02:30:00 PM PDT 2025",
"",
"12.5 out of 13.0"
);
- FindUngradedSubmissions.TestOutputDetails details = TestOutputDetailsProviderFromTestOutputFile.parseTestOutputDetails(lines);
+ Path testOutput = mock(Path.class);
+ FindUngradedSubmissions.TestOutputDetails details = TestOutputDetailsProviderFromTestOutputFile.parseTestOutputDetails(testOutput, lines);
LocalDateTime submissionTime = LocalDateTime.of(2025, 8, 6, 13, 13, 59);
+ assertThat(details.testOutput(), equalTo(testOutput));
assertThat(details.testedSubmissionTime(), equalTo(submissionTime));
assertThat(details.hasGrade(), equalTo(true));
+ assertThat(details.projectName(), equalTo("Project2"));
+ assertThat(details.grade(), equalTo(12.5));
}
@Test
- void testOutputDetailsWithMessageToStudentDoesNotNeedToBeGraded() {
+ void testOutputDetailsWithMessageToStudentDoesNotNeedToBeGraded() throws TestedProjectSubmissionOutputParser.TestedProjectSubmissionOutputParsingException {
Stream lines = Stream.of(
"Hi, Student. There were some problems with your submission",
"",
@@ -236,8 +311,229 @@ void testOutputDetailsWithMessageToStudentDoesNotNeedToBeGraded() {
"",
" out of 7.0"
);
- FindUngradedSubmissions.TestOutputDetails details = TestOutputDetailsProviderFromTestOutputFile.parseTestOutputDetails(lines);
- assertThat(details.hasGrade(), equalTo(true));
+ Path testOutput = mock(Path.class);
+ FindUngradedSubmissions.TestOutputDetails details = TestOutputDetailsProviderFromTestOutputFile.parseTestOutputDetails(testOutput, lines);
+ assertThat(details.hasGrade(), equalTo(false));
+ assertThat(details.hasBeenReviewed(), equalTo(true));
+ }
+
+ @Test
+ void submissionAnalysisIncludesGradeNeedsToBeRecorded() {
+ FindUngradedSubmissions.SubmissionDetailsProvider submissionDetailsProvider = mock(FindUngradedSubmissions.SubmissionDetailsProvider.class);
+ String studentId = "student123";
+ LocalDateTime submissionTime = LocalDateTime.now();
+ FindUngradedSubmissions.SubmissionDetails submissionDetails = new FindUngradedSubmissions.SubmissionDetails(studentId, submissionTime);
+ Path submission = getPathToExistingFile();
+ when(submissionDetailsProvider.getSubmissionDetails(submission)).thenReturn(submissionDetails);
+
+ Path testOutput = getPathToExistingFile();
+
+ FindUngradedSubmissions.TestOutputPathProvider testOutputProvider = mock(FindUngradedSubmissions.TestOutputPathProvider.class);
+ when(testOutputProvider.getTestOutput(any(Path.class), eq(studentId))).thenReturn(testOutput);
+
+ FindUngradedSubmissions.TestOutputDetailsProvider testOutputDetailsProvider = mock(FindUngradedSubmissions.TestOutputDetailsProvider.class);
+ LocalDateTime gradedTime = submissionTime.plusDays(1);
+ when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(new FindUngradedSubmissions.TestOutputDetails(testOutput, gradedTime, true, "Project1", 12.5, true));
+
+ FindUngradedSubmissions.GradeBookProvider gradeBookProvider = mock(FindUngradedSubmissions.GradeBookProvider.class);
+ FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, testOutputDetailsProvider, gradeBookProvider);
+ FindUngradedSubmissions.SubmissionAnalysis analysis = finder.analyzeSubmission(submission);
+
+ assertThat(analysis.needsToBeTested(), equalTo(false));
+ assertThat(analysis.needsToBeGraded(), equalTo(false));
+ assertThat(analysis.gradeNeedsToBeRecorded(), equalTo(false));
+ }
+
+ @Test
+ void submissionWithGradeMissingFromGradeBookNeedsToBeRecorded() {
+ FindUngradedSubmissions.SubmissionDetailsProvider submissionDetailsProvider = mock(FindUngradedSubmissions.SubmissionDetailsProvider.class);
+
+ String studentId = "student123";
+ LocalDateTime submissionTime = LocalDateTime.now();
+ FindUngradedSubmissions.SubmissionDetails submissionDetails = new FindUngradedSubmissions.SubmissionDetails(studentId, submissionTime);
+ Path submission = getPathToExistingFile();
+ when(submissionDetailsProvider.getSubmissionDetails(submission)).thenReturn(submissionDetails);
+
+ Path testOutput = getPathToExistingFile();
+
+ FindUngradedSubmissions.TestOutputPathProvider testOutputProvider = mock(FindUngradedSubmissions.TestOutputPathProvider.class);
+ when(testOutputProvider.getTestOutput(any(Path.class), eq(studentId))).thenReturn(testOutput);
+
+ FindUngradedSubmissions.TestOutputDetailsProvider testOutputDetailsProvider = mock(FindUngradedSubmissions.TestOutputDetailsProvider.class);
+ LocalDateTime gradedTime = submissionTime.plusDays(1);
+ when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(new FindUngradedSubmissions.TestOutputDetails(testOutput, gradedTime, true, "Project1", 12.5, true));
+
+ // Create a mock GradeBook with a student that doesn't have a grade for Project1
+ GradeBook gradeBook = mock(GradeBook.class);
+ Student student = mock(Student.class);
+ when(gradeBook.getStudent(studentId)).thenReturn(Optional.of(student));
+ when(student.getGrade("Project1")).thenReturn(null); // No grade recorded
+
+ FindUngradedSubmissions.GradeBookProvider gradeBookProvider = mock(FindUngradedSubmissions.GradeBookProvider.class);
+ when(gradeBookProvider.getGradeBook()).thenReturn(Optional.of(gradeBook));
+
+ FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, testOutputDetailsProvider, gradeBookProvider);
+ FindUngradedSubmissions.SubmissionAnalysis analysis = finder.analyzeSubmission(submission);
+
+ assertThat(analysis.needsToBeTested(), equalTo(false));
+ assertThat(analysis.needsToBeGraded(), equalTo(false));
+ assertThat(analysis.gradeNeedsToBeRecorded(), equalTo(true)); // This should be true!
+ }
+
+ @Test
+ void submissionWithDifferentGradeInGradeBookNeedsToBeRecorded() {
+ FindUngradedSubmissions.SubmissionDetailsProvider submissionDetailsProvider = mock(FindUngradedSubmissions.SubmissionDetailsProvider.class);
+
+ String studentId = "student456";
+ LocalDateTime submissionTime = LocalDateTime.now();
+ FindUngradedSubmissions.SubmissionDetails submissionDetails = new FindUngradedSubmissions.SubmissionDetails(studentId, submissionTime);
+ Path submission = getPathToExistingFile();
+ when(submissionDetailsProvider.getSubmissionDetails(submission)).thenReturn(submissionDetails);
+
+ Path testOutput = getPathToExistingFile();
+
+ FindUngradedSubmissions.TestOutputPathProvider testOutputProvider = mock(FindUngradedSubmissions.TestOutputPathProvider.class);
+ when(testOutputProvider.getTestOutput(any(Path.class), eq(studentId))).thenReturn(testOutput);
+
+ FindUngradedSubmissions.TestOutputDetailsProvider testOutputDetailsProvider = mock(FindUngradedSubmissions.TestOutputDetailsProvider.class);
+ LocalDateTime gradedTime = submissionTime.plusDays(1);
+ when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(new FindUngradedSubmissions.TestOutputDetails(testOutput, gradedTime, true, "Project2", 12.5, true));
+
+ // Create a mock GradeBook with a student that has a DIFFERENT grade for Project2
+ GradeBook gradeBook = mock(GradeBook.class);
+ Student student = mock(Student.class);
+ Grade grade = mock(Grade.class);
+
+ when(gradeBook.getStudent(studentId)).thenReturn(Optional.of(student));
+ when(student.getGrade("Project2")).thenReturn(grade);
+ when(grade.getScore()).thenReturn(10.0); // GradeBook has 10.0, test output has 12.5
+
+ FindUngradedSubmissions.GradeBookProvider gradeBookProvider = mock(FindUngradedSubmissions.GradeBookProvider.class);
+ when(gradeBookProvider.getGradeBook()).thenReturn(Optional.of(gradeBook));
+
+ FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, testOutputDetailsProvider, gradeBookProvider);
+ FindUngradedSubmissions.SubmissionAnalysis analysis = finder.analyzeSubmission(submission);
+
+ assertThat(analysis.needsToBeTested(), equalTo(false));
+ assertThat(analysis.needsToBeGraded(), equalTo(false));
+ assertThat(analysis.gradeNeedsToBeRecorded(), equalTo(true)); // Grades differ!
+ }
+
+ @Test
+ void submissionWithMatchingGradeInGradeBookDoesNotNeedToBeRecorded() {
+ FindUngradedSubmissions.SubmissionDetailsProvider submissionDetailsProvider = mock(FindUngradedSubmissions.SubmissionDetailsProvider.class);
+
+ String studentId = "student789";
+ LocalDateTime submissionTime = LocalDateTime.now();
+ FindUngradedSubmissions.SubmissionDetails submissionDetails = new FindUngradedSubmissions.SubmissionDetails(studentId, submissionTime);
+ Path submission = getPathToExistingFile();
+ when(submissionDetailsProvider.getSubmissionDetails(submission)).thenReturn(submissionDetails);
+
+ Path testOutput = getPathToExistingFile();
+
+ FindUngradedSubmissions.TestOutputPathProvider testOutputProvider = mock(FindUngradedSubmissions.TestOutputPathProvider.class);
+ when(testOutputProvider.getTestOutput(any(Path.class), eq(studentId))).thenReturn(testOutput);
+
+ FindUngradedSubmissions.TestOutputDetailsProvider testOutputDetailsProvider = mock(FindUngradedSubmissions.TestOutputDetailsProvider.class);
+ LocalDateTime gradedTime = submissionTime.plusDays(1);
+ when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(new FindUngradedSubmissions.TestOutputDetails(testOutput, gradedTime, true, "Project3", 15.0, true));
+
+ // Create a mock GradeBook with a student that has a MATCHING grade for Project3
+ GradeBook gradeBook = mock(GradeBook.class);
+ Student student = mock(Student.class);
+ Grade grade = mock(Grade.class);
+
+ when(gradeBook.getStudent(studentId)).thenReturn(Optional.of(student));
+ when(student.getGrade("Project3")).thenReturn(grade);
+ when(grade.getScore()).thenReturn(15.0); // GradeBook and test output both have 15.0
+
+ FindUngradedSubmissions.GradeBookProvider gradeBookProvider = mock(FindUngradedSubmissions.GradeBookProvider.class);
+ when(gradeBookProvider.getGradeBook()).thenReturn(Optional.of(gradeBook));
+
+ FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, testOutputDetailsProvider, gradeBookProvider);
+ FindUngradedSubmissions.SubmissionAnalysis analysis = finder.analyzeSubmission(submission);
+
+ assertThat(analysis.needsToBeTested(), equalTo(false));
+ assertThat(analysis.needsToBeGraded(), equalTo(false));
+ assertThat(analysis.gradeNeedsToBeRecorded(), equalTo(false)); // Grades match!
+ }
+
+ @Test
+ void ungradedSubmissionIsNotConsideredUnrecordedEvenIfGradebookHasGrade() {
+ FindUngradedSubmissions.SubmissionDetailsProvider submissionDetailsProvider = mock(FindUngradedSubmissions.SubmissionDetailsProvider.class);
+
+ String studentId = "student999";
+ LocalDateTime submissionTime = LocalDateTime.now();
+ FindUngradedSubmissions.SubmissionDetails submissionDetails = new FindUngradedSubmissions.SubmissionDetails(studentId, submissionTime);
+ Path submission = getPathToExistingFile();
+ when(submissionDetailsProvider.getSubmissionDetails(submission)).thenReturn(submissionDetails);
+
+ Path testOutput = getPathToExistingFile();
+
+ FindUngradedSubmissions.TestOutputPathProvider testOutputProvider = mock(FindUngradedSubmissions.TestOutputPathProvider.class);
+ when(testOutputProvider.getTestOutput(any(Path.class), eq(studentId))).thenReturn(testOutput);
+
+ FindUngradedSubmissions.TestOutputDetailsProvider testOutputDetailsProvider = mock(FindUngradedSubmissions.TestOutputDetailsProvider.class);
+ LocalDateTime gradedTime = submissionTime.plusDays(1);
+ // Test output has NO grade yet (hasGrade = false, grade = null)
+ boolean hasBeenReviewed = false;
+ when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(new FindUngradedSubmissions.TestOutputDetails(testOutput, gradedTime, false, "Project4", null, hasBeenReviewed));
+
+ // Create a mock GradeBook with a student that HAS a grade for Project4 in the gradebook
+ GradeBook gradeBook = mock(GradeBook.class);
+ Student student = mock(Student.class);
+ Grade grade = mock(Grade.class);
+
+ when(gradeBook.getStudent(studentId)).thenReturn(Optional.of(student));
+ when(student.getGrade("Project4")).thenReturn(grade);
+ when(grade.getScore()).thenReturn(20.0); // GradeBook has a grade, but test output doesn't
+
+ FindUngradedSubmissions.GradeBookProvider gradeBookProvider = mock(FindUngradedSubmissions.GradeBookProvider.class);
+ when(gradeBookProvider.getGradeBook()).thenReturn(Optional.of(gradeBook));
+
+ FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, testOutputDetailsProvider, gradeBookProvider);
+ FindUngradedSubmissions.SubmissionAnalysis analysis = finder.analyzeSubmission(submission);
+
+ // The submission needs to be graded (no grade in test output yet)
+ assertThat(analysis.needsToBeTested(), equalTo(false));
+ assertThat(analysis.needsToBeGraded(), equalTo(true));
+ // Even though gradebook has a grade, since submission isn't graded yet, it should NOT be considered unrecorded
+ assertThat(analysis.gradeNeedsToBeRecorded(), equalTo(false));
+ }
+
+
+ @Test
+ void reviewedSubmissionDoesNotNeedToBeTestedGradedOrRecorded() {
+ FindUngradedSubmissions.SubmissionDetailsProvider submissionDetailsProvider = mock(FindUngradedSubmissions.SubmissionDetailsProvider.class);
+
+ String studentId = "student999";
+ LocalDateTime submissionTime = LocalDateTime.now();
+ FindUngradedSubmissions.SubmissionDetails submissionDetails = new FindUngradedSubmissions.SubmissionDetails(studentId, submissionTime);
+ Path submission = getPathToExistingFile();
+ when(submissionDetailsProvider.getSubmissionDetails(submission)).thenReturn(submissionDetails);
+
+ Path testOutput = getPathToExistingFile();
+
+ FindUngradedSubmissions.TestOutputPathProvider testOutputProvider = mock(FindUngradedSubmissions.TestOutputPathProvider.class);
+ when(testOutputProvider.getTestOutput(any(Path.class), eq(studentId))).thenReturn(testOutput);
+
+ FindUngradedSubmissions.TestOutputDetailsProvider testOutputDetailsProvider = mock(FindUngradedSubmissions.TestOutputDetailsProvider.class);
+ LocalDateTime gradedTime = submissionTime.plusDays(1);
+ // Test output has NO grade yet (hasGrade = false, grade = null)
+ boolean hasGrade = false;
+ boolean hasBeenReviewed = true;
+ when(testOutputDetailsProvider.getTestOutputDetails(testOutput)).thenReturn(new FindUngradedSubmissions.TestOutputDetails(testOutput, gradedTime, hasGrade, "Project4", null, hasBeenReviewed));
+
+ FindUngradedSubmissions.GradeBookProvider gradeBookProvider = mock(FindUngradedSubmissions.GradeBookProvider.class);
+
+ FindUngradedSubmissions finder = new FindUngradedSubmissions(submissionDetailsProvider, testOutputProvider, testOutputDetailsProvider, gradeBookProvider);
+ FindUngradedSubmissions.SubmissionAnalysis analysis = finder.analyzeSubmission(submission);
+
+ // The submission needs to be graded (no grade in test output yet)
+ assertThat(analysis.needsToBeTested(), equalTo(false));
+ assertThat(analysis.needsToBeGraded(), equalTo(false));
+ assertThat(analysis.gradeNeedsToBeRecorded(), equalTo(false));
}
}
+
diff --git a/grader/src/test/java/edu/pdx/cs/joy/grader/ProjectGradesImporterTest.java b/grader/src/test/java/edu/pdx/cs/joy/grader/ProjectGradesImporterTest.java
index 26b8096e7..6df9b148f 100644
--- a/grader/src/test/java/edu/pdx/cs/joy/grader/ProjectGradesImporterTest.java
+++ b/grader/src/test/java/edu/pdx/cs/joy/grader/ProjectGradesImporterTest.java
@@ -1,5 +1,7 @@
package edu.pdx.cs.joy.grader;
+import edu.pdx.cs.joy.grader.TestedProjectSubmissionOutputParser.TestedProjectSubmissionOutputParsingException;
+import edu.pdx.cs.joy.grader.TestedProjectSubmissionOutputParserTest.TestedProjectSubmissionOutput;
import edu.pdx.cs.joy.grader.gradebook.Assignment;
import edu.pdx.cs.joy.grader.gradebook.GradeBook;
import edu.pdx.cs.joy.grader.gradebook.Student;
@@ -8,9 +10,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.io.Reader;
-import java.io.StringReader;
-import java.util.regex.Matcher;
+import java.io.IOException;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
@@ -20,104 +20,10 @@
public class ProjectGradesImporterTest {
- private Logger logger = LoggerFactory.getLogger(this.getClass().getPackage().getName());
+ private final Logger logger = LoggerFactory.getLogger(this.getClass().getPackage().getName());
@Test
- public void gradedProjectWithNoGradeThrowsScoreNotFoundException() throws ProjectGradesImporter.ScoreNotFoundException {
- GradedProject project = new GradedProject();
- project.addLine("asdfhjkl");
- project.addLine("iadguow");
-
- assertThrows(ProjectGradesImporter.ScoreNotFoundException.class, () ->
- ProjectGradesImporter.getScoreFrom(project.getReader())
- );
- }
-
- @Test
- public void scoreRegularExpressionWorkWithSimpleCase() {
- Matcher matcher = ProjectGradesImporter.scorePattern.matcher("3.0 out of 4.5");
- assertThat(matcher.find(), equalTo(true));
- assertThat(matcher.groupCount(), equalTo(2));
- assertThat(matcher.group(1), equalTo("3.0"));
- assertThat(matcher.group(2), equalTo("4.5"));
- }
-
- @Test
- public void gradedProjectWithOutOfHasValidScore() throws ProjectGradesImporter.ScoreNotFoundException {
- GradedProject project = new GradedProject();
- project.addLine("3.4 out of 3.5");
- project.addLine("iadguow");
-
- ProjectGradesImporter.ProjectScore score = ProjectGradesImporter.getScoreFrom(project.getReader());
- assertThat(score.getScore(), equalTo(3.4));
- assertThat(score.getTotalPoints(), equalTo(3.5));
- }
-
- @Test
- public void scoreMatchesRegardlessOfCase() {
- Matcher matcher = ProjectGradesImporter.scorePattern.matcher("4.0 OUT OF 5.0");
- assertThat(matcher.find(), equalTo(true));
- assertThat(matcher.groupCount(), equalTo(2));
- assertThat(matcher.group(1), equalTo("4.0"));
- assertThat(matcher.group(2), equalTo("5.0"));
- }
-
- @Test
- public void scoreMatchesIntegerPoints() {
- Matcher matcher = ProjectGradesImporter.scorePattern.matcher("4 out of 5");
- assertThat(matcher.find(), equalTo(true));
- assertThat(matcher.groupCount(), equalTo(2));
- assertThat(matcher.group(1), equalTo("4"));
- assertThat(matcher.group(2), equalTo("5"));
- }
-
- @Test
- public void scoreMatchesInTheMiddleOfOtherText() {
- Matcher matcher = ProjectGradesImporter.scorePattern.matcher("You got 4 out of 5 points");
- assertThat(matcher.find(), equalTo(true));
- assertThat(matcher.groupCount(), equalTo(2));
- assertThat(matcher.group(1), equalTo("4"));
- assertThat(matcher.group(2), equalTo("5"));
- }
-
- @Test
- public void gradedProjectWithIntegerPoints() throws ProjectGradesImporter.ScoreNotFoundException {
- GradedProject project = new GradedProject();
- project.addLine("3 out of 5");
- project.addLine("iadguow");
-
- ProjectGradesImporter.ProjectScore score = ProjectGradesImporter.getScoreFrom(project.getReader());
- assertThat(score.getScore(), equalTo(3.0));
- assertThat(score.getTotalPoints(), equalTo(5.0));
- }
-
- private class GradedProject {
- private StringBuilder sb = new StringBuilder();
-
- public void addLine(String line) {
- sb.append(line);
- sb.append("\n");
- }
-
- public Reader getReader() {
- return new StringReader(sb.toString());
- }
- }
-
- @Test
- public void onlyFirstScoreIsReturned() throws ProjectGradesImporter.ScoreNotFoundException {
- GradedProject project = new GradedProject();
- project.addLine("3.4 out of 3.5");
- project.addLine("iadguow");
- project.addLine("3.3 out of 3.4");
-
- ProjectGradesImporter.ProjectScore score = ProjectGradesImporter.getScoreFrom(project.getReader());
- assertThat(score.getScore(), equalTo(3.4));
- assertThat(score.getTotalPoints(), equalTo(3.5));
- }
-
- @Test
- public void scoreIsRecordedInGradeBook() throws ProjectGradesImporter.ScoreNotFoundException {
+ public void scoreIsRecordedInGradeBook() throws TestedProjectSubmissionOutputParsingException, IOException {
String studentId = "student";
Assignment assignment = new Assignment("project", 6.0);
@@ -126,7 +32,9 @@ public void scoreIsRecordedInGradeBook() throws ProjectGradesImporter.ScoreNotFo
gradeBook.addAssignment(assignment);
String score = "5.8";
- GradedProject project = new GradedProject();
+ TestedProjectSubmissionOutput project = new TestedProjectSubmissionOutput();
+ project.addLine(" The Joy of Coding Project 2: edu.pdx.cs410J.student.Project2");
+ project.addLine(" Submitted on Wed Feb 4 05:07:17 PM PST 2026");
project.addLine(score + " out of 6.0");
project.addLine("");
project.addLine("asdfasd");
@@ -144,7 +52,7 @@ public void scoreIsRecordedInGradeBook() throws ProjectGradesImporter.ScoreNotFo
}
@Test
- public void throwIllegalStateExceptionWhenTotalPointsInReportDoesNotMatchGradeBook() throws ProjectGradesImporter.ScoreNotFoundException {
+ public void throwIllegalStateExceptionWhenTotalPointsInReportDoesNotMatchGradeBook() {
String studentId = "student";
Assignment assignment = new Assignment("project", 8.0);
@@ -152,7 +60,9 @@ public void throwIllegalStateExceptionWhenTotalPointsInReportDoesNotMatchGradeBo
gradeBook.addStudent(new Student(studentId));
gradeBook.addAssignment(assignment);
- GradedProject project = new GradedProject();
+ TestedProjectSubmissionOutput project = new TestedProjectSubmissionOutput();
+ project.addLine(" The Joy of Coding Project 2: edu.pdx.cs410J.student.Project2");
+ project.addLine(" Submitted on Wed Feb 4 05:07:17 PM PST 2026");
project.addLine("5.8 out of 6.0");
project.addLine("");
project.addLine("asdfasd");
@@ -164,14 +74,16 @@ public void throwIllegalStateExceptionWhenTotalPointsInReportDoesNotMatchGradeBo
}
@Test
- public void logWarningWhenStudentDoesNotExistInGradeBook() throws ProjectGradesImporter.ScoreNotFoundException {
+ public void logWarningWhenStudentDoesNotExistInGradeBook() throws TestedProjectSubmissionOutputParsingException, IOException {
String studentId = "student";
Assignment assignment = new Assignment("project", 6.0);
GradeBook gradeBook = new GradeBook("test");
gradeBook.addAssignment(assignment);
- GradedProject project = new GradedProject();
+ TestedProjectSubmissionOutput project = new TestedProjectSubmissionOutput();
+ project.addLine(" The Joy of Coding Project 2: edu.pdx.cs410J.student.Project2");
+ project.addLine(" Submitted on Wed Feb 4 05:07:17 PM PST 2026");
project.addLine("5.8 out of 6.0");
project.addLine("");
project.addLine("asdfasd");
diff --git a/grader/src/test/java/edu/pdx/cs/joy/grader/TestedProjectSubmissionOutputParserTest.java b/grader/src/test/java/edu/pdx/cs/joy/grader/TestedProjectSubmissionOutputParserTest.java
new file mode 100644
index 000000000..0951dd15e
--- /dev/null
+++ b/grader/src/test/java/edu/pdx/cs/joy/grader/TestedProjectSubmissionOutputParserTest.java
@@ -0,0 +1,256 @@
+package edu.pdx.cs.joy.grader;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.time.LocalDateTime;
+import java.util.regex.Matcher;
+
+import static edu.pdx.cs.joy.grader.TestedProjectSubmissionOutputParser.*;
+import static edu.pdx.cs.joy.grader.TestedProjectSubmissionOutputParser.parseTestedSubmissionOutput;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class TestedProjectSubmissionOutputParserTest {
+
+ @Test
+ public void gradedProjectWithNoGradeThrowsScoreNotFoundException() {
+ TestedProjectSubmissionOutput project = new TestedProjectSubmissionOutput();
+ project.addLine("asdfhjkl");
+ project.addLine("iadguow");
+
+ assertThrows(TestedProjectSubmissionOutputParsingException.class, () ->
+ ProjectGradesImporter.getScoreFrom(project.getReader())
+ );
+ }
+
+ @Test
+ public void scoreRegularExpressionWorkWithSimpleCase() {
+ Matcher matcher = scorePattern.matcher("3.0 out of 4.5");
+ assertThat(matcher.find(), equalTo(true));
+ assertThat(matcher.groupCount(), equalTo(2));
+ assertThat(matcher.group(1), equalTo("3.0"));
+ assertThat(matcher.group(2), equalTo("4.5"));
+ }
+
+ @Test
+ public void gradedProjectWithOutOfHasValidScore() throws TestedProjectSubmissionOutputParsingException, IOException {
+ TestedProjectSubmissionOutput project = new TestedProjectSubmissionOutput();
+ project.addLine(" The Joy of Coding Project 2: edu.pdx.cs410J.student.Project2");
+ project.addLine(" Submitted on Wed Feb 4 05:07:17 PM PST 2026");
+ project.addLine("3.4 out of 3.5");
+ project.addLine("iadguow");
+
+ ProjectScore score = parseTestedSubmissionOutput(project.getReader());
+ assertThat(score.getScore(), equalTo(3.4));
+ assertThat(score.getTotalPoints(), equalTo(3.5));
+ }
+
+ @Test
+ public void scoreMatchesRegardlessOfCase() {
+ Matcher matcher = scorePattern.matcher("4.0 OUT OF 5.0");
+ assertThat(matcher.find(), equalTo(true));
+ assertThat(matcher.groupCount(), equalTo(2));
+ assertThat(matcher.group(1), equalTo("4.0"));
+ assertThat(matcher.group(2), equalTo("5.0"));
+ }
+
+ @Test
+ public void scoreMatchesIntegerPoints() {
+ Matcher matcher = scorePattern.matcher("4 out of 5");
+ assertThat(matcher.find(), equalTo(true));
+ assertThat(matcher.groupCount(), equalTo(2));
+ assertThat(matcher.group(1), equalTo("4"));
+ assertThat(matcher.group(2), equalTo("5"));
+ }
+
+ @Test
+ public void scoreMatchesInTheMiddleOfOtherText() {
+ Matcher matcher = scorePattern.matcher("You got 4 out of 5 points");
+ assertThat(matcher.find(), equalTo(true));
+ assertThat(matcher.groupCount(), equalTo(2));
+ assertThat(matcher.group(1), equalTo("4"));
+ assertThat(matcher.group(2), equalTo("5"));
+ }
+
+ @Test
+ public void gradedProjectWithIntegerPoints() throws TestedProjectSubmissionOutputParsingException, IOException {
+ TestedProjectSubmissionOutput project = new TestedProjectSubmissionOutput();
+ project.addLine(" The Joy of Coding Project 2: edu.pdx.cs410J.student.Project2");
+ project.addLine(" Submitted on Wed Feb 4 05:07:17 PM PST 2026");
+ project.addLine("3 out of 5");
+ project.addLine("iadguow");
+
+ ProjectScore score = parseTestedSubmissionOutput(project.getReader());
+ assertThat(score.getScore(), equalTo(3.0));
+ assertThat(score.getTotalPoints(), equalTo(5.0));
+ }
+
+ static class TestedProjectSubmissionOutput {
+ private final StringBuilder sb = new StringBuilder();
+
+ public void addLine(String line) {
+ sb.append(line);
+ sb.append("\n");
+ }
+
+ public Reader getReader() {
+ return new StringReader(sb.toString());
+ }
+ }
+
+ @Test
+ public void onlyFirstScoreIsReturned() throws TestedProjectSubmissionOutputParsingException, IOException {
+ TestedProjectSubmissionOutput project = new TestedProjectSubmissionOutput();
+ project.addLine(" The Joy of Coding Project 2: edu.pdx.cs410J.student.Project2");
+ project.addLine(" Submitted on Wed Feb 4 05:07:17 PM PST 2026");
+ project.addLine("3.4 out of 3.5");
+ project.addLine("iadguow");
+ project.addLine("3.3 out of 3.4");
+
+ ProjectScore score = parseTestedSubmissionOutput(project.getReader());
+ assertThat(score.getScore(), equalTo(3.4));
+ assertThat(score.getTotalPoints(), equalTo(3.5));
+ }
+
+ @Test
+ public void projectNameIsIdentified() throws TestedProjectSubmissionOutputParsingException, IOException {
+ String projectName = "Project2";
+
+ TestedProjectSubmissionOutput project = new TestedProjectSubmissionOutput();
+ project.addLine("");
+ project.addLine(" The Joy of Coding Project 2: edu.pdx.cs410J.student." + projectName);
+ project.addLine(" Submitted by Student Name");
+ project.addLine(" Submitted on Wed Feb 4 05:07:17 PM PST 2026");
+ project.addLine(" Graded on Wed Feb 4 06:08:07 PM PST 2026");
+ project.addLine("");
+ project.addLine("5.5 out of 6.0");
+ project.addLine("");
+
+ ProjectScore score = parseTestedSubmissionOutput(project.getReader());
+ assertThat(score.getScore(), equalTo(5.5));
+ assertThat(score.getTotalPoints(), equalTo(6.0));
+ assertThat(score.getProjectName(), equalTo(projectName));
+ assertThat(score.isReviewed(), equalTo(true));
+ }
+
+ @Test
+ public void koansProjectNameIsIdentified() throws TestedProjectSubmissionOutputParsingException, IOException {
+ TestedProjectSubmissionOutput project = new TestedProjectSubmissionOutput();
+ project.addLine("");
+ project.addLine(" The Joy of Coding Koans");
+ project.addLine(" Submitted by Student Name");
+ project.addLine(" Submitted on Wed Feb 4 05:07:17 PM PST 2026");
+ project.addLine(" Graded on Wed Feb 4 06:08:07 PM PST 2026");
+ project.addLine("");
+ project.addLine("5.5 out of 6.0");
+ project.addLine("");
+
+ ProjectScore score = parseTestedSubmissionOutput(project.getReader());
+ assertThat(score.getScore(), equalTo(5.5));
+ assertThat(score.getTotalPoints(), equalTo(6.0));
+ assertThat(score.getProjectName(), equalTo("koans"));
+ assertThat(score.isReviewed(), equalTo(true));
+ }
+
+ @Test
+ public void scoreRegularExpressionWorksWithMissingScore() {
+ Matcher matcher = scorePattern.matcher(" out of 4.5");
+ assertThat(matcher.find(), equalTo(true));
+ assertThat(matcher.groupCount(), equalTo(2));
+ assertThat(matcher.group(1), equalTo(null));
+ assertThat(matcher.group(2), equalTo("4.5"));
+ }
+
+ @Test
+ public void ungradedProjectWithoutCommentIsNotReviewed() throws TestedProjectSubmissionOutputParsingException, IOException {
+ String projectName = "Project2";
+
+ TestedProjectSubmissionOutput project = new TestedProjectSubmissionOutput();
+ project.addLine("");
+ project.addLine(" The Joy of Coding Project 2: edu.pdx.cs410J.student." + projectName);
+ project.addLine(" Submitted by Student Name");
+ project.addLine(" Submitted on Wed Feb 4 05:07:17 PM PST 2026");
+ project.addLine(" Graded on Wed Feb 4 06:08:07 PM PST 2026");
+ project.addLine("");
+ project.addLine(" out of 6.0");
+ project.addLine("");
+
+ ProjectScore score = parseTestedSubmissionOutput(project.getReader());
+ assertThat(score.getScore(), equalTo(Double.NaN));
+ assertThat(score.getTotalPoints(), equalTo(6.0));
+ assertThat(score.getProjectName(), equalTo(projectName));
+ assertThat(score.isReviewed(), equalTo(false));
+ }
+
+ @Test
+ public void testOutputWithCommentsForStudentIsMarkedAsReviewed() throws TestedProjectSubmissionOutputParsingException, IOException {
+ String projectName = "Project2";
+
+ TestedProjectSubmissionOutput project = new TestedProjectSubmissionOutput();
+ project.addLine("");
+ project.addLine("Hey Student Name. I found some issues with your code. Please fix them and resubmit.");
+ project.addLine("");
+ project.addLine(" The Joy of Coding Project 2: edu.pdx.cs410J.student." + projectName);
+ project.addLine(" Submitted by Student Name");
+ project.addLine(" Submitted on Wed Feb 4 05:07:17 PM PST 2026");
+ project.addLine(" Graded on Wed Feb 4 06:08:07 PM PST 2026");
+ project.addLine("");
+ project.addLine(" out of 6.0");
+ project.addLine("");
+
+ ProjectScore score = parseTestedSubmissionOutput(project.getReader());
+ assertThat(score.getScore(), equalTo(Double.NaN));
+ assertThat(score.getTotalPoints(), equalTo(6.0));
+ assertThat(score.getProjectName(), equalTo(projectName));
+ assertThat(score.isReviewed(), equalTo(true));
+ }
+
+
+ @Test
+ void parseSubmissionTimeFromAndroidProjectTestOutputLine() {
+ LocalDateTime submissionTime = parseSubmissionTime(" Submitted on 2025-08-18T11:34:19.017953486");
+ LocalDateTime expectedTime = LocalDateTime.of(2025, 8, 18, 11, 34, 19, 17953486);
+ assertThat(submissionTime, equalTo(expectedTime));
+ }
+
+ @Test
+ void parseSubmissionTimeFromTestOutputLine() {
+ LocalDateTime submissionTime = parseSubmissionTime(" Submitted on Wed Aug 6 01:13:59 PM PDT 2025");
+ LocalDateTime expectedTime = LocalDateTime.of(2025, 8, 6, 13, 13, 59);
+ assertThat(submissionTime, equalTo(expectedTime));
+ }
+
+ @Test
+ void parseSubmissionTimeFromTestOutputLineWithTwoDigitDay() {
+ LocalDateTime submissionTime = parseSubmissionTime(" Submitted on Wed Jul 23 12:59:13 PM PDT 2025");
+ LocalDateTime expectedTime = LocalDateTime.of(2025, 7, 23, 12, 59, 13);
+ assertThat(submissionTime, equalTo(expectedTime));
+ }
+
+ @Test
+ public void submissionTimeIsIdentified() throws TestedProjectSubmissionOutputParsingException, IOException {
+ String projectName = "Project2";
+ LocalDateTime submissionTime = LocalDateTime.of(2026, 2, 4, 17, 7, 17);
+
+ TestedProjectSubmissionOutput project = new TestedProjectSubmissionOutput();
+ project.addLine("");
+ project.addLine(" The Joy of Coding Project 2: edu.pdx.cs410J.student." + projectName);
+ project.addLine(" Submitted by Student Name");
+ project.addLine(" Submitted on Wed Feb 4 05:07:17 PM PST 2026");
+ project.addLine(" Graded on Wed Feb 4 06:08:07 PM PST 2026");
+ project.addLine("");
+ project.addLine("5.5 out of 6.0");
+ project.addLine("");
+
+ ProjectScore score = parseTestedSubmissionOutput(project.getReader());
+ assertThat(score.getScore(), equalTo(5.5));
+ assertThat(score.getTotalPoints(), equalTo(6.0));
+ assertThat(score.getProjectName(), equalTo(projectName));
+ assertThat(score.isReviewed(), equalTo(true));
+ assertThat(score.getSubmissionTime(), equalTo(submissionTime));
+ }
+}
diff --git a/pom.xml b/pom.xml
index 381fd2ac5..415de3f02 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2,7 +2,7 @@
4.0.0
io.github.davidwhitlock.joy
- 1.2.4
+ 1.2.5-SNAPSHOT
joy
pom
Java Example Code
@@ -94,7 +94,7 @@
0.75
0
- 1.5.0
+ 1.5.1-SNAPSHOT
@@ -705,5 +705,9 @@
GitHub Pages
scm:git:git@github.com:JoyOfCodingPDX/JoyOfCoding.git
+
+ central
+ https://central.sonatype.com/repository/maven-snapshots/
+
diff --git a/projects-parent/archetypes-parent/airline-archetype/pom.xml b/projects-parent/archetypes-parent/airline-archetype/pom.xml
index 93e3048ef..3aa97eb02 100644
--- a/projects-parent/archetypes-parent/airline-archetype/pom.xml
+++ b/projects-parent/archetypes-parent/airline-archetype/pom.xml
@@ -5,10 +5,10 @@
archetypes-parent
io.github.davidwhitlock.joy
- 2.2.5
+ 2.2.6-SNAPSHOT
airline-archetype
- 2.2.5
+ 2.2.6-SNAPSHOT
maven-archetype
airline-archetype
diff --git a/projects-parent/archetypes-parent/airline-archetype/src/main/resources/archetype-resources/pom.xml b/projects-parent/archetypes-parent/airline-archetype/src/main/resources/archetype-resources/pom.xml
index 539ad345d..352ff393d 100644
--- a/projects-parent/archetypes-parent/airline-archetype/src/main/resources/archetype-resources/pom.xml
+++ b/projects-parent/archetypes-parent/airline-archetype/src/main/resources/archetype-resources/pom.xml
@@ -3,7 +3,7 @@
joy
io.github.davidwhitlock.joy
- 1.2.4
+ 1.2.5-SNAPSHOT
4.0.0
${groupId}
@@ -32,12 +32,12 @@
io.github.davidwhitlock.joy
projects
- 3.0.4
+ 3.0.5-SNAPSHOT
io.github.davidwhitlock.joy
projects
- 3.0.4
+ 3.0.5-SNAPSHOT
tests
test
diff --git a/projects-parent/archetypes-parent/airline-web-archetype/pom.xml b/projects-parent/archetypes-parent/airline-web-archetype/pom.xml
index b16530cbf..74b8dfaee 100644
--- a/projects-parent/archetypes-parent/airline-web-archetype/pom.xml
+++ b/projects-parent/archetypes-parent/airline-web-archetype/pom.xml
@@ -4,10 +4,10 @@
archetypes-parent
io.github.davidwhitlock.joy
- 2.2.5
+ 2.2.6-SNAPSHOT
airline-web-archetype
- 3.0.4
+ 3.0.5-SNAPSHOT
maven-archetype
airline-web-archetype
diff --git a/projects-parent/archetypes-parent/airline-web-archetype/src/main/resources/archetype-resources/pom.xml b/projects-parent/archetypes-parent/airline-web-archetype/src/main/resources/archetype-resources/pom.xml
index 7a39f8740..615ecfa64 100644
--- a/projects-parent/archetypes-parent/airline-web-archetype/src/main/resources/archetype-resources/pom.xml
+++ b/projects-parent/archetypes-parent/airline-web-archetype/src/main/resources/archetype-resources/pom.xml
@@ -3,7 +3,7 @@
joy
io.github.davidwhitlock.joy
- 1.2.4
+ 1.2.5-SNAPSHOT
4.0.0
${groupId}
@@ -21,17 +21,17 @@
io.github.davidwhitlock.joy
projects
- 3.0.4
+ 3.0.5-SNAPSHOT
io.github.davidwhitlock.joy
examples
- 1.4.0
+ 1.4.1-SNAPSHOT
io.github.davidwhitlock.joy
projects
- 3.0.4
+ 3.0.5-SNAPSHOT
tests
test
diff --git a/projects-parent/archetypes-parent/apptbook-archetype/pom.xml b/projects-parent/archetypes-parent/apptbook-archetype/pom.xml
index d92720d00..160595f58 100644
--- a/projects-parent/archetypes-parent/apptbook-archetype/pom.xml
+++ b/projects-parent/archetypes-parent/apptbook-archetype/pom.xml
@@ -4,10 +4,10 @@
archetypes-parent
io.github.davidwhitlock.joy
- 2.2.5
+ 2.2.6-SNAPSHOT
apptbook-archetype
- 2.2.5
+ 2.2.6-SNAPSHOT
maven-archetype
apptbook-archetype
diff --git a/projects-parent/archetypes-parent/apptbook-archetype/src/main/resources/archetype-resources/pom.xml b/projects-parent/archetypes-parent/apptbook-archetype/src/main/resources/archetype-resources/pom.xml
index fbc4dfbfb..a821103c4 100644
--- a/projects-parent/archetypes-parent/apptbook-archetype/src/main/resources/archetype-resources/pom.xml
+++ b/projects-parent/archetypes-parent/apptbook-archetype/src/main/resources/archetype-resources/pom.xml
@@ -3,7 +3,7 @@
joy
io.github.davidwhitlock.joy
- 1.2.4
+ 1.2.5-SNAPSHOT
4.0.0
${groupId}
@@ -32,12 +32,12 @@
io.github.davidwhitlock.joy
projects
- 3.0.4
+ 3.0.5-SNAPSHOT
io.github.davidwhitlock.joy
projects
- 3.0.4
+ 3.0.5-SNAPSHOT
tests
test
diff --git a/projects-parent/archetypes-parent/apptbook-web-archetype/pom.xml b/projects-parent/archetypes-parent/apptbook-web-archetype/pom.xml
index 98bf72215..093d4e0d5 100644
--- a/projects-parent/archetypes-parent/apptbook-web-archetype/pom.xml
+++ b/projects-parent/archetypes-parent/apptbook-web-archetype/pom.xml
@@ -4,10 +4,10 @@
archetypes-parent
io.github.davidwhitlock.joy
- 2.2.5
+ 2.2.6-SNAPSHOT
apptbook-web-archetype
- 3.0.4
+ 3.0.5-SNAPSHOT
maven-archetype
apptbook-web-archetype
diff --git a/projects-parent/archetypes-parent/apptbook-web-archetype/src/main/resources/archetype-resources/pom.xml b/projects-parent/archetypes-parent/apptbook-web-archetype/src/main/resources/archetype-resources/pom.xml
index 3d7398728..0c4904030 100644
--- a/projects-parent/archetypes-parent/apptbook-web-archetype/src/main/resources/archetype-resources/pom.xml
+++ b/projects-parent/archetypes-parent/apptbook-web-archetype/src/main/resources/archetype-resources/pom.xml
@@ -3,7 +3,7 @@
joy
io.github.davidwhitlock.joy
- 1.2.4
+ 1.2.5-SNAPSHOT
4.0.0
${groupId}
@@ -21,17 +21,17 @@
io.github.davidwhitlock.joy
projects
- 3.0.4
+ 3.0.5-SNAPSHOT
io.github.davidwhitlock.joy
examples
- 1.4.0
+ 1.4.1-SNAPSHOT
io.github.davidwhitlock.joy
projects
- 3.0.4
+ 3.0.5-SNAPSHOT
tests
test
diff --git a/projects-parent/archetypes-parent/java-koans-archetype/pom.xml b/projects-parent/archetypes-parent/java-koans-archetype/pom.xml
index 1faba518f..a54161c15 100644
--- a/projects-parent/archetypes-parent/java-koans-archetype/pom.xml
+++ b/projects-parent/archetypes-parent/java-koans-archetype/pom.xml
@@ -3,13 +3,13 @@
archetypes-parent
io.github.davidwhitlock.joy
- 2.2.5
+ 2.2.6-SNAPSHOT
4.0.0
java-koans-archetype
- 2.2.5
+ 2.2.6-SNAPSHOT
maven-archetype
java-koans-archetype
diff --git a/projects-parent/archetypes-parent/java-koans-archetype/src/main/resources/archetype-resources/pom.xml b/projects-parent/archetypes-parent/java-koans-archetype/src/main/resources/archetype-resources/pom.xml
index 8fcb467e3..0e98d913c 100644
--- a/projects-parent/archetypes-parent/java-koans-archetype/src/main/resources/archetype-resources/pom.xml
+++ b/projects-parent/archetypes-parent/java-koans-archetype/src/main/resources/archetype-resources/pom.xml
@@ -4,7 +4,7 @@
joy
io.github.davidwhitlock.joy
- 1.2.4
+ 1.2.5-SNAPSHOT
${artifactId}
${groupId}
@@ -50,7 +50,7 @@
io.github.davidwhitlock.joy.com.sandwich
koans-lib
- 1.2.3
+ 1.2.4-SNAPSHOT