Repository: serg-delft/andy
Issue: Score of 100/100 is possible even if not everything is completed

Grades >= 99.5 get rounded up to 100. 

This is an edge case: any grade between 99 and 100 should be rounded down to 99, otherwise students may get confused and think that they have completed an exercise even if they haven't actually passed everything.

For example:
```
Status: Done
Andy v0.27.1-85578d2 (2022-04-25T09:29:42+0000)

[...]

--- Assessment
Branch coverage: 20/21 (overall weight=0.10)
Mutation coverage: 27/27 (overall weight=0.30)
Code checks: 0/0 (overall weight=0.00)
Meta tests: 17/17 (overall weight=0.60)

Final grade: 100/100

    Super congrats!

[...]
```

Note the number of covered branches - only 20 out of 21 branches are covered, but due to the low weight of the branch coverage score, this branch is worth less than 0.5/100, so the final score becomes 99.5/100, which Andy incorrectly rounds up to 100/100.

Changed files:
src/main/java/nl/tudelft/cse1110/andy/grade/GradeCalculator.java, src/test/java/unit/grade/GradeCalculatorTest.java

diff --git a/src/main/java/nl/tudelft/cse1110/andy/grade/GradeCalculator.java b/src/main/java/nl/tudelft/cse1110/andy/grade/GradeCalculator.java
index d6ccdc3..6e71536 100644
--- a/src/main/java/nl/tudelft/cse1110/andy/grade/GradeCalculator.java
+++ b/src/main/java/nl/tudelft/cse1110/andy/grade/GradeCalculator.java
@@ -24,6 +24,11 @@ public class GradeCalculator {
         if(finalGrade < 0 || finalGrade > 100)
             throw new RuntimeException("Invalid grade calculation");
 
+        // Grades between 99.5 and 100 should be rounded down to 99 instead of up
+        if (finalGrade == 100 && hasIncompleteComponents(gradeValues, weights)) {
+            finalGrade = 99;
+        }
+
         return finalGrade;
     }
 
@@ -79,4 +84,16 @@ public class GradeCalculator {
         return (float)checksPassed / totalChecks;
     }
 
+    /**
+     * @param gradeValues the grade values
+     * @param weights     the weights
+     * @return whether any of the components with positive weight are incomplete
+     */
+    private boolean hasIncompleteComponents(GradeValues gradeValues, GradeWeight weights) {
+        return weights.getBranchCoverageWeight() > 0.0f && gradeValues.getCoveredBranches() < gradeValues.getTotalBranches() ||
+               weights.getMutationCoverageWeight() > 0.0f && gradeValues.getDetectedMutations() < gradeValues.getTotalMutations() ||
+               weights.getMetaTestsWeight() > 0.0f && gradeValues.getMetaTestsPassed() < gradeValues.getTotalMetaTests() ||
+               weights.getCodeChecksWeight() > 0.0f && gradeValues.getChecksPassed() < gradeValues.getTotalChecks();
+    }
+
 }
diff --git a/src/test/java/unit/grade/GradeCalculatorTest.java b/src/test/java/unit/grade/GradeCalculatorTest.java
index d834ba6..2d340ec 100644
--- a/src/test/java/unit/grade/GradeCalculatorTest.java
+++ b/src/test/java/unit/grade/GradeCalculatorTest.java
@@ -3,6 +3,7 @@ package unit.grade;
 import nl.tudelft.cse1110.andy.grade.GradeCalculator;
 import nl.tudelft.cse1110.andy.grade.GradeValues;
 import nl.tudelft.cse1110.andy.grade.GradeWeight;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
@@ -81,4 +82,52 @@ public class GradeCalculatorTest {
                 of(25, 25, 55, 55, 100, 100, 2, 5, 94) // 0.4*1 + 0.3*1 + 0.2*1 +  0.1*(2/5) = 0.94 --> 94
         );
     }
+
+    /*
+     * Test where the grade is between 99.5 and 100, should be rounded down to 99 and not
+     * rounded up to 100, as 100 should only be achievable if everything
+     * with a positive weight is fully completed.
+     */
+    @Test
+    void withGradeBetween99And100() {
+        GradeWeight weights = new GradeWeight(0.1f, 0.3f, 0.6f, 0.0f);
+
+        GradeValues grades = new GradeValues();
+        grades.setBranchGrade(20, 21);
+        grades.setMutationGrade(27, 27);
+        grades.setMetaGrade(17, 17);
+        grades.setCheckGrade(0, 0);
+
+        int finalGrade = new GradeCalculator().calculateFinalGrade(grades, weights);
+
+        assertThat(finalGrade).isEqualTo(99);
+    }
+
+    @ParameterizedTest
+    @MethodSource("zeroWeights")
+    void withZeroWeights(int coveredBranches, int totalBranches,
+                         int detectedMutations, int totalMutations,
+                         int metaTestsPassed, int totalMetaTests,
+                         int checksPassed, int totalChecks, int expectedGrade) {
+
+        GradeWeight weights = new GradeWeight(0.4f, 0.0f, 0.5f, 0.1f);
+
+        GradeValues grades = new GradeValues();
+        grades.setBranchGrade(coveredBranches, totalBranches);
+        grades.setMutationGrade(detectedMutations, totalMutations);
+        grades.setMetaGrade(metaTestsPassed, totalMetaTests);
+        grades.setCheckGrade(checksPassed, totalChecks);
+
+        int finalGrade = new GradeCalculator().calculateFinalGrade(grades, weights);
+
+        assertThat(finalGrade).isEqualTo(expectedGrade);
+    }
+
+    private static Stream<Arguments> zeroWeights() {
+        return Stream.of(
+                of(25, 25, 2, 55, 100, 100, 5, 5, 100), // 0.4*1 + 0*(2/55) + 0.5*1 +  0.1*1 = 1.0 --> 100
+                of(25, 25, 2, 55, 100, 100, 4, 5, 98), // 0.4*1 + 0*(2/55) + 0.5*1 +  0.1*(4/5) = 0.98 --> 98
+                of(25, 25, 55, 55, 100, 100, 4, 5, 98) // 0.4*1 + 0*1 + 0.5*1 +  0.1*(4/5) = 0.98 --> 98
+        );
+    }
 }