Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
ef33ef8
switch to test coverage to line coverage
mewilker Nov 12, 2025
8f4c8dd
add util function that gets package name where unit tests are expected
mewilker Nov 19, 2025
adfe91a
score unit tests based on coverage rather than number failing
mewilker Nov 19, 2025
954d879
Currently changed the unitTestPackageForCoverage to return an empty s…
ZakkeryDaRebel Dec 3, 2025
d7291ac
Have the unitTests for Phase 5 be in a package called facade
ZakkeryDaRebel Jan 19, 2026
bde9a3f
Moved where we have the checking for unitTests are. Remove all unneed…
ZakkeryDaRebel Jan 19, 2026
fa85f67
Merge branch 'main' into 398-code-coverage-grading
ZakkeryDaRebel Jan 30, 2026
36b06dd
Merge Extra Credit Rubric Separation PR Commits
ZakkeryDaRebel Feb 2, 2026
f582e9b
Removed isAdmin requirement to see the line coverage results
ZakkeryDaRebel Feb 2, 2026
a6775dc
UnitTestGrader private variables for the code coverage requirement an…
ZakkeryDaRebel Feb 2, 2026
ce31fbc
UnitTestGrader changed the getNotes method to return a list of all th…
ZakkeryDaRebel Feb 2, 2026
a63050c
UnitTestGrader changed the notes if the user has no tests or has fail…
ZakkeryDaRebel Feb 2, 2026
7ef624d
update autograder rubric config table insert statement for coverage a…
mewilker May 1, 2026
3bc5eb5
add checks to make sure score is not NaN
mewilker May 1, 2026
c6c106b
add CoverageRequirement to allow coverage checking on a class or a pa…
mewilker May 12, 2026
757afc3
basic untested coverage verifier
mewilker May 19, 2026
b655006
fix file coverage verifier and implement in compile process
mewilker May 19, 2026
a879e25
remove irrelevant fixme comment
mewilker May 19, 2026
2c2faeb
readability fixes
mewilker May 19, 2026
323b534
add minimum coverage percent as a config value in database
mewilker May 20, 2026
89ea9fd
add extra coverage percent as a config value in database
mewilker May 20, 2026
7469043
update javadocs for new values in private config
mewilker May 20, 2026
6f61688
add frontend code to change and display coverage percent
mewilker May 20, 2026
7812bab
add code coverage to the penalty update requests
mewilker May 20, 2026
f6aede4
update javadocs
mewilker May 20, 2026
abe416e
add logger to unit test grader to indicate when dao could not be read…
mewilker May 21, 2026
8512bcb
add coverage type as a configurable option on backend
mewilker May 21, 2026
38ea840
add javadoc comments for coverage type
mewilker May 21, 2026
a20b496
fix enum verification getting and setting values
mewilker May 21, 2026
08f59eb
add a toggle for branch or line coverage in private config
mewilker May 21, 2026
0135bfd
Merge branch 'main' into 398-code-coverage-grading
mewilker May 22, 2026
9c70b9a
add coverage to ConfigurationDaoTest
mewilker May 22, 2026
3d9b0f2
update line endings from crlf to lf
mewilker May 22, 2026
481b0e8
format with prettier
mewilker May 22, 2026
3779d24
changed line endings
mewilker May 22, 2026
ad99d71
fix percent comparisons
mewilker May 22, 2026
b3fe587
Merge branch 'main' into 398-code-coverage-grading
mewilker Jun 25, 2026
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 @@ -10,7 +10,7 @@ The primary things that needs to be updated on an ongoing basis are:

## Statements
> [!NOTE]
> Updated as of **WINTER 2026**
> Updated as of **SUMMER 2026**

```mysql
INSERT INTO rubric_config (phase, type, category, criteria, points, rubric_id) VALUES
Expand All @@ -23,16 +23,16 @@ INSERT INTO rubric_config (phase, type, category, criteria, points, rubric_id) V
('Phase3', 'GIT_COMMITS', 'Git Commits', 'Necessary commit amount', 0, '90344_2520'),
('Phase3', 'PASSOFF_TESTS', 'Web API Works', 'All pass off test cases in StandardAPITests.java succeed', 125, '_5202'),
('Phase3', 'QUALITY', 'Code Quality', 'Chess Code Quality Rubric (see GitHub)', 30, '_3003'),
('Phase3', 'UNIT_TESTS', 'Unit Tests', 'All test cases pass\nEach public method on your Service classes has two test cases, one positive test and one negative test\nEvery test case includes an Assert statement of some type', 25, '90344_776'),
('Phase3', 'UNIT_TESTS', 'Unit Tests', 'All test cases pass\nCode coverage on service package must be at least 80% line coverage\nEvery test case includes an Assert statement of some type', 25, '90344_776'),
('Phase4', 'GIT_COMMITS', 'Git Commits', 'Necessary commit amount', 0, '90346_6245'),
('Phase4', 'PASSOFF_TESTS', 'Functionality', 'All pass off test cases succeed', 100, '_2614'),
('Phase4', 'QUALITY', 'Code Quality', 'Chess Code Quality Rubric (see GitHub)', 30, '90346_8398'),
('Phase4', 'UNIT_TESTS', 'Unit Tests', 'All test cases pass\nEach public method on DAO classes has two test cases, one positive test and one negative test\nEvery test case includes an Assert statement of some type', 25, '90346_5755'),
('Phase4', 'UNIT_TESTS', 'Unit Tests', 'All test cases pass\nCode coverage on dataaccess package must be at least 80% line coverage\nEvery test case includes an Assert statement of some type', 25, '90346_5755'),
('Phase5', 'GIT_COMMITS', 'Git Commits', 'Necessary commit amount', 0, '90347_8497'),
('Phase5', 'QUALITY', 'Code Quality', 'Chess Code Quality Rubric (see GitHub)', 30, '90347_9378'),
('Phase5', 'UNIT_TESTS', 'Unit Tests', 'All test cases pass\nEach public method on the Server Facade class has two test cases, one positive test and one negative test\nEvery test case includes an Assert statement of some type', 25, '90347_2215'),
('Phase5', 'UNIT_TESTS', 'Unit Tests', 'All test cases pass\nCode coverage on facade package must be at least 80% line coverage\nEvery test case includes an Assert statement of some type', 25, '90347_2215'),
('Phase6', 'GIT_COMMITS', 'Git Commits', 'Necessary commit amount', 0, '90348_9048'),
('Phase6', 'PASSOFF_TESTS', 'Automated Pass Off Test Cases', 'Each provided test case passed is worth a proportional number of points ((passed / total) * 50).', 50, '90348_899'),
('Phase6', 'PASSOFF_TESTS', 'Automated Pass Off Test Cases', 'All pass off test cases succeed', 50, '90348_899'),
('Phase6', 'QUALITY', 'Code Quality', 'Chess Code Quality Rubric (see GitHub)', 30, '90348_3792'),
('Quality', 'QUALITY', 'Code Quality', 'Chess Code Quality Rubric (see GitHub)', 30, NULL);
```
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ public CompileHelper(GradingContext gradingContext) {

private final Collection<StudentCodeVerifier> currentVerifiers =
List.of(new ProjectStructureVerifier(), new ModuleIndependenceVerifier(), new ModifiedTestFilesVerifier(),
new TestLocationVerifier(), new TestClassNameVerifier(), new ServerFacadeTestPortVerifier(), new ModifiedWebResourcesVerifier());
new TestLocationVerifier(), new TestClassNameVerifier(), new ServerFacadeTestPortVerifier(),
new ModifiedWebResourcesVerifier(), new CodeCoverageVerifier());


private final Collection<StudentCodeModifier> currentModifiers =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package edu.byu.cs.autograder.compile.verifers;

import edu.byu.cs.autograder.GradingContext;
import edu.byu.cs.autograder.GradingException;
import edu.byu.cs.autograder.compile.StudentCodeReader;
import edu.byu.cs.autograder.compile.StudentCodeVerifier;
import edu.byu.cs.model.CoverageRequirement;
import edu.byu.cs.model.Phase;
import edu.byu.cs.util.PhaseUtils;

import java.io.File;
import java.util.Set;
import java.util.TreeSet;

/**
* Verifies that the necessary packages and classes exists to collect code coverage on unit tests
*/
public class CodeCoverageVerifier implements StudentCodeVerifier {
private final Set<String> missingPackages = new TreeSet<>();
private final Set<String> missingFiles = new TreeSet<>();

@Override
public void verify(GradingContext context, StudentCodeReader reader) throws GradingException {
Phase currPhase = context.phase();
CoverageRequirement coverage = PhaseUtils.unitTestCoverageRequirements(currPhase);
if (coverage.type() == CoverageRequirement.CoverageType.PACKAGE){
File packageDirectory = new File(context.stageRepo(), coverage.name());
if (!packageDirectory.isDirectory()){
missingPackages.add(coverage.name());
}
}
else{

if (reader.filesMatching(buildFileRegex(coverage.name())).findAny().isEmpty()){
missingFiles.add(coverage.name());
}
}

String message = buildMessage();
if (!message.isBlank()){
context.observer().notifyWarning(message);
}
}

private String buildMessage(){
StringBuilder stringBuilder = new StringBuilder();
if (!missingPackages.isEmpty()){
stringBuilder.append("Missing package(s) for Code Coverage: ")
.append(String.join(", ", missingPackages))
.append(".\n");
}
if (!missingFiles.isEmpty()){
stringBuilder.append("Missing file(s) for Code Coverage: ")
.append(String.join(", ", missingFiles))
.append(".\n");
}
if (!stringBuilder.isEmpty()){
stringBuilder.append("Code coverage cannot be calculated without the proper project structure.\n")
.append("Please double check the phase's specification.");
}
return stringBuilder.toString();
}

private String buildFileRegex(String fileName){
return "^.*[\\/]"+ fileName + "\\.java$|^"+ fileName + "\\.java$";
}
}
44 changes: 34 additions & 10 deletions src/main/java/edu/byu/cs/autograder/test/CoverageAnalyzer.java
Original file line number Diff line number Diff line change
@@ -1,29 +1,47 @@
package edu.byu.cs.autograder.test;

import edu.byu.cs.autograder.GradingException;
import edu.byu.cs.dataAccess.DaoService;
import edu.byu.cs.dataAccess.DataAccessException;
import edu.byu.cs.dataAccess.daoInterface.ConfigurationDao;
import edu.byu.cs.model.ClassCoverageAnalysis;
import edu.byu.cs.model.CoverageAnalysis;
import edu.byu.cs.util.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.util.Collection;
import java.util.HashSet;

/**
* Parses the code coverage output stored in a JaCoCo CSV file into a
* {@link CoverageAnalysis} containing the appropriate coverage results
* {@link CoverageAnalysis} containing the appropriate coverage results.
* <br>
* Supports Branch and Line coverage. Default coverage is Line Coverage.
*/
public class CoverageAnalyzer {
/**
* The type of coverage to be tested for. This is the only value that should be
* updated if coverage requirements change.
*/
private static final String COVERAGE_TESTED = "BRANCH";
private static final String COVERAGE_MISSED_HEADER = COVERAGE_TESTED + "_MISSED";
private static final String COVERAGE_COVERED_HEADER = COVERAGE_TESTED + "_COVERED";
private final String coverageMissedHeader;
private final String coverageCoveredHeader;
private static final String PACKAGE_HEADER = "PACKAGE";
private static final String CLASS_HEADER = "CLASS";

private static final Logger LOGGER = LoggerFactory.getLogger(CoverageAnalyzer.class);

public CoverageAnalyzer(){
String coverageTested = "LINE";
try{
String config = DaoService.getConfigurationDao().getConfiguration(ConfigurationDao.Configuration.COVERAGE_TYPE, String.class);
if (config.equals("BRANCH")){ //ensure the value is valid
coverageTested = config;
}
} catch (DataAccessException e) {
LOGGER.error("Could not get coverage type, using line coverage", e);
}
coverageMissedHeader = coverageTested + "_MISSED";
coverageCoveredHeader = coverageTested + "_COVERED";
}

/**
* Parses the output of a JaCoCo CSV file, if it exists,
* and returns the coverage results as a {@link CoverageAnalysis}
Expand Down Expand Up @@ -52,8 +70,14 @@ public CoverageAnalysis parse(File jacocoCsvOutput) throws GradingException {
switch (s) {
case CLASS_HEADER -> classHeader = i;
case PACKAGE_HEADER -> packageHeader = i;
case COVERAGE_MISSED_HEADER -> coverageMissedHeader = i;
case COVERAGE_COVERED_HEADER -> coverageCoveredHeader = i;
default -> {
if (s.equals(this.coverageMissedHeader)){
coverageMissedHeader = i;
}
else if (s.equals(this.coverageCoveredHeader)) {
coverageCoveredHeader = i;
}
}
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/main/java/edu/byu/cs/autograder/test/TestGrader.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ public Rubric.Results runTests() throws GradingException, DataAccessException {
} else {
results = new TestHelper().runJUnitTests(new File(gradingContext.stageRepo(),
"/" + module + "/target/" + module + "-test-dependencies.jar"), stageTestsPath,
packagesToTest(), ignoredTests(), modulesToCheckCoverage());
packagesToTest(), ignoredTests(), modulesToCheckCoverage(),
PhaseUtils.unitTestCoverageRequirements(gradingContext.phase()));
}

if (results.root() == null) {
Expand Down
34 changes: 29 additions & 5 deletions src/main/java/edu/byu/cs/autograder/test/TestHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import edu.byu.cs.autograder.GradingException;
import edu.byu.cs.dataAccess.DaoService;
import edu.byu.cs.dataAccess.daoInterface.ConfigurationDao;
import edu.byu.cs.model.ClassCoverageAnalysis;
import edu.byu.cs.model.CoverageAnalysis;
import edu.byu.cs.model.CoverageRequirement;
import edu.byu.cs.model.Rubric;
import edu.byu.cs.model.TestNode;
import edu.byu.cs.model.TestOutput;
Expand All @@ -15,10 +17,8 @@
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.*;
import java.util.regex.Pattern;

/**
* A helper class for running common test operations
Expand Down Expand Up @@ -161,7 +161,7 @@ private static List<String> getCompileCommands(String stagePath, String chessJar
* @return A TestNode object containing the results of the tests.
*/
TestOutput runJUnitTests(File uberJar, File compiledTests, Set<String> packagesToTest,
Set<String> ignoredTests, Set<String> coverageModules) throws GradingException {
Set<String> ignoredTests, Set<String> coverageModules, CoverageRequirement coverageRequirement) throws GradingException {
// Process cannot handle relative paths or wildcards,
// so we need to only use absolute paths and find
// to get the files
Expand Down Expand Up @@ -199,6 +199,7 @@ TestOutput runJUnitTests(File uberJar, File compiledTests, Set<String> packagesT
File coverageOutput = new File(testOutputDirectory, "coverage.csv");

CoverageAnalysis coverage = coverageOutput.exists() ? new CoverageAnalyzer().parse(coverageOutput) : null;
coverage = removeUnmatchedPackages(coverageRequirement, coverage);
TestNode testAnalysis = testAnalyzer.parse(junitXmlOutput, ignoredTests);

return new TestOutput(testAnalysis, coverage, trimErrorOutput(error));
Expand All @@ -208,6 +209,29 @@ TestOutput runJUnitTests(File uberJar, File compiledTests, Set<String> packagesT
}
}

private CoverageAnalysis removeUnmatchedPackages(CoverageRequirement requirement, CoverageAnalysis coverage) {
if (coverage == null || coverage.classAnalyses() == null || requirement == null) {
return coverage;
}
Pattern pattern = Pattern.compile("^" + requirement.name());
if (pattern.pattern().isEmpty()){
return coverage;
}
Collection<ClassCoverageAnalysis> matchedList = new ArrayList<>();
for (ClassCoverageAnalysis classCoverageAnalysis : coverage.classAnalyses()) {
if (requirement.type() == CoverageRequirement.CoverageType.CLASS) {
if (pattern.matcher(classCoverageAnalysis.className()).find()) {
matchedList.add(classCoverageAnalysis);
}
} else {
if (pattern.matcher(classCoverageAnalysis.packageName()).find()) {
matchedList.add(classCoverageAnalysis);
}
}
}
return new CoverageAnalysis(matchedList);
}

private static List<String> getRunCommands(Set<String> packagesToTest, String uberJarPath) {
List<String> commands = new ArrayList<>();
commands.add("java");
Expand Down
Loading
Loading