From ef33ef85b9eb8824b32c14b0668ecf8988d2e211 Mon Sep 17 00:00:00 2001 From: mewilker Date: Wed, 12 Nov 2025 00:10:57 +0000 Subject: [PATCH 01/34] switch to test coverage to line coverage --- src/main/java/edu/byu/cs/autograder/test/CoverageAnalyzer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/byu/cs/autograder/test/CoverageAnalyzer.java b/src/main/java/edu/byu/cs/autograder/test/CoverageAnalyzer.java index 1d8ee404c..5bd5a4052 100644 --- a/src/main/java/edu/byu/cs/autograder/test/CoverageAnalyzer.java +++ b/src/main/java/edu/byu/cs/autograder/test/CoverageAnalyzer.java @@ -18,7 +18,7 @@ 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_TESTED = "LINE"; private static final String COVERAGE_MISSED_HEADER = COVERAGE_TESTED + "_MISSED"; private static final String COVERAGE_COVERED_HEADER = COVERAGE_TESTED + "_COVERED"; private static final String PACKAGE_HEADER = "PACKAGE"; From 8f4c8dd393e30a5b7b9d0eb13a5db1ead0dab7f5 Mon Sep 17 00:00:00 2001 From: mewilker Date: Wed, 19 Nov 2025 01:37:06 +0000 Subject: [PATCH 02/34] add util function that gets package name where unit tests are expected --- src/main/java/edu/byu/cs/util/PhaseUtils.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/edu/byu/cs/util/PhaseUtils.java b/src/main/java/edu/byu/cs/util/PhaseUtils.java index 34c9422a5..b48e01185 100644 --- a/src/main/java/edu/byu/cs/util/PhaseUtils.java +++ b/src/main/java/edu/byu/cs/util/PhaseUtils.java @@ -198,6 +198,15 @@ public static int minUnitTests(Phase phase) throws GradingException { }; } + public static String unitTestPackageForCoverage(Phase phase) throws GradingException { + return switch (phase){ + case Phase3 -> "service"; + case Phase4 -> "dataaccess"; //TODO: may need to have a subpackage + case Phase5 -> "client"; //FIXME: definately needs a subpackage + default -> throw new GradingException("No unit tests for this phaase"); + }; + } + /** * Gets the modules needed to check for code coverage while testing student-written unit tests * From adfe91a3b3ba325dc080c36ab1689fb80ac1611d Mon Sep 17 00:00:00 2001 From: mewilker Date: Wed, 19 Nov 2025 01:37:35 +0000 Subject: [PATCH 03/34] score unit tests based on coverage rather than number failing --- .../cs/autograder/test/UnitTestGrader.java | 72 ++++++++++++++----- 1 file changed, 55 insertions(+), 17 deletions(-) diff --git a/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java b/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java index 37d8c2ee5..9a2b594f6 100644 --- a/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java +++ b/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java @@ -1,16 +1,19 @@ package edu.byu.cs.autograder.test; +import java.io.File; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Pattern; + import edu.byu.cs.autograder.GradingContext; import edu.byu.cs.autograder.GradingException; +import edu.byu.cs.model.ClassCoverageAnalysis; +import edu.byu.cs.model.CoverageAnalysis; import edu.byu.cs.model.Rubric; -import edu.byu.cs.model.TestOutput; import edu.byu.cs.model.TestNode; +import edu.byu.cs.model.TestOutput; import edu.byu.cs.util.PhaseUtils; -import java.io.File; -import java.util.HashSet; -import java.util.Set; - /** * Runs and scores the unit tests for the phase a submission is graded for */ @@ -51,25 +54,60 @@ protected float getScore(TestOutput testOutput) throws GradingException { if (totalTests == 0) return 0; - int minTests = PhaseUtils.minUnitTests(gradingContext.phase()); + if (testResults.getNumTestsFailed() > 0) { + return 0; + } - if (totalTests < minTests) return (float) testResults.getNumTestsPassed() / minTests; + float coveragePercent = getCoveragePercent(testOutput.coverage()); + float targetPercent = 0.8F; // how much we want covered, change me if too low or high - return testResults.getNumTestsPassed() / totalTests; + if (coveragePercent > targetPercent){ + return 1; + } + + return coveragePercent / targetPercent; } @Override protected String getNotes(TestOutput testOutput) throws GradingException { TestNode testResults = testOutput.root(); - Integer totalTestsRun = testResults.getNumTestsFailed() + testResults.getNumTestsPassed(); - if (testResults.getNumTestsPassed() + testResults.getNumTestsFailed() < PhaseUtils.minUnitTests(gradingContext.phase())) - return "Not enough tests: each " + PhaseUtils.unitTestCodeUnderTest(gradingContext.phase()) + - " method should have a positive and negative test"; - return switch (testResults.getNumTestsFailed()) { - case 0 -> testResults.getNumTestsPassed() + "/" + totalTestsRun + " tests passed"; - case 1 -> "1/" + totalTestsRun + " test failed"; - default -> testResults.getNumTestsFailed() + "/" + totalTestsRun + " tests failed"; - }; + float coverage = getCoveragePercent(testOutput.coverage()); + StringBuilder notes = new StringBuilder("Coverage: " + coverage*100); + notes.append("\n"); + var pattern = Pattern.compile("^" + PhaseUtils.unitTestPackageForCoverage(gradingContext.phase())); + for (ClassCoverageAnalysis i : testOutput.coverage().classAnalyses()){ + var matcher = pattern.matcher(i.packageName()); + //TODO: make sure the class is in the covered package, probably include a phase util for this + if (matcher.find()){ + var total = (i.covered() + i.missed()); + notes.append(i.packageName()) + .append(".") + .append(i.className()) + .append(":") + .append(i.covered()) + .append("/") + .append(total) + .append("\n"); + } + } + return notes.toString(); + + } + + private float getCoveragePercent(CoverageAnalysis coverage) throws GradingException{ + float covered = 0; + float total = 0; + var pattern = Pattern.compile("^" + PhaseUtils.unitTestPackageForCoverage(gradingContext.phase())); + for (ClassCoverageAnalysis i : coverage.classAnalyses()){ + var matcher = pattern.matcher(i.packageName()); + //TODO: make sure the class is in the covered package, probably include a phase util for this + if (matcher.find()){ + covered += i.covered(); + total += (i.covered() + i.missed()); + } + } + + return covered / total; } @Override From 954d879dbb389badca3b89b9f9e04ea05c3c1ac4 Mon Sep 17 00:00:00 2001 From: Tristan Weech Date: Wed, 3 Dec 2025 11:41:08 -0700 Subject: [PATCH 04/34] Currently changed the unitTestPackageForCoverage to return an empty string so that nothing is removed from the coverage results if it is not Phase 3-4. --- src/main/java/edu/byu/cs/util/PhaseUtils.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/byu/cs/util/PhaseUtils.java b/src/main/java/edu/byu/cs/util/PhaseUtils.java index b48e01185..48293950f 100644 --- a/src/main/java/edu/byu/cs/util/PhaseUtils.java +++ b/src/main/java/edu/byu/cs/util/PhaseUtils.java @@ -202,8 +202,10 @@ public static String unitTestPackageForCoverage(Phase phase) throws GradingExcep return switch (phase){ case Phase3 -> "service"; case Phase4 -> "dataaccess"; //TODO: may need to have a subpackage - case Phase5 -> "client"; //FIXME: definately needs a subpackage - default -> throw new GradingException("No unit tests for this phaase"); + case Phase5 -> ""; //FIXME: definately needs a subpackage + // Originally was "client" which broke the code as there is no client package inside of + // the client module. So changed it to "" to get everything from the client module + default -> ""; }; } From d7291ac4ae2017996c10d5a326dde01163e8e76a Mon Sep 17 00:00:00 2001 From: Tristan Weech Date: Mon, 19 Jan 2026 11:19:48 -0700 Subject: [PATCH 05/34] Have the unitTests for Phase 5 be in a package called facade --- src/main/java/edu/byu/cs/util/PhaseUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/byu/cs/util/PhaseUtils.java b/src/main/java/edu/byu/cs/util/PhaseUtils.java index 48293950f..580f6c113 100644 --- a/src/main/java/edu/byu/cs/util/PhaseUtils.java +++ b/src/main/java/edu/byu/cs/util/PhaseUtils.java @@ -202,7 +202,7 @@ public static String unitTestPackageForCoverage(Phase phase) throws GradingExcep return switch (phase){ case Phase3 -> "service"; case Phase4 -> "dataaccess"; //TODO: may need to have a subpackage - case Phase5 -> ""; //FIXME: definately needs a subpackage + case Phase5 -> "facade"; //FIXME: definately needs a subpackage // Originally was "client" which broke the code as there is no client package inside of // the client module. So changed it to "" to get everything from the client module default -> ""; From bde9a3ff13afbaf66330ca097950c7235e9c01fe Mon Sep 17 00:00:00 2001 From: Tristan Weech Date: Mon, 19 Jan 2026 11:21:01 -0700 Subject: [PATCH 06/34] Moved where we have the checking for unitTests are. Remove all unneeded line coverage checking if not for unitTests. --- .../byu/cs/autograder/test/TestGrader.java | 3 +- .../byu/cs/autograder/test/TestHelper.java | 24 ++++++++--- .../cs/autograder/test/UnitTestGrader.java | 42 +++++++------------ 3 files changed, 36 insertions(+), 33 deletions(-) diff --git a/src/main/java/edu/byu/cs/autograder/test/TestGrader.java b/src/main/java/edu/byu/cs/autograder/test/TestGrader.java index 5224afbf9..c19fbb5ee 100644 --- a/src/main/java/edu/byu/cs/autograder/test/TestGrader.java +++ b/src/main/java/edu/byu/cs/autograder/test/TestGrader.java @@ -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(), extraCreditTests(), modulesToCheckCoverage()); + packagesToTest(), extraCreditTests(), modulesToCheckCoverage(), + PhaseUtils.unitTestPackageForCoverage(gradingContext.phase())); } if (results.root() == null) { diff --git a/src/main/java/edu/byu/cs/autograder/test/TestHelper.java b/src/main/java/edu/byu/cs/autograder/test/TestHelper.java index b08c88c64..f2ea0b4ea 100644 --- a/src/main/java/edu/byu/cs/autograder/test/TestHelper.java +++ b/src/main/java/edu/byu/cs/autograder/test/TestHelper.java @@ -3,10 +3,12 @@ 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.Rubric; import edu.byu.cs.model.TestOutput; import edu.byu.cs.util.FileUtils; +import edu.byu.cs.util.PhaseUtils; import edu.byu.cs.util.ProcessUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,10 +16,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 @@ -160,7 +160,7 @@ private static List getCompileCommands(String stagePath, String chessJar * @return A TestNode object containing the results of the tests. */ TestOutput runJUnitTests(File uberJar, File compiledTests, Set packagesToTest, - Set extraCreditTests, Set coverageModules) throws GradingException { + Set extraCreditTests, Set coverageModules, String packageForCoverage) throws GradingException { // Process cannot handle relative paths or wildcards, // so we need to only use absolute paths and find // to get the files @@ -198,6 +198,7 @@ TestOutput runJUnitTests(File uberJar, File compiledTests, Set packagesT File coverageOutput = new File(testOutputDirectory, "coverage.csv"); CoverageAnalysis coverage = coverageOutput.exists() ? new CoverageAnalyzer().parse(coverageOutput) : null; + coverage = removeUnmatchedPackages(Pattern.compile("^" + packageForCoverage), coverage); TestAnalyzer.TestAnalysis testAnalysis = testAnalyzer.parse(junitXmlOutput, extraCreditTests); return new TestOutput(testAnalysis.root(), testAnalysis.extraCredit(), coverage, trimErrorOutput(error)); @@ -207,6 +208,19 @@ TestOutput runJUnitTests(File uberJar, File compiledTests, Set packagesT } } + private CoverageAnalysis removeUnmatchedPackages(Pattern pattern, CoverageAnalysis coverage) { + if (coverage == null || coverage.classAnalyses() == null || pattern.pattern().isEmpty()) { + return coverage; + } + Collection matchedList = new ArrayList<>(); + for (ClassCoverageAnalysis classCoverageAnalysis : coverage.classAnalyses()) { + if (pattern.matcher(classCoverageAnalysis.packageName()).find()) { + matchedList.add(classCoverageAnalysis); + } + } + return new CoverageAnalysis(matchedList); + } + private static List getRunCommands(Set packagesToTest, String uberJarPath) { List commands = new ArrayList<>(); commands.add("java"); diff --git a/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java b/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java index 9a2b594f6..66bf82d34 100644 --- a/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java +++ b/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java @@ -3,7 +3,6 @@ import java.io.File; import java.util.HashSet; import java.util.Set; -import java.util.regex.Pattern; import edu.byu.cs.autograder.GradingContext; import edu.byu.cs.autograder.GradingException; @@ -69,44 +68,33 @@ protected float getScore(TestOutput testOutput) throws GradingException { } @Override - protected String getNotes(TestOutput testOutput) throws GradingException { + protected String getNotes(TestOutput testOutput) { TestNode testResults = testOutput.root(); float coverage = getCoveragePercent(testOutput.coverage()); - StringBuilder notes = new StringBuilder("Coverage: " + coverage*100); + StringBuilder notes = new StringBuilder("Coverage: " + coverage*100 + "%"); notes.append("\n"); - var pattern = Pattern.compile("^" + PhaseUtils.unitTestPackageForCoverage(gradingContext.phase())); + //All of this is notes, but it is in the details section, so do we even want this? for (ClassCoverageAnalysis i : testOutput.coverage().classAnalyses()){ - var matcher = pattern.matcher(i.packageName()); - //TODO: make sure the class is in the covered package, probably include a phase util for this - if (matcher.find()){ - var total = (i.covered() + i.missed()); - notes.append(i.packageName()) - .append(".") - .append(i.className()) - .append(":") - .append(i.covered()) - .append("/") - .append(total) - .append("\n"); - } + var total = (i.covered() + i.missed()); + notes.append(i.packageName()) + .append(".") + .append(i.className()) + .append(":") + .append(i.covered()) + .append("/") + .append(total) + .append("\n"); } return notes.toString(); - } - private float getCoveragePercent(CoverageAnalysis coverage) throws GradingException{ + private float getCoveragePercent(CoverageAnalysis coverage) { float covered = 0; float total = 0; - var pattern = Pattern.compile("^" + PhaseUtils.unitTestPackageForCoverage(gradingContext.phase())); for (ClassCoverageAnalysis i : coverage.classAnalyses()){ - var matcher = pattern.matcher(i.packageName()); - //TODO: make sure the class is in the covered package, probably include a phase util for this - if (matcher.find()){ - covered += i.covered(); - total += (i.covered() + i.missed()); - } + covered += i.covered(); + total += (i.covered() + i.missed()); } - return covered / total; } From 36b06dd40976daad39256d39e5697a667e086ec2 Mon Sep 17 00:00:00 2001 From: Tristan Weech Date: Mon, 2 Feb 2026 14:07:51 -0700 Subject: [PATCH 07/34] Merge Extra Credit Rubric Separation PR Commits --- src/main/java/edu/byu/cs/autograder/test/TestGrader.java | 7 ++----- src/main/java/edu/byu/cs/autograder/test/TestHelper.java | 5 +---- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/main/java/edu/byu/cs/autograder/test/TestGrader.java b/src/main/java/edu/byu/cs/autograder/test/TestGrader.java index c2036a9e5..6feb5f846 100644 --- a/src/main/java/edu/byu/cs/autograder/test/TestGrader.java +++ b/src/main/java/edu/byu/cs/autograder/test/TestGrader.java @@ -71,13 +71,12 @@ 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(), extraCreditTests(), modulesToCheckCoverage(), + packagesToTest(), ignoredTests(), modulesToCheckCoverage(), PhaseUtils.unitTestPackageForCoverage(gradingContext.phase())); } if (results.root() == null) { - results = new TestOutput(new TestNode(), null, new CoverageAnalysis(new HashSet<>()), results.error()); + results = new TestOutput(new TestNode(), new CoverageAnalysis(new HashSet<>()), results.error()); TestNode.countTests(results.root()); LOGGER.error("{} tests failed to run for {} in phase {}", name(), gradingContext.netId(), PhaseUtils.getPhaseAsString(gradingContext.phase())); @@ -105,8 +104,6 @@ private void compileTests() throws GradingException { protected abstract Set packagesToTest() throws GradingException; - protected abstract Set extraCreditTests() throws GradingException; - protected abstract String testName(); protected abstract float getScore(TestOutput testResults) throws GradingException; diff --git a/src/main/java/edu/byu/cs/autograder/test/TestHelper.java b/src/main/java/edu/byu/cs/autograder/test/TestHelper.java index baaeada7a..c13aea184 100644 --- a/src/main/java/edu/byu/cs/autograder/test/TestHelper.java +++ b/src/main/java/edu/byu/cs/autograder/test/TestHelper.java @@ -9,7 +9,6 @@ import edu.byu.cs.model.TestNode; import edu.byu.cs.model.TestOutput; import edu.byu.cs.util.FileUtils; -import edu.byu.cs.util.PhaseUtils; import edu.byu.cs.util.ProcessUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -161,8 +160,7 @@ private static List getCompileCommands(String stagePath, String chessJar * @return A TestNode object containing the results of the tests. */ TestOutput runJUnitTests(File uberJar, File compiledTests, Set packagesToTest, - Set ignoredTests, Set coverageModules) throws GradingException { - Set extraCreditTests, Set coverageModules, String packageForCoverage) throws GradingException { + Set ignoredTests, Set coverageModules, String packageForCoverage) throws GradingException { // Process cannot handle relative paths or wildcards, // so we need to only use absolute paths and find // to get the files @@ -201,7 +199,6 @@ TestOutput runJUnitTests(File uberJar, File compiledTests, Set packagesT CoverageAnalysis coverage = coverageOutput.exists() ? new CoverageAnalyzer().parse(coverageOutput) : null; coverage = removeUnmatchedPackages(Pattern.compile("^" + packageForCoverage), coverage); - TestAnalyzer.TestAnalysis testAnalysis = testAnalyzer.parse(junitXmlOutput, extraCreditTests); TestNode testAnalysis = testAnalyzer.parse(junitXmlOutput, ignoredTests); return new TestOutput(testAnalysis, coverage, trimErrorOutput(error)); From f582e9b7383e5627169cae1a8414a0da231e920e Mon Sep 17 00:00:00 2001 From: Tristan Weech Date: Mon, 2 Feb 2026 14:09:05 -0700 Subject: [PATCH 08/34] Removed isAdmin requirement to see the line coverage results --- .../frontend/src/views/StudentView/RubricItemResultsView.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/resources/frontend/src/views/StudentView/RubricItemResultsView.vue b/src/main/resources/frontend/src/views/StudentView/RubricItemResultsView.vue index 99bcdc2d6..f662edc79 100644 --- a/src/main/resources/frontend/src/views/StudentView/RubricItemResultsView.vue +++ b/src/main/resources/frontend/src/views/StudentView/RubricItemResultsView.vue @@ -31,7 +31,6 @@ const areErrorDetailsOpen = ref(false); targetPercent; + notes.append((isGoodCoverage) ? "✓" : "✗").append(" ") + .append(i.packageName()) .append(".") .append(i.className()) - .append(":") - .append(i.covered()) - .append("/") - .append(total) .append("\n"); } + notes.append("See Details for line coverage count") + .append("\n"); return notes.toString(); } From a63050c3fa6bf1f7f7af1084acd47adcca8e7738 Mon Sep 17 00:00:00 2001 From: Tristan Weech Date: Mon, 2 Feb 2026 14:13:56 -0700 Subject: [PATCH 11/34] UnitTestGrader changed the notes if the user has no tests or has failed any tests --- .../byu/cs/autograder/test/UnitTestGrader.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java b/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java index 2ef3e8edf..adbfdf4ac 100644 --- a/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java +++ b/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java @@ -70,6 +70,23 @@ protected float getScore(TestOutput testOutput) throws GradingException { @Override protected String getNotes(TestOutput testOutput) { TestNode testResults = testOutput.root(); + StringBuilder notes = new StringBuilder(); + + float totalTests = testResults.getNumTestsFailed() + testResults.getNumTestsPassed(); + if (totalTests == 0) { + notes.append("Did not find any unit tests. See Rubric for unit test requirement details") + .append("\n"); + return notes.toString(); + } + + if (testResults.getNumTestsFailed() > 0) { + notes.append(testResults.getNumTestsFailed() + " unit tests failed") + .append("\n") + .append("See Details for failed test") + .append("\n"); + return notes.toString(); + } + float coverage = getCoveragePercent(testOutput.coverage()); notes.append("Coverage: " + coverage*100 + "%") .append("\n"); From 7ef624dbf62594d04633f8c797f0e95d7409a004 Mon Sep 17 00:00:00 2001 From: mewilker Date: Fri, 1 May 2026 17:21:36 +0000 Subject: [PATCH 12/34] update autograder rubric config table insert statement for coverage and remove phase 6 partial credit --- .../db-insert-statements/insert-rubric-database.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/getting-started/db-insert-statements/insert-rubric-database.md b/docs/getting-started/db-insert-statements/insert-rubric-database.md index 79adee945..02ff5b477 100644 --- a/docs/getting-started/db-insert-statements/insert-rubric-database.md +++ b/docs/getting-started/db-insert-statements/insert-rubric-database.md @@ -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 @@ -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); ``` From 3bc5eb55fd34bf2d161ada740cb857f837e2ff38 Mon Sep 17 00:00:00 2001 From: mewilker Date: Fri, 1 May 2026 18:29:32 +0000 Subject: [PATCH 13/34] add checks to make sure score is not NaN --- .../edu/byu/cs/autograder/test/UnitTestGrader.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java b/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java index adbfdf4ac..edf947da3 100644 --- a/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java +++ b/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java @@ -56,6 +56,9 @@ protected float getScore(TestOutput testOutput) throws GradingException { } float coveragePercent = getCoveragePercent(testOutput.coverage()); + if (Float.isNaN(coveragePercent)){ + return 0; + } if (coveragePercent > extraCreditPercent) { return 1.05F; @@ -88,8 +91,15 @@ protected String getNotes(TestOutput testOutput) { } float coverage = getCoveragePercent(testOutput.coverage()); - notes.append("Coverage: " + coverage*100 + "%") + if (Float.isNaN(coverage)){ + notes.append("Could not calculate Coverage Percent. See Rubric for unit test requirement details") .append("\n"); + return notes.toString(); + } + + notes.append("Coverage: " + coverage*100 + "%") + .append("\n"); + for (ClassCoverageAnalysis i : testOutput.coverage().classAnalyses()){ var total = (i.covered() + i.missed()); From c6c106b8e8975b686d9c19b3a485cad4c1562d63 Mon Sep 17 00:00:00 2001 From: mewilker Date: Tue, 12 May 2026 01:13:21 +0000 Subject: [PATCH 14/34] add CoverageRequirement to allow coverage checking on a class or a package --- .../byu/cs/autograder/test/TestGrader.java | 2 +- .../byu/cs/autograder/test/TestHelper.java | 23 ++++++++++++++----- .../cs/autograder/test/UnitTestGrader.java | 1 + .../edu/byu/cs/model/CoverageRequirement.java | 8 +++++++ src/main/java/edu/byu/cs/util/PhaseUtils.java | 11 +++++---- 5 files changed, 33 insertions(+), 12 deletions(-) create mode 100644 src/main/java/edu/byu/cs/model/CoverageRequirement.java diff --git a/src/main/java/edu/byu/cs/autograder/test/TestGrader.java b/src/main/java/edu/byu/cs/autograder/test/TestGrader.java index 6feb5f846..a30aa7d79 100644 --- a/src/main/java/edu/byu/cs/autograder/test/TestGrader.java +++ b/src/main/java/edu/byu/cs/autograder/test/TestGrader.java @@ -72,7 +72,7 @@ public Rubric.Results runTests() throws GradingException, DataAccessException { results = new TestHelper().runJUnitTests(new File(gradingContext.stageRepo(), "/" + module + "/target/" + module + "-test-dependencies.jar"), stageTestsPath, packagesToTest(), ignoredTests(), modulesToCheckCoverage(), - PhaseUtils.unitTestPackageForCoverage(gradingContext.phase())); + PhaseUtils.unitTestCoverageRequirements(gradingContext.phase())); } if (results.root() == null) { diff --git a/src/main/java/edu/byu/cs/autograder/test/TestHelper.java b/src/main/java/edu/byu/cs/autograder/test/TestHelper.java index c13aea184..0cda6211f 100644 --- a/src/main/java/edu/byu/cs/autograder/test/TestHelper.java +++ b/src/main/java/edu/byu/cs/autograder/test/TestHelper.java @@ -5,6 +5,7 @@ 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; @@ -160,7 +161,7 @@ private static List getCompileCommands(String stagePath, String chessJar * @return A TestNode object containing the results of the tests. */ TestOutput runJUnitTests(File uberJar, File compiledTests, Set packagesToTest, - Set ignoredTests, Set coverageModules, String packageForCoverage) throws GradingException { + Set ignoredTests, Set 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 @@ -198,7 +199,7 @@ TestOutput runJUnitTests(File uberJar, File compiledTests, Set packagesT File coverageOutput = new File(testOutputDirectory, "coverage.csv"); CoverageAnalysis coverage = coverageOutput.exists() ? new CoverageAnalyzer().parse(coverageOutput) : null; - coverage = removeUnmatchedPackages(Pattern.compile("^" + packageForCoverage), coverage); + coverage = removeUnmatchedPackages(coverageRequirement, coverage); TestNode testAnalysis = testAnalyzer.parse(junitXmlOutput, ignoredTests); return new TestOutput(testAnalysis, coverage, trimErrorOutput(error)); @@ -208,14 +209,24 @@ TestOutput runJUnitTests(File uberJar, File compiledTests, Set packagesT } } - private CoverageAnalysis removeUnmatchedPackages(Pattern pattern, CoverageAnalysis coverage) { - if (coverage == null || coverage.classAnalyses() == null || pattern.pattern().isEmpty()) { + 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 matchedList = new ArrayList<>(); for (ClassCoverageAnalysis classCoverageAnalysis : coverage.classAnalyses()) { - if (pattern.matcher(classCoverageAnalysis.packageName()).find()) { - matchedList.add(classCoverageAnalysis); + 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); diff --git a/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java b/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java index edf947da3..aaab5e43e 100644 --- a/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java +++ b/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java @@ -19,6 +19,7 @@ public class UnitTestGrader extends TestGrader { private final float targetPercent = 0.8F; // how much we want covered, change me if too low or high private final float extraCreditPercent = 0.9F; // double check with professors + //TODO: consider making me a config point that is set up on the autograder admin config page like penalty percent public UnitTestGrader(GradingContext gradingContext) { super(gradingContext); diff --git a/src/main/java/edu/byu/cs/model/CoverageRequirement.java b/src/main/java/edu/byu/cs/model/CoverageRequirement.java new file mode 100644 index 000000000..7b334fa94 --- /dev/null +++ b/src/main/java/edu/byu/cs/model/CoverageRequirement.java @@ -0,0 +1,8 @@ +package edu.byu.cs.model; + +public record CoverageRequirement(CoverageType type, String name) { + public enum CoverageType{ + PACKAGE, + CLASS + } +} diff --git a/src/main/java/edu/byu/cs/util/PhaseUtils.java b/src/main/java/edu/byu/cs/util/PhaseUtils.java index f18b41684..cc91e4bf7 100644 --- a/src/main/java/edu/byu/cs/util/PhaseUtils.java +++ b/src/main/java/edu/byu/cs/util/PhaseUtils.java @@ -5,6 +5,7 @@ import edu.byu.cs.dataAccess.daoInterface.ConfigurationDao; import edu.byu.cs.dataAccess.DaoService; import edu.byu.cs.dataAccess.DataAccessException; +import edu.byu.cs.model.CoverageRequirement; import edu.byu.cs.model.Phase; import edu.byu.cs.model.Rubric; import edu.byu.cs.model.RubricConfig; @@ -198,14 +199,14 @@ public static int minUnitTests(Phase phase) throws GradingException { }; } - public static String unitTestPackageForCoverage(Phase phase) throws GradingException { + public static CoverageRequirement unitTestCoverageRequirements(Phase phase) throws GradingException { return switch (phase){ - case Phase3 -> "service"; - case Phase4 -> "dataaccess"; //TODO: may need to have a subpackage - case Phase5 -> "facade"; //FIXME: definately needs a subpackage + case Phase3 -> new CoverageRequirement( CoverageRequirement.CoverageType.PACKAGE,"service"); + case Phase4 -> new CoverageRequirement( CoverageRequirement.CoverageType.PACKAGE,"dataaccess.sql"); + case Phase5 -> new CoverageRequirement(CoverageRequirement.CoverageType.CLASS, "ServerFacade"); + default -> new CoverageRequirement(CoverageRequirement.CoverageType.PACKAGE,""); // Originally was "client" which broke the code as there is no client package inside of // the client module. So changed it to "" to get everything from the client module - default -> ""; }; } From 757afc3701aa8946a75857ed1abe18b8bbd69c41 Mon Sep 17 00:00:00 2001 From: mewilker Date: Mon, 18 May 2026 18:06:34 -0600 Subject: [PATCH 15/34] basic untested coverage verifier --- .../verifers/CodeCoverageVerifier.java | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 src/main/java/edu/byu/cs/autograder/compile/verifers/CodeCoverageVerifier.java diff --git a/src/main/java/edu/byu/cs/autograder/compile/verifers/CodeCoverageVerifier.java b/src/main/java/edu/byu/cs/autograder/compile/verifers/CodeCoverageVerifier.java new file mode 100644 index 000000000..62643cbf1 --- /dev/null +++ b/src/main/java/edu/byu/cs/autograder/compile/verifers/CodeCoverageVerifier.java @@ -0,0 +1,63 @@ +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 missingPackages = new TreeSet<>(); + private final Set missingFiles = new TreeSet<>(); + + @Override + public void verify(GradingContext context, StudentCodeReader reader) throws GradingException { + Phase currPhase = context.phase(); + CoverageRequirement coverage = PhaseUtils.unitTestCoverageRequirements(currPhase); + //FIXME: phases without unit tests + 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(coverage.name()).anyMatch(x->true)){ + 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(); + } +} From b6550063f301757938ac4354728424280fbddac0 Mon Sep 17 00:00:00 2001 From: mewilker Date: Tue, 19 May 2026 23:19:23 +0000 Subject: [PATCH 16/34] fix file coverage verifier and implement in compile process --- .../java/edu/byu/cs/autograder/compile/CompileHelper.java | 3 ++- .../autograder/compile/verifers/CodeCoverageVerifier.java | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/byu/cs/autograder/compile/CompileHelper.java b/src/main/java/edu/byu/cs/autograder/compile/CompileHelper.java index c2c5eb188..815839bd5 100644 --- a/src/main/java/edu/byu/cs/autograder/compile/CompileHelper.java +++ b/src/main/java/edu/byu/cs/autograder/compile/CompileHelper.java @@ -26,7 +26,8 @@ public CompileHelper(GradingContext gradingContext) { private final Collection 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 currentModifiers = diff --git a/src/main/java/edu/byu/cs/autograder/compile/verifers/CodeCoverageVerifier.java b/src/main/java/edu/byu/cs/autograder/compile/verifers/CodeCoverageVerifier.java index 62643cbf1..2eaf825b8 100644 --- a/src/main/java/edu/byu/cs/autograder/compile/verifers/CodeCoverageVerifier.java +++ b/src/main/java/edu/byu/cs/autograder/compile/verifers/CodeCoverageVerifier.java @@ -31,7 +31,8 @@ public void verify(GradingContext context, StudentCodeReader reader) throws Grad } } else{ - if (reader.filesMatching(coverage.name()).anyMatch(x->true)){ + + if (reader.filesMatching(buildFileRegex(coverage.name())).count() == 0){ missingFiles.add(coverage.name()); } } @@ -60,4 +61,9 @@ private String buildMessage(){ } return stringBuilder.toString(); } + + private String buildFileRegex(String fileName){ + String regex = "^.*[\\/]"+ fileName + "\\.java$|^"+ fileName + "\\.java$"; + return regex; + } } From a879e254291d9c94c8d35ca1918ffcbb93f78c8e Mon Sep 17 00:00:00 2001 From: mewilker Date: Tue, 19 May 2026 17:22:38 -0600 Subject: [PATCH 17/34] remove irrelevant fixme comment --- .../byu/cs/autograder/compile/verifers/CodeCoverageVerifier.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/edu/byu/cs/autograder/compile/verifers/CodeCoverageVerifier.java b/src/main/java/edu/byu/cs/autograder/compile/verifers/CodeCoverageVerifier.java index 2eaf825b8..f82279272 100644 --- a/src/main/java/edu/byu/cs/autograder/compile/verifers/CodeCoverageVerifier.java +++ b/src/main/java/edu/byu/cs/autograder/compile/verifers/CodeCoverageVerifier.java @@ -23,7 +23,6 @@ public class CodeCoverageVerifier implements StudentCodeVerifier { public void verify(GradingContext context, StudentCodeReader reader) throws GradingException { Phase currPhase = context.phase(); CoverageRequirement coverage = PhaseUtils.unitTestCoverageRequirements(currPhase); - //FIXME: phases without unit tests if (coverage.type() == CoverageRequirement.CoverageType.PACKAGE){ File packageDirectory = new File(context.stageRepo(), coverage.name()); if (!packageDirectory.isDirectory()){ From 2c2faeb5c49cd24a416f83b854896195365e5ec7 Mon Sep 17 00:00:00 2001 From: mewilker Date: Tue, 19 May 2026 17:29:57 -0600 Subject: [PATCH 18/34] readability fixes --- .../cs/autograder/compile/verifers/CodeCoverageVerifier.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/edu/byu/cs/autograder/compile/verifers/CodeCoverageVerifier.java b/src/main/java/edu/byu/cs/autograder/compile/verifers/CodeCoverageVerifier.java index f82279272..7dec285b8 100644 --- a/src/main/java/edu/byu/cs/autograder/compile/verifers/CodeCoverageVerifier.java +++ b/src/main/java/edu/byu/cs/autograder/compile/verifers/CodeCoverageVerifier.java @@ -31,7 +31,7 @@ public void verify(GradingContext context, StudentCodeReader reader) throws Grad } else{ - if (reader.filesMatching(buildFileRegex(coverage.name())).count() == 0){ + if (reader.filesMatching(buildFileRegex(coverage.name())).findAny().isEmpty()){ missingFiles.add(coverage.name()); } } @@ -62,7 +62,6 @@ private String buildMessage(){ } private String buildFileRegex(String fileName){ - String regex = "^.*[\\/]"+ fileName + "\\.java$|^"+ fileName + "\\.java$"; - return regex; + return "^.*[\\/]"+ fileName + "\\.java$|^"+ fileName + "\\.java$"; } } From 323b534a5117f000f45ab9469509b00ca1200835 Mon Sep 17 00:00:00 2001 From: mewilker Date: Tue, 19 May 2026 18:00:46 -0600 Subject: [PATCH 19/34] add minimum coverage percent as a config value in database --- .../edu/byu/cs/autograder/test/UnitTestGrader.java | 14 +++++++++++++- .../java/edu/byu/cs/dataAccess/DaoService.java | 1 + .../dataAccess/daoInterface/ConfigurationDao.java | 3 ++- src/main/java/edu/byu/cs/model/PrivateConfig.java | 3 ++- .../java/edu/byu/cs/service/ConfigService.java | 3 ++- 5 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java b/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java index aaab5e43e..b3e0c955e 100644 --- a/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java +++ b/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java @@ -5,6 +5,9 @@ import edu.byu.cs.autograder.GradingContext; 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.model.Rubric; @@ -17,12 +20,21 @@ */ public class UnitTestGrader extends TestGrader { - private final float targetPercent = 0.8F; // how much we want covered, change me if too low or high + private final float targetPercent; // if config can't load, 80% private final float extraCreditPercent = 0.9F; // double check with professors //TODO: consider making me a config point that is set up on the autograder admin config page like penalty percent public UnitTestGrader(GradingContext gradingContext) { super(gradingContext); + float percent; + try{ + percent = DaoService.getConfigurationDao().getConfiguration(ConfigurationDao.Configuration.COVERAGE_PERCENT, Float.class); + } + catch (DataAccessException e){ + percent = 0.8F; + //do something if fails + } + targetPercent = percent; } @Override diff --git a/src/main/java/edu/byu/cs/dataAccess/DaoService.java b/src/main/java/edu/byu/cs/dataAccess/DaoService.java index 3063e32d7..f70dfa9df 100644 --- a/src/main/java/edu/byu/cs/dataAccess/DaoService.java +++ b/src/main/java/edu/byu/cs/dataAccess/DaoService.java @@ -83,6 +83,7 @@ public static void initializeMemoryDAOs() { configurationDao.setConfiguration(ConfigurationDao.Configuration.PER_DAY_LATE_PENALTY, 0.1f, Float.class); configurationDao.setConfiguration(ConfigurationDao.Configuration.LINES_PER_COMMIT_REQUIRED, 5, Integer.class); configurationDao.setConfiguration(ConfigurationDao.Configuration.CLOCK_FORGIVENESS_MINUTES, 3, Integer.class); + configurationDao.setConfiguration(ConfigurationDao.Configuration.COVERAGE_PERCENT, 0.8f, Float.class); } catch (DataAccessException e) { throw new RuntimeException(e); } diff --git a/src/main/java/edu/byu/cs/dataAccess/daoInterface/ConfigurationDao.java b/src/main/java/edu/byu/cs/dataAccess/daoInterface/ConfigurationDao.java index 6e369499f..48eed0d2b 100644 --- a/src/main/java/edu/byu/cs/dataAccess/daoInterface/ConfigurationDao.java +++ b/src/main/java/edu/byu/cs/dataAccess/daoInterface/ConfigurationDao.java @@ -36,6 +36,7 @@ enum Configuration { CLOCK_FORGIVENESS_MINUTES, MAX_ERROR_OUTPUT_CHARS, HOLIDAY_LIST, - SLACK_LINK + SLACK_LINK, + COVERAGE_PERCENT } } diff --git a/src/main/java/edu/byu/cs/model/PrivateConfig.java b/src/main/java/edu/byu/cs/model/PrivateConfig.java index 6a15b02e4..1bcfacf93 100644 --- a/src/main/java/edu/byu/cs/model/PrivateConfig.java +++ b/src/main/java/edu/byu/cs/model/PrivateConfig.java @@ -34,7 +34,8 @@ public record PenaltyConfig( float gitCommitPenalty, int maxLateDaysPenalized, int linesChangedPerCommit, - int clockForgivenessMinutes + int clockForgivenessMinutes, + float coveragePercent ){ } /** diff --git a/src/main/java/edu/byu/cs/service/ConfigService.java b/src/main/java/edu/byu/cs/service/ConfigService.java index 4ccdad158..86541c3bc 100644 --- a/src/main/java/edu/byu/cs/service/ConfigService.java +++ b/src/main/java/edu/byu/cs/service/ConfigService.java @@ -127,7 +127,8 @@ private static PrivateConfig.PenaltyConfig generatePenaltyConfig() throws DataAc dao.getConfiguration(Configuration.GIT_COMMIT_PENALTY, Float.class), dao.getConfiguration(Configuration.MAX_LATE_DAYS_TO_PENALIZE, Integer.class), dao.getConfiguration(Configuration.LINES_PER_COMMIT_REQUIRED, Integer.class), - dao.getConfiguration(Configuration.CLOCK_FORGIVENESS_MINUTES, Integer.class) + dao.getConfiguration(Configuration.CLOCK_FORGIVENESS_MINUTES, Integer.class), + dao.getConfiguration(Configuration.COVERAGE_PERCENT, Float.class) ); } From 89ea9fd17fbb28532a929bbd96707a964134e681 Mon Sep 17 00:00:00 2001 From: mewilker Date: Tue, 19 May 2026 19:08:32 -0600 Subject: [PATCH 20/34] add extra coverage percent as a config value in database --- .../java/edu/byu/cs/autograder/test/UnitTestGrader.java | 7 +++++-- src/main/java/edu/byu/cs/dataAccess/DaoService.java | 1 + .../byu/cs/dataAccess/daoInterface/ConfigurationDao.java | 3 ++- src/main/java/edu/byu/cs/model/PrivateConfig.java | 3 ++- src/main/java/edu/byu/cs/service/ConfigService.java | 3 ++- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java b/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java index b3e0c955e..92bede154 100644 --- a/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java +++ b/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java @@ -21,20 +21,23 @@ public class UnitTestGrader extends TestGrader { private final float targetPercent; // if config can't load, 80% - private final float extraCreditPercent = 0.9F; // double check with professors + private final float extraCreditPercent; // if config can't load, 90% //TODO: consider making me a config point that is set up on the autograder admin config page like penalty percent public UnitTestGrader(GradingContext gradingContext) { super(gradingContext); float percent; + float extraPercent; try{ percent = DaoService.getConfigurationDao().getConfiguration(ConfigurationDao.Configuration.COVERAGE_PERCENT, Float.class); + extraPercent = DaoService.getConfigurationDao().getConfiguration(ConfigurationDao.Configuration.EXTRA_COVERAGE_PERCENT, Float.class); } catch (DataAccessException e){ percent = 0.8F; - //do something if fails + extraPercent = 0.9F; } targetPercent = percent; + extraCreditPercent = extraPercent; } @Override diff --git a/src/main/java/edu/byu/cs/dataAccess/DaoService.java b/src/main/java/edu/byu/cs/dataAccess/DaoService.java index f70dfa9df..e1f4172b1 100644 --- a/src/main/java/edu/byu/cs/dataAccess/DaoService.java +++ b/src/main/java/edu/byu/cs/dataAccess/DaoService.java @@ -84,6 +84,7 @@ public static void initializeMemoryDAOs() { configurationDao.setConfiguration(ConfigurationDao.Configuration.LINES_PER_COMMIT_REQUIRED, 5, Integer.class); configurationDao.setConfiguration(ConfigurationDao.Configuration.CLOCK_FORGIVENESS_MINUTES, 3, Integer.class); configurationDao.setConfiguration(ConfigurationDao.Configuration.COVERAGE_PERCENT, 0.8f, Float.class); + configurationDao.setConfiguration(ConfigurationDao.Configuration.EXTRA_COVERAGE_PERCENT, 0.9f, Float.class); } catch (DataAccessException e) { throw new RuntimeException(e); } diff --git a/src/main/java/edu/byu/cs/dataAccess/daoInterface/ConfigurationDao.java b/src/main/java/edu/byu/cs/dataAccess/daoInterface/ConfigurationDao.java index 48eed0d2b..17c740e73 100644 --- a/src/main/java/edu/byu/cs/dataAccess/daoInterface/ConfigurationDao.java +++ b/src/main/java/edu/byu/cs/dataAccess/daoInterface/ConfigurationDao.java @@ -37,6 +37,7 @@ enum Configuration { MAX_ERROR_OUTPUT_CHARS, HOLIDAY_LIST, SLACK_LINK, - COVERAGE_PERCENT + COVERAGE_PERCENT, + EXTRA_COVERAGE_PERCENT } } diff --git a/src/main/java/edu/byu/cs/model/PrivateConfig.java b/src/main/java/edu/byu/cs/model/PrivateConfig.java index 1bcfacf93..2408daf20 100644 --- a/src/main/java/edu/byu/cs/model/PrivateConfig.java +++ b/src/main/java/edu/byu/cs/model/PrivateConfig.java @@ -35,7 +35,8 @@ public record PenaltyConfig( int maxLateDaysPenalized, int linesChangedPerCommit, int clockForgivenessMinutes, - float coveragePercent + float coveragePercent, + float extraCoveragePercent ){ } /** diff --git a/src/main/java/edu/byu/cs/service/ConfigService.java b/src/main/java/edu/byu/cs/service/ConfigService.java index 86541c3bc..e4548154e 100644 --- a/src/main/java/edu/byu/cs/service/ConfigService.java +++ b/src/main/java/edu/byu/cs/service/ConfigService.java @@ -128,7 +128,8 @@ private static PrivateConfig.PenaltyConfig generatePenaltyConfig() throws DataAc dao.getConfiguration(Configuration.MAX_LATE_DAYS_TO_PENALIZE, Integer.class), dao.getConfiguration(Configuration.LINES_PER_COMMIT_REQUIRED, Integer.class), dao.getConfiguration(Configuration.CLOCK_FORGIVENESS_MINUTES, Integer.class), - dao.getConfiguration(Configuration.COVERAGE_PERCENT, Float.class) + dao.getConfiguration(Configuration.COVERAGE_PERCENT, Float.class), + dao.getConfiguration(Configuration.EXTRA_COVERAGE_PERCENT, Float.class) ); } From 7469043b3b80bc9cc729ca939c6c593b2fefd9ad Mon Sep 17 00:00:00 2001 From: mewilker Date: Wed, 20 May 2026 22:02:38 +0000 Subject: [PATCH 21/34] update javadocs for new values in private config --- .../java/edu/byu/cs/model/PrivateConfig.java | 110 +++++++++--------- 1 file changed, 56 insertions(+), 54 deletions(-) diff --git a/src/main/java/edu/byu/cs/model/PrivateConfig.java b/src/main/java/edu/byu/cs/model/PrivateConfig.java index 2408daf20..c3072c4a1 100644 --- a/src/main/java/edu/byu/cs/model/PrivateConfig.java +++ b/src/main/java/edu/byu/cs/model/PrivateConfig.java @@ -1,54 +1,56 @@ -package edu.byu.cs.model; - -import java.util.List; -import java.util.Map; - -/** - * Represents the configuration values that only admins should see - * - * @param penalty A {@link PenaltyConfig} containing information about penalties - * students may receive - * @param courseNumber The number assigned to the course on Canvas - * @param assignments A list containing information about an assignment in - * Canvas for each {@link AssignmentConfig}. - * @param holidays A list of holidays the AutoGrader doesn't count towards the late penalty - */ -public record PrivateConfig( - PenaltyConfig penalty, - int courseNumber, - List assignments, - String[] holidays -) { - /** - * Represents the configuration information needed for penalties students may receive - * - * @param perDayLatePenalty The penalty to apply on a submission for everyday late - * @param gitCommitPenalty the penalty to apply on a submission for missing commits - * @param maxLateDaysPenalized the maximum number of days the late penalty should apply - * @param linesChangedPerCommit the minimum number of lines needed for a commit to count - * @param clockForgivenessMinutes the number of minutes a commit can be authored - * past the time of submission - */ - public record PenaltyConfig( - float perDayLatePenalty, - float gitCommitPenalty, - int maxLateDaysPenalized, - int linesChangedPerCommit, - int clockForgivenessMinutes, - float coveragePercent, - float extraCoveragePercent - ){ } - - /** - * Represents the configuration information for a Canvas assignment - * - * @param phase the phase associated with the Canvas assignment - * @param assignmentId the id of the assignment in Canvas - * @param rubricItems the rubric items for the Canvas assignment - */ - public record AssignmentConfig( - Phase phase, - int assignmentId, - Map rubricItems - ){ } -} +package edu.byu.cs.model; + +import java.util.List; +import java.util.Map; + +/** + * Represents the configuration values that only admins should see + * + * @param penalty A {@link PenaltyConfig} containing information about penalties + * students may receive + * @param courseNumber The number assigned to the course on Canvas + * @param assignments A list containing information about an assignment in + * Canvas for each {@link AssignmentConfig}. + * @param holidays A list of holidays the AutoGrader doesn't count towards the late penalty + */ +public record PrivateConfig( + PenaltyConfig penalty, + int courseNumber, + List assignments, + String[] holidays +) { + /** + * Represents the configuration information needed for penalties students may receive + * + * @param perDayLatePenalty The penalty to apply on a submission for everyday late + * @param gitCommitPenalty the penalty to apply on a submission for missing commits + * @param maxLateDaysPenalized the maximum number of days the late penalty should apply + * @param linesChangedPerCommit the minimum number of lines needed for a commit to count + * @param clockForgivenessMinutes the number of minutes a commit can be authored + * past the time of submission + * @param coveragePercent the percentage of code coverage expected for a submission to receive full credit + * @param extraCoveragePercent the percentage of code coverage expected for a submission to receive extra credit + */ + public record PenaltyConfig( + float perDayLatePenalty, + float gitCommitPenalty, + int maxLateDaysPenalized, + int linesChangedPerCommit, + int clockForgivenessMinutes, + float coveragePercent, + float extraCoveragePercent + ){ } + + /** + * Represents the configuration information for a Canvas assignment + * + * @param phase the phase associated with the Canvas assignment + * @param assignmentId the id of the assignment in Canvas + * @param rubricItems the rubric items for the Canvas assignment + */ + public record AssignmentConfig( + Phase phase, + int assignmentId, + Map rubricItems + ){ } +} From 6f61688de3b4a8ecb65f789b987e6a0ad4eaf2a9 Mon Sep 17 00:00:00 2001 From: mewilker Date: Wed, 20 May 2026 22:06:34 +0000 Subject: [PATCH 22/34] add frontend code to change and display coverage percent --- .../components/config/PenaltyConfigEditor.vue | 24 ++++++++++++++++++- .../frontend/src/services/configService.ts | 4 ++++ .../resources/frontend/src/stores/config.ts | 2 ++ .../src/views/AdminView/ConfigView.vue | 8 +++++++ 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/main/resources/frontend/src/components/config/PenaltyConfigEditor.vue b/src/main/resources/frontend/src/components/config/PenaltyConfigEditor.vue index 293f2f7f7..4ec0d7bc4 100644 --- a/src/main/resources/frontend/src/components/config/PenaltyConfigEditor.vue +++ b/src/main/resources/frontend/src/components/config/PenaltyConfigEditor.vue @@ -14,6 +14,8 @@ const maxLateDays = ref(config.admin.penalty.maxLateDaysPenalized); const gitPenalty = ref(Math.round(config.admin.penalty.gitCommitPenalty * 100)); const linesChangedPerCommit = ref(config.admin.penalty.linesChangedPerCommit); const clockForgivenessMinutes = ref(config.admin.penalty.clockForgivenessMinutes); +const coveragePercent = ref(Math.round(config.admin.penalty.coveragePercent * 100)); +const extraCoveragePercent = ref(Math.round(config.admin.penalty.extraCoveragePercent * 100)); const valuesReady = () => { return ( @@ -23,7 +25,11 @@ const valuesReady = () => { latePenalty.value <= 100 && maxLateDays.value >= 0 && linesChangedPerCommit.value >= 0 && - clockForgivenessMinutes.value >= 0 + clockForgivenessMinutes.value >= 0 && + coveragePercent.value >= 0 && + coveragePercent.value <= 100 && + extraCoveragePercent.value >= 0 && + extraCoveragePercent.value <= 100 ); }; @@ -35,6 +41,8 @@ const submit = async () => { latePenalty.value / 100, linesChangedPerCommit.value, clockForgivenessMinutes.value, + coveragePercent.value / 100, + extraCoveragePercent.value / 100 ); closeEditor(); @@ -85,6 +93,20 @@ const submit = async () => {

minutes

+
+

Coverage Percent

+

+ The percentage of code coverage expected for unit tests to receive full credit. +

+

%

+
+
+

Extra Coverage Percent

+

+ The percentage of code coverage expected for unit tests to receive extra credit. +

+

%

+
diff --git a/src/main/resources/frontend/src/services/configService.ts b/src/main/resources/frontend/src/services/configService.ts index dddf181a9..41c0e52a7 100644 --- a/src/main/resources/frontend/src/services/configService.ts +++ b/src/main/resources/frontend/src/services/configService.ts @@ -16,6 +16,8 @@ export const setPenalties = async ( perDayLatePenalty: number, linesChangedPerCommit: number, clockForgivenessMinutes: number, + coveragePercent: number, + extraCoveragePercent: number ) => { await doSetConfigItem("/api/admin/config/penalties", { maxLateDaysPenalized: maxLateDaysPenalized, @@ -23,6 +25,8 @@ export const setPenalties = async ( perDayLatePenalty: perDayLatePenalty, linesChangedPerCommit: linesChangedPerCommit, clockForgivenessMinutes: clockForgivenessMinutes, + coveragePercent: coveragePercent, + extraCoveragePercent: extraCoveragePercent, }); }; diff --git a/src/main/resources/frontend/src/stores/config.ts b/src/main/resources/frontend/src/stores/config.ts index dabf3f869..99d65ab5c 100644 --- a/src/main/resources/frontend/src/stores/config.ts +++ b/src/main/resources/frontend/src/stores/config.ts @@ -39,6 +39,8 @@ export type PrivateConfig = { maxLateDaysPenalized: number; linesChangedPerCommit: number; clockForgivenessMinutes: number; + coveragePercent: number; + extraCoveragePercent: number; }; courseNumber: number; diff --git a/src/main/resources/frontend/src/views/AdminView/ConfigView.vue b/src/main/resources/frontend/src/views/AdminView/ConfigView.vue index 99ec2d926..118b80eb9 100644 --- a/src/main/resources/frontend/src/views/AdminView/ConfigView.vue +++ b/src/main/resources/frontend/src/views/AdminView/ConfigView.vue @@ -160,6 +160,14 @@ onMounted(async () => { Clock Forgiveness: {{ config.admin.penalty.clockForgivenessMinutes }} minutes

+

+ Coverage Percent: {{ Math.round(config.admin.penalty.coveragePercent * 100) }}% +

+

+ Extra Coverage Percent: {{ Math.round(config.admin.penalty.extraCoveragePercent * 100) }}% +

From 7812bab010aa52f9ee2a2b0cb691f9d7a4f25174 Mon Sep 17 00:00:00 2001 From: mewilker Date: Wed, 20 May 2026 16:17:22 -0600 Subject: [PATCH 23/34] add code coverage to the penalty update requests --- .../edu/byu/cs/model/request/ConfigPenaltyUpdateRequest.java | 4 +++- src/main/java/edu/byu/cs/service/ConfigService.java | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/byu/cs/model/request/ConfigPenaltyUpdateRequest.java b/src/main/java/edu/byu/cs/model/request/ConfigPenaltyUpdateRequest.java index 321ddecaf..7cbc54ec7 100644 --- a/src/main/java/edu/byu/cs/model/request/ConfigPenaltyUpdateRequest.java +++ b/src/main/java/edu/byu/cs/model/request/ConfigPenaltyUpdateRequest.java @@ -15,5 +15,7 @@ public record ConfigPenaltyUpdateRequest( int maxLateDaysPenalized, float gitCommitPenalty, int linesChangedPerCommit, - int clockForgivenessMinutes + int clockForgivenessMinutes, + float coveragePercent, + float extraCoveragePercent ) {} diff --git a/src/main/java/edu/byu/cs/service/ConfigService.java b/src/main/java/edu/byu/cs/service/ConfigService.java index e4548154e..8e97f5d4d 100644 --- a/src/main/java/edu/byu/cs/service/ConfigService.java +++ b/src/main/java/edu/byu/cs/service/ConfigService.java @@ -12,6 +12,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.ObjectInputFilter; import java.time.*; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; @@ -365,12 +366,16 @@ public static void processPenaltyUpdates(User user, ConfigPenaltyUpdateRequest r validateNonNegativeInt(request.clockForgivenessMinutes(), "Clock Forgiveness Minutes"); validateNonNegativeInt(request.maxLateDaysPenalized(), "Max Late Days Penalized"); validateNonNegativeInt(request.linesChangedPerCommit(), "Lines Changed Per Commit"); + validateValidPercentFloat(request.coveragePercent(), "Code Coverage Percent"); + validateValidPercentFloat(request.extraCoveragePercent(), "Extra Code Coverage Percent"); setConfigItem(user, Configuration.GIT_COMMIT_PENALTY, request.gitCommitPenalty(), Float.class); setConfigItem(user, Configuration.PER_DAY_LATE_PENALTY, request.perDayLatePenalty(), Float.class); setConfigItem(user, Configuration.CLOCK_FORGIVENESS_MINUTES, request.clockForgivenessMinutes(), Integer.class); setConfigItem(user, Configuration.MAX_LATE_DAYS_TO_PENALIZE, request.maxLateDaysPenalized(), Integer.class); setConfigItem(user, Configuration.LINES_PER_COMMIT_REQUIRED, request.linesChangedPerCommit(), Integer.class); + setConfigItem(user, Configuration.COVERAGE_PERCENT, request.coveragePercent(), Float.class); + setConfigItem(user, Configuration.EXTRA_COVERAGE_PERCENT, request.extraCoveragePercent(), Float.class); } /** From f6aede45635f227ae18a1bcd619394e3680fda22 Mon Sep 17 00:00:00 2001 From: mewilker Date: Wed, 20 May 2026 22:19:27 +0000 Subject: [PATCH 24/34] update javadocs --- .../edu/byu/cs/model/request/ConfigPenaltyUpdateRequest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/edu/byu/cs/model/request/ConfigPenaltyUpdateRequest.java b/src/main/java/edu/byu/cs/model/request/ConfigPenaltyUpdateRequest.java index 7cbc54ec7..723d661ec 100644 --- a/src/main/java/edu/byu/cs/model/request/ConfigPenaltyUpdateRequest.java +++ b/src/main/java/edu/byu/cs/model/request/ConfigPenaltyUpdateRequest.java @@ -9,6 +9,8 @@ * @param linesChangedPerCommit the minimum number of lines needed for a commit to count * @param clockForgivenessMinutes the number of minutes a commit can be authored * past the time of submission + * @param coveragePercent the percentage of code coverage expected for a submission to receive full credit + * @param extraCoveragePercent the percentage of code coverage expected for a submission to receive extra credit */ public record ConfigPenaltyUpdateRequest( float perDayLatePenalty, From abe416edb8ca86329523d6d638924d44012e94df Mon Sep 17 00:00:00 2001 From: mewilker Date: Wed, 20 May 2026 18:40:37 -0600 Subject: [PATCH 25/34] add logger to unit test grader to indicate when dao could not be read from --- .../java/edu/byu/cs/autograder/test/UnitTestGrader.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java b/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java index 92bede154..f8e647006 100644 --- a/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java +++ b/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java @@ -14,6 +14,8 @@ import edu.byu.cs.model.TestNode; import edu.byu.cs.model.TestOutput; import edu.byu.cs.util.PhaseUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Runs and scores the unit tests for the phase a submission is graded for @@ -22,7 +24,8 @@ public class UnitTestGrader extends TestGrader { private final float targetPercent; // if config can't load, 80% private final float extraCreditPercent; // if config can't load, 90% - //TODO: consider making me a config point that is set up on the autograder admin config page like penalty percent + + private static final Logger LOGGER = LoggerFactory.getLogger(UnitTestGrader.class); public UnitTestGrader(GradingContext gradingContext) { super(gradingContext); @@ -35,6 +38,7 @@ public UnitTestGrader(GradingContext gradingContext) { catch (DataAccessException e){ percent = 0.8F; extraPercent = 0.9F; + LOGGER.error("Could not get coverage percents from config, using default values of 80% and 90%", e); } targetPercent = percent; extraCreditPercent = extraPercent; From 8512bcbd0fb4f1ace4dbc493e8ffaebfb67b0906 Mon Sep 17 00:00:00 2001 From: mewilker Date: Wed, 20 May 2026 18:58:30 -0600 Subject: [PATCH 26/34] add coverage type as a configurable option on backend --- .../cs/autograder/test/CoverageAnalyzer.java | 40 ++++++++++++++----- .../edu/byu/cs/dataAccess/DaoService.java | 1 + .../daoInterface/ConfigurationDao.java | 3 +- .../java/edu/byu/cs/model/PrivateConfig.java | 5 ++- .../request/ConfigPenaltyUpdateRequest.java | 11 ++++- .../edu/byu/cs/service/ConfigService.java | 4 +- 6 files changed, 50 insertions(+), 14 deletions(-) diff --git a/src/main/java/edu/byu/cs/autograder/test/CoverageAnalyzer.java b/src/main/java/edu/byu/cs/autograder/test/CoverageAnalyzer.java index 5bd5a4052..7889bddab 100644 --- a/src/main/java/edu/byu/cs/autograder/test/CoverageAnalyzer.java +++ b/src/main/java/edu/byu/cs/autograder/test/CoverageAnalyzer.java @@ -1,9 +1,14 @@ 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; @@ -14,16 +19,27 @@ * {@link CoverageAnalysis} containing the appropriate coverage results */ 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 = "LINE"; - 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} @@ -52,8 +68,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; + } + } } } diff --git a/src/main/java/edu/byu/cs/dataAccess/DaoService.java b/src/main/java/edu/byu/cs/dataAccess/DaoService.java index e1f4172b1..87f708c57 100644 --- a/src/main/java/edu/byu/cs/dataAccess/DaoService.java +++ b/src/main/java/edu/byu/cs/dataAccess/DaoService.java @@ -85,6 +85,7 @@ public static void initializeMemoryDAOs() { configurationDao.setConfiguration(ConfigurationDao.Configuration.CLOCK_FORGIVENESS_MINUTES, 3, Integer.class); configurationDao.setConfiguration(ConfigurationDao.Configuration.COVERAGE_PERCENT, 0.8f, Float.class); configurationDao.setConfiguration(ConfigurationDao.Configuration.EXTRA_COVERAGE_PERCENT, 0.9f, Float.class); + configurationDao.setConfiguration(ConfigurationDao.Configuration.COVERAGE_TYPE, "LINE", String.class); } catch (DataAccessException e) { throw new RuntimeException(e); } diff --git a/src/main/java/edu/byu/cs/dataAccess/daoInterface/ConfigurationDao.java b/src/main/java/edu/byu/cs/dataAccess/daoInterface/ConfigurationDao.java index 17c740e73..af1fe8443 100644 --- a/src/main/java/edu/byu/cs/dataAccess/daoInterface/ConfigurationDao.java +++ b/src/main/java/edu/byu/cs/dataAccess/daoInterface/ConfigurationDao.java @@ -38,6 +38,7 @@ enum Configuration { HOLIDAY_LIST, SLACK_LINK, COVERAGE_PERCENT, - EXTRA_COVERAGE_PERCENT + EXTRA_COVERAGE_PERCENT, + COVERAGE_TYPE } } diff --git a/src/main/java/edu/byu/cs/model/PrivateConfig.java b/src/main/java/edu/byu/cs/model/PrivateConfig.java index c3072c4a1..44ae0aa81 100644 --- a/src/main/java/edu/byu/cs/model/PrivateConfig.java +++ b/src/main/java/edu/byu/cs/model/PrivateConfig.java @@ -1,5 +1,7 @@ package edu.byu.cs.model; +import edu.byu.cs.model.request.ConfigPenaltyUpdateRequest; + import java.util.List; import java.util.Map; @@ -38,7 +40,8 @@ public record PenaltyConfig( int linesChangedPerCommit, int clockForgivenessMinutes, float coveragePercent, - float extraCoveragePercent + float extraCoveragePercent, + ConfigPenaltyUpdateRequest.CoverageType coverageType ){ } /** diff --git a/src/main/java/edu/byu/cs/model/request/ConfigPenaltyUpdateRequest.java b/src/main/java/edu/byu/cs/model/request/ConfigPenaltyUpdateRequest.java index 723d661ec..64ad9d320 100644 --- a/src/main/java/edu/byu/cs/model/request/ConfigPenaltyUpdateRequest.java +++ b/src/main/java/edu/byu/cs/model/request/ConfigPenaltyUpdateRequest.java @@ -19,5 +19,12 @@ public record ConfigPenaltyUpdateRequest( int linesChangedPerCommit, int clockForgivenessMinutes, float coveragePercent, - float extraCoveragePercent -) {} + float extraCoveragePercent, + CoverageType coverageType +) { + public enum CoverageType { + LINE, + BRANCH + } +} + diff --git a/src/main/java/edu/byu/cs/service/ConfigService.java b/src/main/java/edu/byu/cs/service/ConfigService.java index 8e97f5d4d..f7029c03f 100644 --- a/src/main/java/edu/byu/cs/service/ConfigService.java +++ b/src/main/java/edu/byu/cs/service/ConfigService.java @@ -130,7 +130,8 @@ private static PrivateConfig.PenaltyConfig generatePenaltyConfig() throws DataAc dao.getConfiguration(Configuration.LINES_PER_COMMIT_REQUIRED, Integer.class), dao.getConfiguration(Configuration.CLOCK_FORGIVENESS_MINUTES, Integer.class), dao.getConfiguration(Configuration.COVERAGE_PERCENT, Float.class), - dao.getConfiguration(Configuration.EXTRA_COVERAGE_PERCENT, Float.class) + dao.getConfiguration(Configuration.EXTRA_COVERAGE_PERCENT, Float.class), + ConfigPenaltyUpdateRequest.CoverageType.valueOf(dao.getConfiguration(Configuration.COVERAGE_TYPE, String.class)) ); } @@ -376,6 +377,7 @@ public static void processPenaltyUpdates(User user, ConfigPenaltyUpdateRequest r setConfigItem(user, Configuration.LINES_PER_COMMIT_REQUIRED, request.linesChangedPerCommit(), Integer.class); setConfigItem(user, Configuration.COVERAGE_PERCENT, request.coveragePercent(), Float.class); setConfigItem(user, Configuration.EXTRA_COVERAGE_PERCENT, request.extraCoveragePercent(), Float.class); + setConfigItem(user, Configuration.COVERAGE_TYPE, request.coverageType().name(), String.class); } /** From 38ea840edb564ae182c8b29b1c0824233e393a2b Mon Sep 17 00:00:00 2001 From: mewilker Date: Thu, 21 May 2026 20:26:59 +0000 Subject: [PATCH 27/34] add javadoc comments for coverage type --- .../cs/autograder/test/CoverageAnalyzer.java | 200 +++++++++--------- .../java/edu/byu/cs/model/PrivateConfig.java | 1 + .../request/ConfigPenaltyUpdateRequest.java | 61 +++--- 3 files changed, 133 insertions(+), 129 deletions(-) diff --git a/src/main/java/edu/byu/cs/autograder/test/CoverageAnalyzer.java b/src/main/java/edu/byu/cs/autograder/test/CoverageAnalyzer.java index 7889bddab..92420fe50 100644 --- a/src/main/java/edu/byu/cs/autograder/test/CoverageAnalyzer.java +++ b/src/main/java/edu/byu/cs/autograder/test/CoverageAnalyzer.java @@ -1,99 +1,101 @@ -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 - */ -public class CoverageAnalyzer { - 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} - * - * @param jacocoCsvOutput the file storing the coverage results from running the tests - * @return a {@link CoverageAnalysis} containing the code coverage results - * @throws GradingException if an issue arose parsing the coverage results - */ - public CoverageAnalysis parse(File jacocoCsvOutput) throws GradingException { - Collection classAnalyses = new HashSet<>(); - if(!jacocoCsvOutput.exists()) { - return new CoverageAnalysis(classAnalyses); - } - - String csv = FileUtils.readStringFromFile(jacocoCsvOutput); - String[] lines = csv.split("\n"); - - Integer classHeader = null; - Integer packageHeader = null; - Integer coverageMissedHeader = null; - Integer coverageCoveredHeader = null; - - String[] headers = lines[0].split(","); - for(int i = 0; i < headers.length; i++) { - String s = headers[i].trim(); - switch (s) { - case CLASS_HEADER -> classHeader = i; - case PACKAGE_HEADER -> packageHeader = i; - default -> { - if (s.equals(this.coverageMissedHeader)){ - coverageMissedHeader = i; - } - else if (s.equals(this.coverageCoveredHeader)) { - coverageCoveredHeader = i; - } - } - } - } - - if(classHeader == null || packageHeader == null || coverageMissedHeader == null || coverageCoveredHeader == null) { - throw new GradingException("Error parsing coverage results"); - } - - for(int i = 1; i < lines.length; i++) { - String[] values = lines[i].split(","); - - String className = values[classHeader].trim(); - String packageName = values[packageHeader].trim(); - int coverageMissed = Integer.parseInt(values[coverageMissedHeader].trim()); - int coverageCovered = Integer.parseInt(values[coverageCoveredHeader].trim()); - - classAnalyses.add(new ClassCoverageAnalysis(className, packageName, coverageCovered, coverageMissed)); - } - - return new CoverageAnalysis(classAnalyses); - } -} +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. + *
+ * Supports Branch and Line coverage. Default coverage is Line Coverage. + */ +public class CoverageAnalyzer { + 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} + * + * @param jacocoCsvOutput the file storing the coverage results from running the tests + * @return a {@link CoverageAnalysis} containing the code coverage results + * @throws GradingException if an issue arose parsing the coverage results + */ + public CoverageAnalysis parse(File jacocoCsvOutput) throws GradingException { + Collection classAnalyses = new HashSet<>(); + if(!jacocoCsvOutput.exists()) { + return new CoverageAnalysis(classAnalyses); + } + + String csv = FileUtils.readStringFromFile(jacocoCsvOutput); + String[] lines = csv.split("\n"); + + Integer classHeader = null; + Integer packageHeader = null; + Integer coverageMissedHeader = null; + Integer coverageCoveredHeader = null; + + String[] headers = lines[0].split(","); + for(int i = 0; i < headers.length; i++) { + String s = headers[i].trim(); + switch (s) { + case CLASS_HEADER -> classHeader = i; + case PACKAGE_HEADER -> packageHeader = i; + default -> { + if (s.equals(this.coverageMissedHeader)){ + coverageMissedHeader = i; + } + else if (s.equals(this.coverageCoveredHeader)) { + coverageCoveredHeader = i; + } + } + } + } + + if(classHeader == null || packageHeader == null || coverageMissedHeader == null || coverageCoveredHeader == null) { + throw new GradingException("Error parsing coverage results"); + } + + for(int i = 1; i < lines.length; i++) { + String[] values = lines[i].split(","); + + String className = values[classHeader].trim(); + String packageName = values[packageHeader].trim(); + int coverageMissed = Integer.parseInt(values[coverageMissedHeader].trim()); + int coverageCovered = Integer.parseInt(values[coverageCoveredHeader].trim()); + + classAnalyses.add(new ClassCoverageAnalysis(className, packageName, coverageCovered, coverageMissed)); + } + + return new CoverageAnalysis(classAnalyses); + } +} diff --git a/src/main/java/edu/byu/cs/model/PrivateConfig.java b/src/main/java/edu/byu/cs/model/PrivateConfig.java index 44ae0aa81..f3b8c1375 100644 --- a/src/main/java/edu/byu/cs/model/PrivateConfig.java +++ b/src/main/java/edu/byu/cs/model/PrivateConfig.java @@ -32,6 +32,7 @@ public record PrivateConfig( * past the time of submission * @param coveragePercent the percentage of code coverage expected for a submission to receive full credit * @param extraCoveragePercent the percentage of code coverage expected for a submission to receive extra credit + * @param coverageType the type of code coverage to use when calculating penalties (branch or line) */ public record PenaltyConfig( float perDayLatePenalty, diff --git a/src/main/java/edu/byu/cs/model/request/ConfigPenaltyUpdateRequest.java b/src/main/java/edu/byu/cs/model/request/ConfigPenaltyUpdateRequest.java index 64ad9d320..dc87d2e46 100644 --- a/src/main/java/edu/byu/cs/model/request/ConfigPenaltyUpdateRequest.java +++ b/src/main/java/edu/byu/cs/model/request/ConfigPenaltyUpdateRequest.java @@ -1,30 +1,31 @@ -package edu.byu.cs.model.request; - -/** - * Represents a request for updating the values used for calculating penalties - * - * @param perDayLatePenalty the percentage taken off submission for everyday late - * @param maxLateDaysPenalized the maximum number of days the late penalty should apply - * @param gitCommitPenalty the penalty to apply for missing commits - * @param linesChangedPerCommit the minimum number of lines needed for a commit to count - * @param clockForgivenessMinutes the number of minutes a commit can be authored - * past the time of submission - * @param coveragePercent the percentage of code coverage expected for a submission to receive full credit - * @param extraCoveragePercent the percentage of code coverage expected for a submission to receive extra credit - */ -public record ConfigPenaltyUpdateRequest( - float perDayLatePenalty, - int maxLateDaysPenalized, - float gitCommitPenalty, - int linesChangedPerCommit, - int clockForgivenessMinutes, - float coveragePercent, - float extraCoveragePercent, - CoverageType coverageType -) { - public enum CoverageType { - LINE, - BRANCH - } -} - +package edu.byu.cs.model.request; + +/** + * Represents a request for updating the values used for calculating penalties + * + * @param perDayLatePenalty the percentage taken off submission for everyday late + * @param maxLateDaysPenalized the maximum number of days the late penalty should apply + * @param gitCommitPenalty the penalty to apply for missing commits + * @param linesChangedPerCommit the minimum number of lines needed for a commit to count + * @param clockForgivenessMinutes the number of minutes a commit can be authored + * past the time of submission + * @param coveragePercent the percentage of code coverage expected for a submission to receive full credit + * @param extraCoveragePercent the percentage of code coverage expected for a submission to receive extra credit + * @param coverageType the type of code coverage to use when calculating penalties (branch or line) + */ +public record ConfigPenaltyUpdateRequest( + float perDayLatePenalty, + int maxLateDaysPenalized, + float gitCommitPenalty, + int linesChangedPerCommit, + int clockForgivenessMinutes, + float coveragePercent, + float extraCoveragePercent, + CoverageType coverageType +) { + public enum CoverageType { + LINE, + BRANCH + } +} + From a20b496c0bb7cde0d30e5187249db9a4a261ffe1 Mon Sep 17 00:00:00 2001 From: mewilker Date: Thu, 21 May 2026 15:52:54 -0600 Subject: [PATCH 28/34] fix enum verification getting and setting values --- src/main/java/edu/byu/cs/service/ConfigService.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/byu/cs/service/ConfigService.java b/src/main/java/edu/byu/cs/service/ConfigService.java index f7029c03f..ac3a46b40 100644 --- a/src/main/java/edu/byu/cs/service/ConfigService.java +++ b/src/main/java/edu/byu/cs/service/ConfigService.java @@ -123,6 +123,7 @@ private static PublicConfig.ShutdownConfig generateShutdownConfig() throws DataA // PRIVATE CONFIG GENERATORS // private static PrivateConfig.PenaltyConfig generatePenaltyConfig() throws DataAccessException { + String coverageType = dao.getConfiguration(Configuration.COVERAGE_TYPE, String.class); return new PrivateConfig.PenaltyConfig( dao.getConfiguration(Configuration.PER_DAY_LATE_PENALTY, Float.class), dao.getConfiguration(Configuration.GIT_COMMIT_PENALTY, Float.class), @@ -131,7 +132,8 @@ private static PrivateConfig.PenaltyConfig generatePenaltyConfig() throws DataAc dao.getConfiguration(Configuration.CLOCK_FORGIVENESS_MINUTES, Integer.class), dao.getConfiguration(Configuration.COVERAGE_PERCENT, Float.class), dao.getConfiguration(Configuration.EXTRA_COVERAGE_PERCENT, Float.class), - ConfigPenaltyUpdateRequest.CoverageType.valueOf(dao.getConfiguration(Configuration.COVERAGE_TYPE, String.class)) + coverageType.isEmpty() ? ConfigPenaltyUpdateRequest.CoverageType.LINE : + ConfigPenaltyUpdateRequest.CoverageType.valueOf(coverageType) ); } @@ -369,6 +371,7 @@ public static void processPenaltyUpdates(User user, ConfigPenaltyUpdateRequest r validateNonNegativeInt(request.linesChangedPerCommit(), "Lines Changed Per Commit"); validateValidPercentFloat(request.coveragePercent(), "Code Coverage Percent"); validateValidPercentFloat(request.extraCoveragePercent(), "Extra Code Coverage Percent"); + validateEnum(request.coverageType().name(), ConfigPenaltyUpdateRequest.CoverageType.class); setConfigItem(user, Configuration.GIT_COMMIT_PENALTY, request.gitCommitPenalty(), Float.class); setConfigItem(user, Configuration.PER_DAY_LATE_PENALTY, request.perDayLatePenalty(), Float.class); @@ -472,6 +475,10 @@ private static void validateNonNegativeInt(int value, String name) { } } + private static > void validateEnum(String value, ClassenumName){ + Enum.valueOf(enumName, value); + } + private static void setConfigItem(User admin, Configuration configKey, T value, Class type) throws DataAccessException { ConfigurationDao dao = DaoService.getConfigurationDao(); From 08f59eb0d33ff095a8cc355b40530615da91a1a4 Mon Sep 17 00:00:00 2001 From: mewilker Date: Thu, 21 May 2026 22:50:26 +0000 Subject: [PATCH 29/34] add a toggle for branch or line coverage in private config --- .../src/components/config/PenaltyConfigEditor.vue | 12 +++++++++++- .../resources/frontend/src/services/configService.ts | 4 +++- src/main/resources/frontend/src/stores/config.ts | 4 ++++ .../frontend/src/views/AdminView/ConfigView.vue | 4 ++++ 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/main/resources/frontend/src/components/config/PenaltyConfigEditor.vue b/src/main/resources/frontend/src/components/config/PenaltyConfigEditor.vue index 4ec0d7bc4..3f527507a 100644 --- a/src/main/resources/frontend/src/components/config/PenaltyConfigEditor.vue +++ b/src/main/resources/frontend/src/components/config/PenaltyConfigEditor.vue @@ -16,6 +16,7 @@ const linesChangedPerCommit = ref(config.admin.penalty.linesChangedPerCo const clockForgivenessMinutes = ref(config.admin.penalty.clockForgivenessMinutes); const coveragePercent = ref(Math.round(config.admin.penalty.coveragePercent * 100)); const extraCoveragePercent = ref(Math.round(config.admin.penalty.extraCoveragePercent * 100)); +const coverageType = ref<'LINE' | 'BRANCH'>(config.admin.penalty.coverageType); const valuesReady = () => { return ( @@ -42,7 +43,8 @@ const submit = async () => { linesChangedPerCommit.value, clockForgivenessMinutes.value, coveragePercent.value / 100, - extraCoveragePercent.value / 100 + extraCoveragePercent.value / 100, + coverageType.value ); closeEditor(); @@ -107,6 +109,14 @@ const submit = async () => {

%

+
+

Coverage Type

+

+ The type of coverage to be measured. +

+

Line

+

Branch

+
diff --git a/src/main/resources/frontend/src/services/configService.ts b/src/main/resources/frontend/src/services/configService.ts index 41c0e52a7..de79afeac 100644 --- a/src/main/resources/frontend/src/services/configService.ts +++ b/src/main/resources/frontend/src/services/configService.ts @@ -17,7 +17,8 @@ export const setPenalties = async ( linesChangedPerCommit: number, clockForgivenessMinutes: number, coveragePercent: number, - extraCoveragePercent: number + extraCoveragePercent: number, + coverageType: 'LINE' | 'BRANCH' ) => { await doSetConfigItem("/api/admin/config/penalties", { maxLateDaysPenalized: maxLateDaysPenalized, @@ -27,6 +28,7 @@ export const setPenalties = async ( clockForgivenessMinutes: clockForgivenessMinutes, coveragePercent: coveragePercent, extraCoveragePercent: extraCoveragePercent, + coverageType: coverageType, }); }; diff --git a/src/main/resources/frontend/src/stores/config.ts b/src/main/resources/frontend/src/stores/config.ts index 99d65ab5c..556f55363 100644 --- a/src/main/resources/frontend/src/stores/config.ts +++ b/src/main/resources/frontend/src/stores/config.ts @@ -41,6 +41,7 @@ export type PrivateConfig = { clockForgivenessMinutes: number; coveragePercent: number; extraCoveragePercent: number; + coverageType: 'LINE' | 'BRANCH'; }; courseNumber: number; @@ -81,6 +82,9 @@ export const useConfigStore = defineStore("config", () => { maxLateDaysPenalized: -1, linesChangedPerCommit: -1, clockForgivenessMinutes: -1, + coveragePercent: -1, + extraCoveragePercent: -1, + coverageType: 'LINE', }, courseNumber: -1, assignments: [], diff --git a/src/main/resources/frontend/src/views/AdminView/ConfigView.vue b/src/main/resources/frontend/src/views/AdminView/ConfigView.vue index 118b80eb9..dc839a655 100644 --- a/src/main/resources/frontend/src/views/AdminView/ConfigView.vue +++ b/src/main/resources/frontend/src/views/AdminView/ConfigView.vue @@ -168,6 +168,10 @@ onMounted(async () => { Extra Coverage Percent: {{ Math.round(config.admin.penalty.extraCoveragePercent * 100) }}%

+

+ Coverage Type: {{ config.admin.penalty.coverageType.toLocaleLowerCase()}} +

From 9c70b9a0d5df4ba09aa2c249cfb7def1e03fc016 Mon Sep 17 00:00:00 2001 From: mewilker Date: Fri, 22 May 2026 09:27:12 -0600 Subject: [PATCH 30/34] add coverage to ConfigurationDaoTest --- .../java/edu/byu/cs/dataAccess/base/ConfigurationDaoTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/java/edu/byu/cs/dataAccess/base/ConfigurationDaoTest.java b/src/test/java/edu/byu/cs/dataAccess/base/ConfigurationDaoTest.java index 8c6fe4526..195f2cf8f 100644 --- a/src/test/java/edu/byu/cs/dataAccess/base/ConfigurationDaoTest.java +++ b/src/test/java/edu/byu/cs/dataAccess/base/ConfigurationDaoTest.java @@ -96,8 +96,10 @@ Object generateDummyDataForKey(ConfigurationDao.Configuration key){ -> random.nextInt(1000000,2000000); case MAX_ERROR_OUTPUT_CHARS -> random.nextInt(1, 10000); case GRADER_SHUTDOWN_WARNING_MILLISECONDS -> 86400000; //24 hours in milliseconds - case GIT_COMMIT_PENALTY, PER_DAY_LATE_PENALTY -> random.nextFloat(0, 1); + case GIT_COMMIT_PENALTY, PER_DAY_LATE_PENALTY, COVERAGE_PERCENT, EXTRA_COVERAGE_PERCENT + -> random.nextFloat(0, 1); case SLACK_LINK, BANNER_LINK, BANNER_COLOR, BANNER_MESSAGE-> "https://slack.com"; + case COVERAGE_TYPE -> "LINE"; case STUDENT_SUBMISSIONS_ENABLED -> random.nextBoolean(); case GRADER_SHUTDOWN_DATE, HOLIDAY_LIST, BANNER_EXPIRATION-> Instant.now(); }; From 3d9b0f29e3eb43d2a86080e9a56c50326064a2db Mon Sep 17 00:00:00 2001 From: mewilker Date: Fri, 22 May 2026 15:35:17 +0000 Subject: [PATCH 31/34] update line endings from crlf to lf --- .../cs/autograder/test/CoverageAnalyzer.java | 202 ++++---- .../java/edu/byu/cs/model/PrivateConfig.java | 120 ++--- .../request/ConfigPenaltyUpdateRequest.java | 62 +-- .../components/config/PenaltyConfigEditor.vue | 290 +++++------ .../src/network/ServerCommunicator.ts | 462 +++++++++--------- .../frontend/src/services/configService.ts | 176 +++---- .../resources/frontend/src/stores/config.ts | 256 +++++----- .../src/views/AdminView/ConfigView.vue | 456 ++++++++--------- 8 files changed, 1012 insertions(+), 1012 deletions(-) diff --git a/src/main/java/edu/byu/cs/autograder/test/CoverageAnalyzer.java b/src/main/java/edu/byu/cs/autograder/test/CoverageAnalyzer.java index 92420fe50..7c1c72073 100644 --- a/src/main/java/edu/byu/cs/autograder/test/CoverageAnalyzer.java +++ b/src/main/java/edu/byu/cs/autograder/test/CoverageAnalyzer.java @@ -1,101 +1,101 @@ -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. - *
- * Supports Branch and Line coverage. Default coverage is Line Coverage. - */ -public class CoverageAnalyzer { - 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} - * - * @param jacocoCsvOutput the file storing the coverage results from running the tests - * @return a {@link CoverageAnalysis} containing the code coverage results - * @throws GradingException if an issue arose parsing the coverage results - */ - public CoverageAnalysis parse(File jacocoCsvOutput) throws GradingException { - Collection classAnalyses = new HashSet<>(); - if(!jacocoCsvOutput.exists()) { - return new CoverageAnalysis(classAnalyses); - } - - String csv = FileUtils.readStringFromFile(jacocoCsvOutput); - String[] lines = csv.split("\n"); - - Integer classHeader = null; - Integer packageHeader = null; - Integer coverageMissedHeader = null; - Integer coverageCoveredHeader = null; - - String[] headers = lines[0].split(","); - for(int i = 0; i < headers.length; i++) { - String s = headers[i].trim(); - switch (s) { - case CLASS_HEADER -> classHeader = i; - case PACKAGE_HEADER -> packageHeader = i; - default -> { - if (s.equals(this.coverageMissedHeader)){ - coverageMissedHeader = i; - } - else if (s.equals(this.coverageCoveredHeader)) { - coverageCoveredHeader = i; - } - } - } - } - - if(classHeader == null || packageHeader == null || coverageMissedHeader == null || coverageCoveredHeader == null) { - throw new GradingException("Error parsing coverage results"); - } - - for(int i = 1; i < lines.length; i++) { - String[] values = lines[i].split(","); - - String className = values[classHeader].trim(); - String packageName = values[packageHeader].trim(); - int coverageMissed = Integer.parseInt(values[coverageMissedHeader].trim()); - int coverageCovered = Integer.parseInt(values[coverageCoveredHeader].trim()); - - classAnalyses.add(new ClassCoverageAnalysis(className, packageName, coverageCovered, coverageMissed)); - } - - return new CoverageAnalysis(classAnalyses); - } -} +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. + *
+ * Supports Branch and Line coverage. Default coverage is Line Coverage. + */ +public class CoverageAnalyzer { + 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} + * + * @param jacocoCsvOutput the file storing the coverage results from running the tests + * @return a {@link CoverageAnalysis} containing the code coverage results + * @throws GradingException if an issue arose parsing the coverage results + */ + public CoverageAnalysis parse(File jacocoCsvOutput) throws GradingException { + Collection classAnalyses = new HashSet<>(); + if(!jacocoCsvOutput.exists()) { + return new CoverageAnalysis(classAnalyses); + } + + String csv = FileUtils.readStringFromFile(jacocoCsvOutput); + String[] lines = csv.split("\n"); + + Integer classHeader = null; + Integer packageHeader = null; + Integer coverageMissedHeader = null; + Integer coverageCoveredHeader = null; + + String[] headers = lines[0].split(","); + for(int i = 0; i < headers.length; i++) { + String s = headers[i].trim(); + switch (s) { + case CLASS_HEADER -> classHeader = i; + case PACKAGE_HEADER -> packageHeader = i; + default -> { + if (s.equals(this.coverageMissedHeader)){ + coverageMissedHeader = i; + } + else if (s.equals(this.coverageCoveredHeader)) { + coverageCoveredHeader = i; + } + } + } + } + + if(classHeader == null || packageHeader == null || coverageMissedHeader == null || coverageCoveredHeader == null) { + throw new GradingException("Error parsing coverage results"); + } + + for(int i = 1; i < lines.length; i++) { + String[] values = lines[i].split(","); + + String className = values[classHeader].trim(); + String packageName = values[packageHeader].trim(); + int coverageMissed = Integer.parseInt(values[coverageMissedHeader].trim()); + int coverageCovered = Integer.parseInt(values[coverageCoveredHeader].trim()); + + classAnalyses.add(new ClassCoverageAnalysis(className, packageName, coverageCovered, coverageMissed)); + } + + return new CoverageAnalysis(classAnalyses); + } +} diff --git a/src/main/java/edu/byu/cs/model/PrivateConfig.java b/src/main/java/edu/byu/cs/model/PrivateConfig.java index f3b8c1375..114cdd9d5 100644 --- a/src/main/java/edu/byu/cs/model/PrivateConfig.java +++ b/src/main/java/edu/byu/cs/model/PrivateConfig.java @@ -1,60 +1,60 @@ -package edu.byu.cs.model; - -import edu.byu.cs.model.request.ConfigPenaltyUpdateRequest; - -import java.util.List; -import java.util.Map; - -/** - * Represents the configuration values that only admins should see - * - * @param penalty A {@link PenaltyConfig} containing information about penalties - * students may receive - * @param courseNumber The number assigned to the course on Canvas - * @param assignments A list containing information about an assignment in - * Canvas for each {@link AssignmentConfig}. - * @param holidays A list of holidays the AutoGrader doesn't count towards the late penalty - */ -public record PrivateConfig( - PenaltyConfig penalty, - int courseNumber, - List assignments, - String[] holidays -) { - /** - * Represents the configuration information needed for penalties students may receive - * - * @param perDayLatePenalty The penalty to apply on a submission for everyday late - * @param gitCommitPenalty the penalty to apply on a submission for missing commits - * @param maxLateDaysPenalized the maximum number of days the late penalty should apply - * @param linesChangedPerCommit the minimum number of lines needed for a commit to count - * @param clockForgivenessMinutes the number of minutes a commit can be authored - * past the time of submission - * @param coveragePercent the percentage of code coverage expected for a submission to receive full credit - * @param extraCoveragePercent the percentage of code coverage expected for a submission to receive extra credit - * @param coverageType the type of code coverage to use when calculating penalties (branch or line) - */ - public record PenaltyConfig( - float perDayLatePenalty, - float gitCommitPenalty, - int maxLateDaysPenalized, - int linesChangedPerCommit, - int clockForgivenessMinutes, - float coveragePercent, - float extraCoveragePercent, - ConfigPenaltyUpdateRequest.CoverageType coverageType - ){ } - - /** - * Represents the configuration information for a Canvas assignment - * - * @param phase the phase associated with the Canvas assignment - * @param assignmentId the id of the assignment in Canvas - * @param rubricItems the rubric items for the Canvas assignment - */ - public record AssignmentConfig( - Phase phase, - int assignmentId, - Map rubricItems - ){ } -} +package edu.byu.cs.model; + +import edu.byu.cs.model.request.ConfigPenaltyUpdateRequest; + +import java.util.List; +import java.util.Map; + +/** + * Represents the configuration values that only admins should see + * + * @param penalty A {@link PenaltyConfig} containing information about penalties + * students may receive + * @param courseNumber The number assigned to the course on Canvas + * @param assignments A list containing information about an assignment in + * Canvas for each {@link AssignmentConfig}. + * @param holidays A list of holidays the AutoGrader doesn't count towards the late penalty + */ +public record PrivateConfig( + PenaltyConfig penalty, + int courseNumber, + List assignments, + String[] holidays +) { + /** + * Represents the configuration information needed for penalties students may receive + * + * @param perDayLatePenalty The penalty to apply on a submission for everyday late + * @param gitCommitPenalty the penalty to apply on a submission for missing commits + * @param maxLateDaysPenalized the maximum number of days the late penalty should apply + * @param linesChangedPerCommit the minimum number of lines needed for a commit to count + * @param clockForgivenessMinutes the number of minutes a commit can be authored + * past the time of submission + * @param coveragePercent the percentage of code coverage expected for a submission to receive full credit + * @param extraCoveragePercent the percentage of code coverage expected for a submission to receive extra credit + * @param coverageType the type of code coverage to use when calculating penalties (branch or line) + */ + public record PenaltyConfig( + float perDayLatePenalty, + float gitCommitPenalty, + int maxLateDaysPenalized, + int linesChangedPerCommit, + int clockForgivenessMinutes, + float coveragePercent, + float extraCoveragePercent, + ConfigPenaltyUpdateRequest.CoverageType coverageType + ){ } + + /** + * Represents the configuration information for a Canvas assignment + * + * @param phase the phase associated with the Canvas assignment + * @param assignmentId the id of the assignment in Canvas + * @param rubricItems the rubric items for the Canvas assignment + */ + public record AssignmentConfig( + Phase phase, + int assignmentId, + Map rubricItems + ){ } +} diff --git a/src/main/java/edu/byu/cs/model/request/ConfigPenaltyUpdateRequest.java b/src/main/java/edu/byu/cs/model/request/ConfigPenaltyUpdateRequest.java index dc87d2e46..a5b0f2387 100644 --- a/src/main/java/edu/byu/cs/model/request/ConfigPenaltyUpdateRequest.java +++ b/src/main/java/edu/byu/cs/model/request/ConfigPenaltyUpdateRequest.java @@ -1,31 +1,31 @@ -package edu.byu.cs.model.request; - -/** - * Represents a request for updating the values used for calculating penalties - * - * @param perDayLatePenalty the percentage taken off submission for everyday late - * @param maxLateDaysPenalized the maximum number of days the late penalty should apply - * @param gitCommitPenalty the penalty to apply for missing commits - * @param linesChangedPerCommit the minimum number of lines needed for a commit to count - * @param clockForgivenessMinutes the number of minutes a commit can be authored - * past the time of submission - * @param coveragePercent the percentage of code coverage expected for a submission to receive full credit - * @param extraCoveragePercent the percentage of code coverage expected for a submission to receive extra credit - * @param coverageType the type of code coverage to use when calculating penalties (branch or line) - */ -public record ConfigPenaltyUpdateRequest( - float perDayLatePenalty, - int maxLateDaysPenalized, - float gitCommitPenalty, - int linesChangedPerCommit, - int clockForgivenessMinutes, - float coveragePercent, - float extraCoveragePercent, - CoverageType coverageType -) { - public enum CoverageType { - LINE, - BRANCH - } -} - +package edu.byu.cs.model.request; + +/** + * Represents a request for updating the values used for calculating penalties + * + * @param perDayLatePenalty the percentage taken off submission for everyday late + * @param maxLateDaysPenalized the maximum number of days the late penalty should apply + * @param gitCommitPenalty the penalty to apply for missing commits + * @param linesChangedPerCommit the minimum number of lines needed for a commit to count + * @param clockForgivenessMinutes the number of minutes a commit can be authored + * past the time of submission + * @param coveragePercent the percentage of code coverage expected for a submission to receive full credit + * @param extraCoveragePercent the percentage of code coverage expected for a submission to receive extra credit + * @param coverageType the type of code coverage to use when calculating penalties (branch or line) + */ +public record ConfigPenaltyUpdateRequest( + float perDayLatePenalty, + int maxLateDaysPenalized, + float gitCommitPenalty, + int linesChangedPerCommit, + int clockForgivenessMinutes, + float coveragePercent, + float extraCoveragePercent, + CoverageType coverageType +) { + public enum CoverageType { + LINE, + BRANCH + } +} + diff --git a/src/main/resources/frontend/src/components/config/PenaltyConfigEditor.vue b/src/main/resources/frontend/src/components/config/PenaltyConfigEditor.vue index 3f527507a..7874e092b 100644 --- a/src/main/resources/frontend/src/components/config/PenaltyConfigEditor.vue +++ b/src/main/resources/frontend/src/components/config/PenaltyConfigEditor.vue @@ -1,145 +1,145 @@ - - - - - + + + + + diff --git a/src/main/resources/frontend/src/network/ServerCommunicator.ts b/src/main/resources/frontend/src/network/ServerCommunicator.ts index f13070770..72fda3443 100644 --- a/src/main/resources/frontend/src/network/ServerCommunicator.ts +++ b/src/main/resources/frontend/src/network/ServerCommunicator.ts @@ -1,231 +1,231 @@ -import { useAuthStore } from "@/stores/auth"; -import { ServerError } from "@/network/ServerError"; -import { useConfigStore } from "@/stores/config"; - -/** - * Utility for making authenticated HTTP requests to the server with automatic error handling - * and response parsing. - * - * @example - * // GET request expecting a User response - * const user = await ServerCommunicator.getRequest('/api/user'); - * - * // POST request with body, not expecting response - * await ServerCommunicator.postRequest('/api/logs', { event: 'action' }, false); - */ -export const ServerCommunicator = { - getRequest: getRequest, - getRequestGuaranteed: getRequestGuaranteed, - postRequest: postRequest, - doUnprocessedRequest: doUnprocessedRequest, -}; - -/** - * Makes a GET request to the specified endpoint with a guaranteed response. - * This will not throw an error if the server returns an error, but will print it - * to the console. - * @template T - The type of the expected response (when expectResponse is true) - * @param {string} endpoint - The API endpoint to call - * @param {T} errorResponse - The object the method call should return if the server - * returns nothing or responds with a non-2XX code - * @returns {Promise} Promise that resolves to the response data of type T - */ -function getRequestGuaranteed(endpoint: string, errorResponse: T): Promise { - return getRequest(endpoint, true).catch((_error) => Promise.resolve(errorResponse)); -} - -/** - * Makes a GET request to the specified endpoint. - * @template T - The type of the expected response (when expectResponse is true) - * @param {string} endpoint - The API endpoint to call - * @param {boolean} [expectResponse=true] - Whether to expect and parse a response - * @returns {Promise} Promise that resolves to: - * - The response data of type T when expectResponse is true - * - null when expectResponse is false - * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) - * @throws {Error} when expectResponse is true but no response is received - */ -function getRequest(endpoint: string, expectResponse: false): Promise; -/** - * Makes a GET request to the specified endpoint. - * @template T - The type of the expected response (when expectResponse is true) - * @param {string} endpoint - The API endpoint to call - * @param {boolean} [expectResponse=true] - Whether to expect and parse a response - * @returns {Promise} Promise that resolves to: - * - The response data of type T when expectResponse is true - * - null when expectResponse is false - * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) - * @throws {Error} when expectResponse is true but no response is received - */ -function getRequest(endpoint: string, expectResponse?: boolean): Promise; -function getRequest(endpoint: string, expectResponse: boolean = true): Promise { - return doRequest("GET", endpoint, null, expectResponse); -} - -/** - * Makes a POST request to the specified endpoint. - * @template T - The type of the expected response (when expectResponse is true) - * @param {string} endpoint - The API endpoint to call - * @param {Object | null} [bodyObject=null] - The request body object to send (will be sent as JSON) - * @param {boolean} [expectResponse=true] - Whether to expect and parse a response - * @returns {Promise} Promise that resolves to: - * - The response data of type T when expectResponse is true - * - null when expectResponse is false - * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) - * @throws {Error} when expectResponse is true but no response is received - * - * @example - * // With response - * const user = await postRequest('/api/users', { name: 'John' }); - * - * // Without response - * await postRequest('/api/logs', { event: 'action' }, false); - */ -function postRequest( - endpoint: string, - bodyObject: Object | null, - expectResponse: false, -): Promise; -/** - * Makes a POST request to the specified endpoint. - * @template T - The type of the expected response (when expectResponse is true) - * @param {string} endpoint - The API endpoint to call - * @param {Object | null} [bodyObject=null] - The request body object to send (will be sent as JSON) - * @param {boolean} [expectResponse=true] - Whether to expect and parse a response - * @returns {Promise} Promise that resolves to: - * - The response data of type T when expectResponse is true - * - null when expectResponse is false - * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) - * @throws {Error} when expectResponse is true but no response is received - * - * @example - * // With response - * const user = await postRequest('/api/users', { name: 'John' }); - * - * // Without response - * await postRequest('/api/logs', { event: 'action' }, false); - */ -function postRequest( - endpoint: string, - bodyObject?: Object | null, - expectResponse?: boolean, -): Promise; -function postRequest( - endpoint: string, - bodyObject: Object | null = null, - expectResponse: boolean = true, -): Promise { - return doRequest("POST", endpoint, bodyObject, expectResponse); -} -/** - * Internal method to make an HTTP request. - * @template T - The type of the expected response (when expectResponse is true) - * @param {string} method - The HTTP method to use - * @param {string} endpoint - The API endpoint to call - * @param {Object | null} [bodyObject=null] - The request body object to send (will be sent as JSON) - * @param {boolean} [expectResponse=true] - Whether to expect and parse a response - * @returns {Promise} Promise that resolves to: - * - The response data of type T when expectResponse is true - * - null when expectResponse is false - * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) - * @throws {Error} When expectResponse is true but no response is received - * @internal - */ -function doRequest( - method: string, - endpoint: string, - bodyObject: Object | null, - expectResponse: false, -): Promise; -/** - * Internal method to make an HTTP request. - * @template T - The type of the expected response (when expectResponse is true) - * @param {string} method - The HTTP method to use - * @param {string} endpoint - The API endpoint to call - * @param {Object | null} [bodyObject=null] - The request body object to send (will be sent as JSON) - * @param {boolean} [expectResponse=true] - Whether to expect and parse a response - * @returns {Promise} Promise that resolves to: - * - The response data of type T when expectResponse is true - * - null when expectResponse is false - * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) - * @throws {Error} When expectResponse is true but no response is received - * @internal - */ -function doRequest( - method: string, - endpoint: string, - bodyObject?: Object | null, - expectResponse?: boolean, -): Promise; -/** - * Internal method to make an HTTP request. - * @template T - The type of the expected response (when expectResponse is true) - * @param {string} method - The HTTP method to use - * @param {string} endpoint - The API endpoint to call - * @param {Object | null} [bodyObject=null] - The request body object to send (will be sent as JSON) - * @param {boolean} [expectResponse=true] - Whether to expect and parse a response - * @returns {Promise} Promise that resolves to: - * - The response data of type T when expectResponse is true - * - null when expectResponse is false - * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) - * @throws {Error} When expectResponse is true but no response is received - * @internal - */ -async function doRequest( - method: string, - endpoint: string, - bodyObject: Object | null = null, - expectResponse: boolean = true, -): Promise { - const response = await doUnprocessedRequest(method, endpoint, bodyObject); - - if (!expectResponse) { - return null; - } - - const text = await response.text(); - if (text) { - return JSON.parse(text) as T; - } - - if (bodyObject) { - console.error("Body request:", bodyObject); - } - console.error("Response: ", response); - throw new Error(`Expected a response from ${method} call to ${endpoint} but got none`); -} - -/** - * Makes a raw HTTP request to the server with authentication. - * @param {string} method - The HTTP method to use - * @param {string} endpoint - The API endpoint to call - * @param {Object | null} [bodyObject=null] - The request body object to send (will be sent as JSON) - * @returns {Promise} A promise that resolves to the raw fetch response object - * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) - */ -async function doUnprocessedRequest( - method: string, - endpoint: string, - bodyObject: Object | null = null, -): Promise { - const authToken = useAuthStore().token ?? ""; - - const response = await fetch(useConfigStore().backendUrl + endpoint, { - method: method, - credentials: "include", - headers: { - "Content-Type": "application/json", - Authorization: authToken, - }, - body: bodyObject ? JSON.stringify(bodyObject) : null, - }); - - if (!response.ok) { - console.error( - `A ${response.status} error occurred while making a ${method} request to ${endpoint}`, - ); - console.error(response); - throw new ServerError(endpoint, await response.text(), response.status, response.statusText); - } - return response; -} +import { useAuthStore } from "@/stores/auth"; +import { ServerError } from "@/network/ServerError"; +import { useConfigStore } from "@/stores/config"; + +/** + * Utility for making authenticated HTTP requests to the server with automatic error handling + * and response parsing. + * + * @example + * // GET request expecting a User response + * const user = await ServerCommunicator.getRequest('/api/user'); + * + * // POST request with body, not expecting response + * await ServerCommunicator.postRequest('/api/logs', { event: 'action' }, false); + */ +export const ServerCommunicator = { + getRequest: getRequest, + getRequestGuaranteed: getRequestGuaranteed, + postRequest: postRequest, + doUnprocessedRequest: doUnprocessedRequest, +}; + +/** + * Makes a GET request to the specified endpoint with a guaranteed response. + * This will not throw an error if the server returns an error, but will print it + * to the console. + * @template T - The type of the expected response (when expectResponse is true) + * @param {string} endpoint - The API endpoint to call + * @param {T} errorResponse - The object the method call should return if the server + * returns nothing or responds with a non-2XX code + * @returns {Promise} Promise that resolves to the response data of type T + */ +function getRequestGuaranteed(endpoint: string, errorResponse: T): Promise { + return getRequest(endpoint, true).catch((_error) => Promise.resolve(errorResponse)); +} + +/** + * Makes a GET request to the specified endpoint. + * @template T - The type of the expected response (when expectResponse is true) + * @param {string} endpoint - The API endpoint to call + * @param {boolean} [expectResponse=true] - Whether to expect and parse a response + * @returns {Promise} Promise that resolves to: + * - The response data of type T when expectResponse is true + * - null when expectResponse is false + * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) + * @throws {Error} when expectResponse is true but no response is received + */ +function getRequest(endpoint: string, expectResponse: false): Promise; +/** + * Makes a GET request to the specified endpoint. + * @template T - The type of the expected response (when expectResponse is true) + * @param {string} endpoint - The API endpoint to call + * @param {boolean} [expectResponse=true] - Whether to expect and parse a response + * @returns {Promise} Promise that resolves to: + * - The response data of type T when expectResponse is true + * - null when expectResponse is false + * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) + * @throws {Error} when expectResponse is true but no response is received + */ +function getRequest(endpoint: string, expectResponse?: boolean): Promise; +function getRequest(endpoint: string, expectResponse: boolean = true): Promise { + return doRequest("GET", endpoint, null, expectResponse); +} + +/** + * Makes a POST request to the specified endpoint. + * @template T - The type of the expected response (when expectResponse is true) + * @param {string} endpoint - The API endpoint to call + * @param {Object | null} [bodyObject=null] - The request body object to send (will be sent as JSON) + * @param {boolean} [expectResponse=true] - Whether to expect and parse a response + * @returns {Promise} Promise that resolves to: + * - The response data of type T when expectResponse is true + * - null when expectResponse is false + * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) + * @throws {Error} when expectResponse is true but no response is received + * + * @example + * // With response + * const user = await postRequest('/api/users', { name: 'John' }); + * + * // Without response + * await postRequest('/api/logs', { event: 'action' }, false); + */ +function postRequest( + endpoint: string, + bodyObject: Object | null, + expectResponse: false, +): Promise; +/** + * Makes a POST request to the specified endpoint. + * @template T - The type of the expected response (when expectResponse is true) + * @param {string} endpoint - The API endpoint to call + * @param {Object | null} [bodyObject=null] - The request body object to send (will be sent as JSON) + * @param {boolean} [expectResponse=true] - Whether to expect and parse a response + * @returns {Promise} Promise that resolves to: + * - The response data of type T when expectResponse is true + * - null when expectResponse is false + * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) + * @throws {Error} when expectResponse is true but no response is received + * + * @example + * // With response + * const user = await postRequest('/api/users', { name: 'John' }); + * + * // Without response + * await postRequest('/api/logs', { event: 'action' }, false); + */ +function postRequest( + endpoint: string, + bodyObject?: Object | null, + expectResponse?: boolean, +): Promise; +function postRequest( + endpoint: string, + bodyObject: Object | null = null, + expectResponse: boolean = true, +): Promise { + return doRequest("POST", endpoint, bodyObject, expectResponse); +} +/** + * Internal method to make an HTTP request. + * @template T - The type of the expected response (when expectResponse is true) + * @param {string} method - The HTTP method to use + * @param {string} endpoint - The API endpoint to call + * @param {Object | null} [bodyObject=null] - The request body object to send (will be sent as JSON) + * @param {boolean} [expectResponse=true] - Whether to expect and parse a response + * @returns {Promise} Promise that resolves to: + * - The response data of type T when expectResponse is true + * - null when expectResponse is false + * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) + * @throws {Error} When expectResponse is true but no response is received + * @internal + */ +function doRequest( + method: string, + endpoint: string, + bodyObject: Object | null, + expectResponse: false, +): Promise; +/** + * Internal method to make an HTTP request. + * @template T - The type of the expected response (when expectResponse is true) + * @param {string} method - The HTTP method to use + * @param {string} endpoint - The API endpoint to call + * @param {Object | null} [bodyObject=null] - The request body object to send (will be sent as JSON) + * @param {boolean} [expectResponse=true] - Whether to expect and parse a response + * @returns {Promise} Promise that resolves to: + * - The response data of type T when expectResponse is true + * - null when expectResponse is false + * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) + * @throws {Error} When expectResponse is true but no response is received + * @internal + */ +function doRequest( + method: string, + endpoint: string, + bodyObject?: Object | null, + expectResponse?: boolean, +): Promise; +/** + * Internal method to make an HTTP request. + * @template T - The type of the expected response (when expectResponse is true) + * @param {string} method - The HTTP method to use + * @param {string} endpoint - The API endpoint to call + * @param {Object | null} [bodyObject=null] - The request body object to send (will be sent as JSON) + * @param {boolean} [expectResponse=true] - Whether to expect and parse a response + * @returns {Promise} Promise that resolves to: + * - The response data of type T when expectResponse is true + * - null when expectResponse is false + * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) + * @throws {Error} When expectResponse is true but no response is received + * @internal + */ +async function doRequest( + method: string, + endpoint: string, + bodyObject: Object | null = null, + expectResponse: boolean = true, +): Promise { + const response = await doUnprocessedRequest(method, endpoint, bodyObject); + + if (!expectResponse) { + return null; + } + + const text = await response.text(); + if (text) { + return JSON.parse(text) as T; + } + + if (bodyObject) { + console.error("Body request:", bodyObject); + } + console.error("Response: ", response); + throw new Error(`Expected a response from ${method} call to ${endpoint} but got none`); +} + +/** + * Makes a raw HTTP request to the server with authentication. + * @param {string} method - The HTTP method to use + * @param {string} endpoint - The API endpoint to call + * @param {Object | null} [bodyObject=null] - The request body object to send (will be sent as JSON) + * @returns {Promise} A promise that resolves to the raw fetch response object + * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) + */ +async function doUnprocessedRequest( + method: string, + endpoint: string, + bodyObject: Object | null = null, +): Promise { + const authToken = useAuthStore().token ?? ""; + + const response = await fetch(useConfigStore().backendUrl + endpoint, { + method: method, + credentials: "include", + headers: { + "Content-Type": "application/json", + Authorization: authToken, + }, + body: bodyObject ? JSON.stringify(bodyObject) : null, + }); + + if (!response.ok) { + console.error( + `A ${response.status} error occurred while making a ${method} request to ${endpoint}`, + ); + console.error(response); + throw new ServerError(endpoint, await response.text(), response.status, response.statusText); + } + return response; +} diff --git a/src/main/resources/frontend/src/services/configService.ts b/src/main/resources/frontend/src/services/configService.ts index de79afeac..157f1071e 100644 --- a/src/main/resources/frontend/src/services/configService.ts +++ b/src/main/resources/frontend/src/services/configService.ts @@ -1,88 +1,88 @@ -import { type PrivateConfig, type PublicConfig, useConfigStore } from "@/stores/config"; -import { Phase } from "@/types/types"; -import { ServerCommunicator } from "@/network/ServerCommunicator"; - -export const getPublicConfig = async (): Promise => { - return await ServerCommunicator.getRequest("/api/config"); -}; - -export const getAdminConfig = async (): Promise => { - return await ServerCommunicator.getRequest("/api/admin/config"); -}; - -export const setPenalties = async ( - maxLateDaysPenalized: number, - gitCommitPenalty: number, - perDayLatePenalty: number, - linesChangedPerCommit: number, - clockForgivenessMinutes: number, - coveragePercent: number, - extraCoveragePercent: number, - coverageType: 'LINE' | 'BRANCH' -) => { - await doSetConfigItem("/api/admin/config/penalties", { - maxLateDaysPenalized: maxLateDaysPenalized, - gitCommitPenalty: gitCommitPenalty, - perDayLatePenalty: perDayLatePenalty, - linesChangedPerCommit: linesChangedPerCommit, - clockForgivenessMinutes: clockForgivenessMinutes, - coveragePercent: coveragePercent, - extraCoveragePercent: extraCoveragePercent, - coverageType: coverageType, - }); -}; - -export const setBanner = async ( - message: String, - link: String, - color: String, - expirationTimestamp: String, -): Promise => { - await doSetConfigItem("/api/admin/config/banner", { - bannerMessage: message, - bannerLink: link, - bannerColor: color, - bannerExpiration: expirationTimestamp, - }); -}; - -export const setLivePhases = async (phases: Array): Promise => { - await doSetConfigItem("/api/admin/config/phases", { phases: phases }); -}; - -export const setGraderShutdown = async ( - shutdownTimestamp: string, - shutdownWarningHours: number, -): Promise => { - if (shutdownWarningHours < 0) shutdownWarningHours = 0; - - await doSetConfigItem("/api/admin/config/phases/shutdown", { - shutdownTimestamp: shutdownTimestamp, - shutdownWarningMilliseconds: Math.trunc(shutdownWarningHours * 60 * 60 * 1000), // convert to milliseconds - }); -}; - -export const reloadCourseIds = async (): Promise => { - await doSetConfigItem("/api/admin/config/reloadCourseIds", {}); -}; - -export const setCourseId = async (courseNumber: number) => { - await doSetConfigItem("/api/admin/config/courseId", { courseId: courseNumber }); -}; - -export const updateHolidays = async (dates: string[]) => { - await doSetConfigItem("/api/admin/config/holidays", { - holidays: dates, - }); -}; - -export const setSlackLink = async (slackLink: string) => { - await doSetConfigItem("/api/admin/config/slackLink", { - slackLink: slackLink, - }); -}; - -const doSetConfigItem = async (path: string, body: Object): Promise => { - await ServerCommunicator.postRequest(path, body, false); - await useConfigStore().updateConfig(); -}; +import { type PrivateConfig, type PublicConfig, useConfigStore } from "@/stores/config"; +import { Phase } from "@/types/types"; +import { ServerCommunicator } from "@/network/ServerCommunicator"; + +export const getPublicConfig = async (): Promise => { + return await ServerCommunicator.getRequest("/api/config"); +}; + +export const getAdminConfig = async (): Promise => { + return await ServerCommunicator.getRequest("/api/admin/config"); +}; + +export const setPenalties = async ( + maxLateDaysPenalized: number, + gitCommitPenalty: number, + perDayLatePenalty: number, + linesChangedPerCommit: number, + clockForgivenessMinutes: number, + coveragePercent: number, + extraCoveragePercent: number, + coverageType: 'LINE' | 'BRANCH' +) => { + await doSetConfigItem("/api/admin/config/penalties", { + maxLateDaysPenalized: maxLateDaysPenalized, + gitCommitPenalty: gitCommitPenalty, + perDayLatePenalty: perDayLatePenalty, + linesChangedPerCommit: linesChangedPerCommit, + clockForgivenessMinutes: clockForgivenessMinutes, + coveragePercent: coveragePercent, + extraCoveragePercent: extraCoveragePercent, + coverageType: coverageType, + }); +}; + +export const setBanner = async ( + message: String, + link: String, + color: String, + expirationTimestamp: String, +): Promise => { + await doSetConfigItem("/api/admin/config/banner", { + bannerMessage: message, + bannerLink: link, + bannerColor: color, + bannerExpiration: expirationTimestamp, + }); +}; + +export const setLivePhases = async (phases: Array): Promise => { + await doSetConfigItem("/api/admin/config/phases", { phases: phases }); +}; + +export const setGraderShutdown = async ( + shutdownTimestamp: string, + shutdownWarningHours: number, +): Promise => { + if (shutdownWarningHours < 0) shutdownWarningHours = 0; + + await doSetConfigItem("/api/admin/config/phases/shutdown", { + shutdownTimestamp: shutdownTimestamp, + shutdownWarningMilliseconds: Math.trunc(shutdownWarningHours * 60 * 60 * 1000), // convert to milliseconds + }); +}; + +export const reloadCourseIds = async (): Promise => { + await doSetConfigItem("/api/admin/config/reloadCourseIds", {}); +}; + +export const setCourseId = async (courseNumber: number) => { + await doSetConfigItem("/api/admin/config/courseId", { courseId: courseNumber }); +}; + +export const updateHolidays = async (dates: string[]) => { + await doSetConfigItem("/api/admin/config/holidays", { + holidays: dates, + }); +}; + +export const setSlackLink = async (slackLink: string) => { + await doSetConfigItem("/api/admin/config/slackLink", { + slackLink: slackLink, + }); +}; + +const doSetConfigItem = async (path: string, body: Object): Promise => { + await ServerCommunicator.postRequest(path, body, false); + await useConfigStore().updateConfig(); +}; diff --git a/src/main/resources/frontend/src/stores/config.ts b/src/main/resources/frontend/src/stores/config.ts index 556f55363..c290ea74e 100644 --- a/src/main/resources/frontend/src/stores/config.ts +++ b/src/main/resources/frontend/src/stores/config.ts @@ -1,128 +1,128 @@ -import { reactive, readonly, ref } from "vue"; -import { defineStore } from "pinia"; -import { Phase, type RubricInfo, type RubricType } from "@/types/types"; -import { getAdminConfig, getPublicConfig } from "@/services/configService"; -import { useAuthStore } from "@/stores/auth"; - -type ImportMeta = { - VITE_APP_BACKEND_URL: string; -}; - -/** - * Config available to be read by any user - */ -export type PublicConfig = { - banner: { - message: string; - link: string; - color: string; - expiration: string; - }; - - shutdown: { - timestamp: string; - warningMilliseconds: number; - }; - - livePhases: string[]; - - slackLink: string; -}; - -/** - * Config available to be read only by admins - */ -export type PrivateConfig = { - penalty: { - perDayLatePenalty: number; - gitCommitPenalty: number; - maxLateDaysPenalized: number; - linesChangedPerCommit: number; - clockForgivenessMinutes: number; - coveragePercent: number; - extraCoveragePercent: number; - coverageType: 'LINE' | 'BRANCH'; - }; - - courseNumber: number; - assignments: { - phase: Phase; - assignmentId: number; - rubricItems: Map; - }[]; - - holidays: Date[]; -}; - -// @ts-ignore -const env: ImportMeta = import.meta.env; -export const useConfigStore = defineStore("config", () => { - const publicConfig = reactive({ - banner: { - message: "", - link: "", - color: "", - expiration: "", - }, - - shutdown: { - timestamp: "", - warningMilliseconds: 0, - }, - - livePhases: [], - - slackLink: "", - }); - - const privateConfig = reactive({ - penalty: { - perDayLatePenalty: -1, - gitCommitPenalty: -1, - maxLateDaysPenalized: -1, - linesChangedPerCommit: -1, - clockForgivenessMinutes: -1, - coveragePercent: -1, - extraCoveragePercent: -1, - coverageType: 'LINE', - }, - courseNumber: -1, - assignments: [], - holidays: [], - }); - - const updateConfig = async () => { - if (useAuthStore().isLoggedIn) await updateAdminConfig(); - await updatePublicConfig(); - }; - - const updatePublicConfig = async () => { - const latestPublicConfig: PublicConfig = await getPublicConfig(); - - Object.assign(publicConfig, latestPublicConfig); - - // Backend lets the front end choose the default banner color - if (!publicConfig.banner.color) publicConfig.banner.color = "#4fa0ff"; - }; - - const updateAdminConfig = async () => { - const latestAdminConfig = await getAdminConfig(); - - console.log(latestAdminConfig); - - Object.assign(privateConfig, latestAdminConfig); - - console.log(privateConfig); - }; - - const backendUrl = ref(env.VITE_APP_BACKEND_URL); - - return { - updateConfig, - updatePublicConfig, - updateAdminConfig, - backendUrl: readonly(backendUrl), - public: readonly(publicConfig), - admin: readonly(privateConfig), - }; -}); +import { reactive, readonly, ref } from "vue"; +import { defineStore } from "pinia"; +import { Phase, type RubricInfo, type RubricType } from "@/types/types"; +import { getAdminConfig, getPublicConfig } from "@/services/configService"; +import { useAuthStore } from "@/stores/auth"; + +type ImportMeta = { + VITE_APP_BACKEND_URL: string; +}; + +/** + * Config available to be read by any user + */ +export type PublicConfig = { + banner: { + message: string; + link: string; + color: string; + expiration: string; + }; + + shutdown: { + timestamp: string; + warningMilliseconds: number; + }; + + livePhases: string[]; + + slackLink: string; +}; + +/** + * Config available to be read only by admins + */ +export type PrivateConfig = { + penalty: { + perDayLatePenalty: number; + gitCommitPenalty: number; + maxLateDaysPenalized: number; + linesChangedPerCommit: number; + clockForgivenessMinutes: number; + coveragePercent: number; + extraCoveragePercent: number; + coverageType: 'LINE' | 'BRANCH'; + }; + + courseNumber: number; + assignments: { + phase: Phase; + assignmentId: number; + rubricItems: Map; + }[]; + + holidays: Date[]; +}; + +// @ts-ignore +const env: ImportMeta = import.meta.env; +export const useConfigStore = defineStore("config", () => { + const publicConfig = reactive({ + banner: { + message: "", + link: "", + color: "", + expiration: "", + }, + + shutdown: { + timestamp: "", + warningMilliseconds: 0, + }, + + livePhases: [], + + slackLink: "", + }); + + const privateConfig = reactive({ + penalty: { + perDayLatePenalty: -1, + gitCommitPenalty: -1, + maxLateDaysPenalized: -1, + linesChangedPerCommit: -1, + clockForgivenessMinutes: -1, + coveragePercent: -1, + extraCoveragePercent: -1, + coverageType: 'LINE', + }, + courseNumber: -1, + assignments: [], + holidays: [], + }); + + const updateConfig = async () => { + if (useAuthStore().isLoggedIn) await updateAdminConfig(); + await updatePublicConfig(); + }; + + const updatePublicConfig = async () => { + const latestPublicConfig: PublicConfig = await getPublicConfig(); + + Object.assign(publicConfig, latestPublicConfig); + + // Backend lets the front end choose the default banner color + if (!publicConfig.banner.color) publicConfig.banner.color = "#4fa0ff"; + }; + + const updateAdminConfig = async () => { + const latestAdminConfig = await getAdminConfig(); + + console.log(latestAdminConfig); + + Object.assign(privateConfig, latestAdminConfig); + + console.log(privateConfig); + }; + + const backendUrl = ref(env.VITE_APP_BACKEND_URL); + + return { + updateConfig, + updatePublicConfig, + updateAdminConfig, + backendUrl: readonly(backendUrl), + public: readonly(publicConfig), + admin: readonly(privateConfig), + }; +}); diff --git a/src/main/resources/frontend/src/views/AdminView/ConfigView.vue b/src/main/resources/frontend/src/views/AdminView/ConfigView.vue index dc839a655..b4bf946f2 100644 --- a/src/main/resources/frontend/src/views/AdminView/ConfigView.vue +++ b/src/main/resources/frontend/src/views/AdminView/ConfigView.vue @@ -1,228 +1,228 @@ - - - - - + + + + + From 481b0e8497f444e2f958d753bbde2268d1f8f630 Mon Sep 17 00:00:00 2001 From: mewilker Date: Fri, 22 May 2026 09:58:14 -0600 Subject: [PATCH 32/34] format with prettier --- .../components/config/PenaltyConfigEditor.vue | 16 ++++++++-------- .../frontend/src/services/configService.ts | 2 +- src/main/resources/frontend/src/stores/config.ts | 4 ++-- .../frontend/src/views/AdminView/ConfigView.vue | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/resources/frontend/src/components/config/PenaltyConfigEditor.vue b/src/main/resources/frontend/src/components/config/PenaltyConfigEditor.vue index 7874e092b..29dedb2c2 100644 --- a/src/main/resources/frontend/src/components/config/PenaltyConfigEditor.vue +++ b/src/main/resources/frontend/src/components/config/PenaltyConfigEditor.vue @@ -15,8 +15,10 @@ const gitPenalty = ref(Math.round(config.admin.penalty.gitCommitPenalty const linesChangedPerCommit = ref(config.admin.penalty.linesChangedPerCommit); const clockForgivenessMinutes = ref(config.admin.penalty.clockForgivenessMinutes); const coveragePercent = ref(Math.round(config.admin.penalty.coveragePercent * 100)); -const extraCoveragePercent = ref(Math.round(config.admin.penalty.extraCoveragePercent * 100)); -const coverageType = ref<'LINE' | 'BRANCH'>(config.admin.penalty.coverageType); +const extraCoveragePercent = ref( + Math.round(config.admin.penalty.extraCoveragePercent * 100), +); +const coverageType = ref<"LINE" | "BRANCH">(config.admin.penalty.coverageType); const valuesReady = () => { return ( @@ -44,7 +46,7 @@ const submit = async () => { clockForgivenessMinutes.value, coveragePercent.value / 100, extraCoveragePercent.value / 100, - coverageType.value + coverageType.value, ); closeEditor(); @@ -111,11 +113,9 @@ const submit = async () => {

Coverage Type

-

- The type of coverage to be measured. -

-

Line

-

Branch

+

The type of coverage to be measured.

+

Line

+

Branch

diff --git a/src/main/resources/frontend/src/services/configService.ts b/src/main/resources/frontend/src/services/configService.ts index 157f1071e..a7f6044ee 100644 --- a/src/main/resources/frontend/src/services/configService.ts +++ b/src/main/resources/frontend/src/services/configService.ts @@ -18,7 +18,7 @@ export const setPenalties = async ( clockForgivenessMinutes: number, coveragePercent: number, extraCoveragePercent: number, - coverageType: 'LINE' | 'BRANCH' + coverageType: "LINE" | "BRANCH", ) => { await doSetConfigItem("/api/admin/config/penalties", { maxLateDaysPenalized: maxLateDaysPenalized, diff --git a/src/main/resources/frontend/src/stores/config.ts b/src/main/resources/frontend/src/stores/config.ts index c290ea74e..5a5807843 100644 --- a/src/main/resources/frontend/src/stores/config.ts +++ b/src/main/resources/frontend/src/stores/config.ts @@ -41,7 +41,7 @@ export type PrivateConfig = { clockForgivenessMinutes: number; coveragePercent: number; extraCoveragePercent: number; - coverageType: 'LINE' | 'BRANCH'; + coverageType: "LINE" | "BRANCH"; }; courseNumber: number; @@ -84,7 +84,7 @@ export const useConfigStore = defineStore("config", () => { clockForgivenessMinutes: -1, coveragePercent: -1, extraCoveragePercent: -1, - coverageType: 'LINE', + coverageType: "LINE", }, courseNumber: -1, assignments: [], diff --git a/src/main/resources/frontend/src/views/AdminView/ConfigView.vue b/src/main/resources/frontend/src/views/AdminView/ConfigView.vue index b4bf946f2..c4075b25f 100644 --- a/src/main/resources/frontend/src/views/AdminView/ConfigView.vue +++ b/src/main/resources/frontend/src/views/AdminView/ConfigView.vue @@ -170,7 +170,7 @@ onMounted(async () => {

Coverage Type: {{ config.admin.penalty.coverageType.toLocaleLowerCase()}} + >{{ config.admin.penalty.coverageType.toLocaleLowerCase() }}

From 3779d24ce85dcc0ae98a7fee8bcb9d5cdb82ea21 Mon Sep 17 00:00:00 2001 From: mewilker Date: Fri, 22 May 2026 16:07:04 +0000 Subject: [PATCH 33/34] changed line endings --- .../components/config/PenaltyConfigEditor.vue | 290 +++++------ .../src/network/ServerCommunicator.ts | 462 +++++++++--------- .../frontend/src/services/configService.ts | 176 +++---- .../resources/frontend/src/stores/config.ts | 256 +++++----- .../src/views/AdminView/ConfigView.vue | 456 ++++++++--------- 5 files changed, 820 insertions(+), 820 deletions(-) diff --git a/src/main/resources/frontend/src/components/config/PenaltyConfigEditor.vue b/src/main/resources/frontend/src/components/config/PenaltyConfigEditor.vue index 29dedb2c2..e0d98bad7 100644 --- a/src/main/resources/frontend/src/components/config/PenaltyConfigEditor.vue +++ b/src/main/resources/frontend/src/components/config/PenaltyConfigEditor.vue @@ -1,145 +1,145 @@ - - - - - + + + + + diff --git a/src/main/resources/frontend/src/network/ServerCommunicator.ts b/src/main/resources/frontend/src/network/ServerCommunicator.ts index 72fda3443..f13070770 100644 --- a/src/main/resources/frontend/src/network/ServerCommunicator.ts +++ b/src/main/resources/frontend/src/network/ServerCommunicator.ts @@ -1,231 +1,231 @@ -import { useAuthStore } from "@/stores/auth"; -import { ServerError } from "@/network/ServerError"; -import { useConfigStore } from "@/stores/config"; - -/** - * Utility for making authenticated HTTP requests to the server with automatic error handling - * and response parsing. - * - * @example - * // GET request expecting a User response - * const user = await ServerCommunicator.getRequest('/api/user'); - * - * // POST request with body, not expecting response - * await ServerCommunicator.postRequest('/api/logs', { event: 'action' }, false); - */ -export const ServerCommunicator = { - getRequest: getRequest, - getRequestGuaranteed: getRequestGuaranteed, - postRequest: postRequest, - doUnprocessedRequest: doUnprocessedRequest, -}; - -/** - * Makes a GET request to the specified endpoint with a guaranteed response. - * This will not throw an error if the server returns an error, but will print it - * to the console. - * @template T - The type of the expected response (when expectResponse is true) - * @param {string} endpoint - The API endpoint to call - * @param {T} errorResponse - The object the method call should return if the server - * returns nothing or responds with a non-2XX code - * @returns {Promise} Promise that resolves to the response data of type T - */ -function getRequestGuaranteed(endpoint: string, errorResponse: T): Promise { - return getRequest(endpoint, true).catch((_error) => Promise.resolve(errorResponse)); -} - -/** - * Makes a GET request to the specified endpoint. - * @template T - The type of the expected response (when expectResponse is true) - * @param {string} endpoint - The API endpoint to call - * @param {boolean} [expectResponse=true] - Whether to expect and parse a response - * @returns {Promise} Promise that resolves to: - * - The response data of type T when expectResponse is true - * - null when expectResponse is false - * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) - * @throws {Error} when expectResponse is true but no response is received - */ -function getRequest(endpoint: string, expectResponse: false): Promise; -/** - * Makes a GET request to the specified endpoint. - * @template T - The type of the expected response (when expectResponse is true) - * @param {string} endpoint - The API endpoint to call - * @param {boolean} [expectResponse=true] - Whether to expect and parse a response - * @returns {Promise} Promise that resolves to: - * - The response data of type T when expectResponse is true - * - null when expectResponse is false - * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) - * @throws {Error} when expectResponse is true but no response is received - */ -function getRequest(endpoint: string, expectResponse?: boolean): Promise; -function getRequest(endpoint: string, expectResponse: boolean = true): Promise { - return doRequest("GET", endpoint, null, expectResponse); -} - -/** - * Makes a POST request to the specified endpoint. - * @template T - The type of the expected response (when expectResponse is true) - * @param {string} endpoint - The API endpoint to call - * @param {Object | null} [bodyObject=null] - The request body object to send (will be sent as JSON) - * @param {boolean} [expectResponse=true] - Whether to expect and parse a response - * @returns {Promise} Promise that resolves to: - * - The response data of type T when expectResponse is true - * - null when expectResponse is false - * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) - * @throws {Error} when expectResponse is true but no response is received - * - * @example - * // With response - * const user = await postRequest('/api/users', { name: 'John' }); - * - * // Without response - * await postRequest('/api/logs', { event: 'action' }, false); - */ -function postRequest( - endpoint: string, - bodyObject: Object | null, - expectResponse: false, -): Promise; -/** - * Makes a POST request to the specified endpoint. - * @template T - The type of the expected response (when expectResponse is true) - * @param {string} endpoint - The API endpoint to call - * @param {Object | null} [bodyObject=null] - The request body object to send (will be sent as JSON) - * @param {boolean} [expectResponse=true] - Whether to expect and parse a response - * @returns {Promise} Promise that resolves to: - * - The response data of type T when expectResponse is true - * - null when expectResponse is false - * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) - * @throws {Error} when expectResponse is true but no response is received - * - * @example - * // With response - * const user = await postRequest('/api/users', { name: 'John' }); - * - * // Without response - * await postRequest('/api/logs', { event: 'action' }, false); - */ -function postRequest( - endpoint: string, - bodyObject?: Object | null, - expectResponse?: boolean, -): Promise; -function postRequest( - endpoint: string, - bodyObject: Object | null = null, - expectResponse: boolean = true, -): Promise { - return doRequest("POST", endpoint, bodyObject, expectResponse); -} -/** - * Internal method to make an HTTP request. - * @template T - The type of the expected response (when expectResponse is true) - * @param {string} method - The HTTP method to use - * @param {string} endpoint - The API endpoint to call - * @param {Object | null} [bodyObject=null] - The request body object to send (will be sent as JSON) - * @param {boolean} [expectResponse=true] - Whether to expect and parse a response - * @returns {Promise} Promise that resolves to: - * - The response data of type T when expectResponse is true - * - null when expectResponse is false - * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) - * @throws {Error} When expectResponse is true but no response is received - * @internal - */ -function doRequest( - method: string, - endpoint: string, - bodyObject: Object | null, - expectResponse: false, -): Promise; -/** - * Internal method to make an HTTP request. - * @template T - The type of the expected response (when expectResponse is true) - * @param {string} method - The HTTP method to use - * @param {string} endpoint - The API endpoint to call - * @param {Object | null} [bodyObject=null] - The request body object to send (will be sent as JSON) - * @param {boolean} [expectResponse=true] - Whether to expect and parse a response - * @returns {Promise} Promise that resolves to: - * - The response data of type T when expectResponse is true - * - null when expectResponse is false - * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) - * @throws {Error} When expectResponse is true but no response is received - * @internal - */ -function doRequest( - method: string, - endpoint: string, - bodyObject?: Object | null, - expectResponse?: boolean, -): Promise; -/** - * Internal method to make an HTTP request. - * @template T - The type of the expected response (when expectResponse is true) - * @param {string} method - The HTTP method to use - * @param {string} endpoint - The API endpoint to call - * @param {Object | null} [bodyObject=null] - The request body object to send (will be sent as JSON) - * @param {boolean} [expectResponse=true] - Whether to expect and parse a response - * @returns {Promise} Promise that resolves to: - * - The response data of type T when expectResponse is true - * - null when expectResponse is false - * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) - * @throws {Error} When expectResponse is true but no response is received - * @internal - */ -async function doRequest( - method: string, - endpoint: string, - bodyObject: Object | null = null, - expectResponse: boolean = true, -): Promise { - const response = await doUnprocessedRequest(method, endpoint, bodyObject); - - if (!expectResponse) { - return null; - } - - const text = await response.text(); - if (text) { - return JSON.parse(text) as T; - } - - if (bodyObject) { - console.error("Body request:", bodyObject); - } - console.error("Response: ", response); - throw new Error(`Expected a response from ${method} call to ${endpoint} but got none`); -} - -/** - * Makes a raw HTTP request to the server with authentication. - * @param {string} method - The HTTP method to use - * @param {string} endpoint - The API endpoint to call - * @param {Object | null} [bodyObject=null] - The request body object to send (will be sent as JSON) - * @returns {Promise} A promise that resolves to the raw fetch response object - * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) - */ -async function doUnprocessedRequest( - method: string, - endpoint: string, - bodyObject: Object | null = null, -): Promise { - const authToken = useAuthStore().token ?? ""; - - const response = await fetch(useConfigStore().backendUrl + endpoint, { - method: method, - credentials: "include", - headers: { - "Content-Type": "application/json", - Authorization: authToken, - }, - body: bodyObject ? JSON.stringify(bodyObject) : null, - }); - - if (!response.ok) { - console.error( - `A ${response.status} error occurred while making a ${method} request to ${endpoint}`, - ); - console.error(response); - throw new ServerError(endpoint, await response.text(), response.status, response.statusText); - } - return response; -} +import { useAuthStore } from "@/stores/auth"; +import { ServerError } from "@/network/ServerError"; +import { useConfigStore } from "@/stores/config"; + +/** + * Utility for making authenticated HTTP requests to the server with automatic error handling + * and response parsing. + * + * @example + * // GET request expecting a User response + * const user = await ServerCommunicator.getRequest('/api/user'); + * + * // POST request with body, not expecting response + * await ServerCommunicator.postRequest('/api/logs', { event: 'action' }, false); + */ +export const ServerCommunicator = { + getRequest: getRequest, + getRequestGuaranteed: getRequestGuaranteed, + postRequest: postRequest, + doUnprocessedRequest: doUnprocessedRequest, +}; + +/** + * Makes a GET request to the specified endpoint with a guaranteed response. + * This will not throw an error if the server returns an error, but will print it + * to the console. + * @template T - The type of the expected response (when expectResponse is true) + * @param {string} endpoint - The API endpoint to call + * @param {T} errorResponse - The object the method call should return if the server + * returns nothing or responds with a non-2XX code + * @returns {Promise} Promise that resolves to the response data of type T + */ +function getRequestGuaranteed(endpoint: string, errorResponse: T): Promise { + return getRequest(endpoint, true).catch((_error) => Promise.resolve(errorResponse)); +} + +/** + * Makes a GET request to the specified endpoint. + * @template T - The type of the expected response (when expectResponse is true) + * @param {string} endpoint - The API endpoint to call + * @param {boolean} [expectResponse=true] - Whether to expect and parse a response + * @returns {Promise} Promise that resolves to: + * - The response data of type T when expectResponse is true + * - null when expectResponse is false + * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) + * @throws {Error} when expectResponse is true but no response is received + */ +function getRequest(endpoint: string, expectResponse: false): Promise; +/** + * Makes a GET request to the specified endpoint. + * @template T - The type of the expected response (when expectResponse is true) + * @param {string} endpoint - The API endpoint to call + * @param {boolean} [expectResponse=true] - Whether to expect and parse a response + * @returns {Promise} Promise that resolves to: + * - The response data of type T when expectResponse is true + * - null when expectResponse is false + * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) + * @throws {Error} when expectResponse is true but no response is received + */ +function getRequest(endpoint: string, expectResponse?: boolean): Promise; +function getRequest(endpoint: string, expectResponse: boolean = true): Promise { + return doRequest("GET", endpoint, null, expectResponse); +} + +/** + * Makes a POST request to the specified endpoint. + * @template T - The type of the expected response (when expectResponse is true) + * @param {string} endpoint - The API endpoint to call + * @param {Object | null} [bodyObject=null] - The request body object to send (will be sent as JSON) + * @param {boolean} [expectResponse=true] - Whether to expect and parse a response + * @returns {Promise} Promise that resolves to: + * - The response data of type T when expectResponse is true + * - null when expectResponse is false + * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) + * @throws {Error} when expectResponse is true but no response is received + * + * @example + * // With response + * const user = await postRequest('/api/users', { name: 'John' }); + * + * // Without response + * await postRequest('/api/logs', { event: 'action' }, false); + */ +function postRequest( + endpoint: string, + bodyObject: Object | null, + expectResponse: false, +): Promise; +/** + * Makes a POST request to the specified endpoint. + * @template T - The type of the expected response (when expectResponse is true) + * @param {string} endpoint - The API endpoint to call + * @param {Object | null} [bodyObject=null] - The request body object to send (will be sent as JSON) + * @param {boolean} [expectResponse=true] - Whether to expect and parse a response + * @returns {Promise} Promise that resolves to: + * - The response data of type T when expectResponse is true + * - null when expectResponse is false + * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) + * @throws {Error} when expectResponse is true but no response is received + * + * @example + * // With response + * const user = await postRequest('/api/users', { name: 'John' }); + * + * // Without response + * await postRequest('/api/logs', { event: 'action' }, false); + */ +function postRequest( + endpoint: string, + bodyObject?: Object | null, + expectResponse?: boolean, +): Promise; +function postRequest( + endpoint: string, + bodyObject: Object | null = null, + expectResponse: boolean = true, +): Promise { + return doRequest("POST", endpoint, bodyObject, expectResponse); +} +/** + * Internal method to make an HTTP request. + * @template T - The type of the expected response (when expectResponse is true) + * @param {string} method - The HTTP method to use + * @param {string} endpoint - The API endpoint to call + * @param {Object | null} [bodyObject=null] - The request body object to send (will be sent as JSON) + * @param {boolean} [expectResponse=true] - Whether to expect and parse a response + * @returns {Promise} Promise that resolves to: + * - The response data of type T when expectResponse is true + * - null when expectResponse is false + * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) + * @throws {Error} When expectResponse is true but no response is received + * @internal + */ +function doRequest( + method: string, + endpoint: string, + bodyObject: Object | null, + expectResponse: false, +): Promise; +/** + * Internal method to make an HTTP request. + * @template T - The type of the expected response (when expectResponse is true) + * @param {string} method - The HTTP method to use + * @param {string} endpoint - The API endpoint to call + * @param {Object | null} [bodyObject=null] - The request body object to send (will be sent as JSON) + * @param {boolean} [expectResponse=true] - Whether to expect and parse a response + * @returns {Promise} Promise that resolves to: + * - The response data of type T when expectResponse is true + * - null when expectResponse is false + * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) + * @throws {Error} When expectResponse is true but no response is received + * @internal + */ +function doRequest( + method: string, + endpoint: string, + bodyObject?: Object | null, + expectResponse?: boolean, +): Promise; +/** + * Internal method to make an HTTP request. + * @template T - The type of the expected response (when expectResponse is true) + * @param {string} method - The HTTP method to use + * @param {string} endpoint - The API endpoint to call + * @param {Object | null} [bodyObject=null] - The request body object to send (will be sent as JSON) + * @param {boolean} [expectResponse=true] - Whether to expect and parse a response + * @returns {Promise} Promise that resolves to: + * - The response data of type T when expectResponse is true + * - null when expectResponse is false + * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) + * @throws {Error} When expectResponse is true but no response is received + * @internal + */ +async function doRequest( + method: string, + endpoint: string, + bodyObject: Object | null = null, + expectResponse: boolean = true, +): Promise { + const response = await doUnprocessedRequest(method, endpoint, bodyObject); + + if (!expectResponse) { + return null; + } + + const text = await response.text(); + if (text) { + return JSON.parse(text) as T; + } + + if (bodyObject) { + console.error("Body request:", bodyObject); + } + console.error("Response: ", response); + throw new Error(`Expected a response from ${method} call to ${endpoint} but got none`); +} + +/** + * Makes a raw HTTP request to the server with authentication. + * @param {string} method - The HTTP method to use + * @param {string} endpoint - The API endpoint to call + * @param {Object | null} [bodyObject=null] - The request body object to send (will be sent as JSON) + * @returns {Promise} A promise that resolves to the raw fetch response object + * @throws {ServerError} When the request fails (meaning the server returned a code other than 2XX) + */ +async function doUnprocessedRequest( + method: string, + endpoint: string, + bodyObject: Object | null = null, +): Promise { + const authToken = useAuthStore().token ?? ""; + + const response = await fetch(useConfigStore().backendUrl + endpoint, { + method: method, + credentials: "include", + headers: { + "Content-Type": "application/json", + Authorization: authToken, + }, + body: bodyObject ? JSON.stringify(bodyObject) : null, + }); + + if (!response.ok) { + console.error( + `A ${response.status} error occurred while making a ${method} request to ${endpoint}`, + ); + console.error(response); + throw new ServerError(endpoint, await response.text(), response.status, response.statusText); + } + return response; +} diff --git a/src/main/resources/frontend/src/services/configService.ts b/src/main/resources/frontend/src/services/configService.ts index a7f6044ee..53be99266 100644 --- a/src/main/resources/frontend/src/services/configService.ts +++ b/src/main/resources/frontend/src/services/configService.ts @@ -1,88 +1,88 @@ -import { type PrivateConfig, type PublicConfig, useConfigStore } from "@/stores/config"; -import { Phase } from "@/types/types"; -import { ServerCommunicator } from "@/network/ServerCommunicator"; - -export const getPublicConfig = async (): Promise => { - return await ServerCommunicator.getRequest("/api/config"); -}; - -export const getAdminConfig = async (): Promise => { - return await ServerCommunicator.getRequest("/api/admin/config"); -}; - -export const setPenalties = async ( - maxLateDaysPenalized: number, - gitCommitPenalty: number, - perDayLatePenalty: number, - linesChangedPerCommit: number, - clockForgivenessMinutes: number, - coveragePercent: number, - extraCoveragePercent: number, - coverageType: "LINE" | "BRANCH", -) => { - await doSetConfigItem("/api/admin/config/penalties", { - maxLateDaysPenalized: maxLateDaysPenalized, - gitCommitPenalty: gitCommitPenalty, - perDayLatePenalty: perDayLatePenalty, - linesChangedPerCommit: linesChangedPerCommit, - clockForgivenessMinutes: clockForgivenessMinutes, - coveragePercent: coveragePercent, - extraCoveragePercent: extraCoveragePercent, - coverageType: coverageType, - }); -}; - -export const setBanner = async ( - message: String, - link: String, - color: String, - expirationTimestamp: String, -): Promise => { - await doSetConfigItem("/api/admin/config/banner", { - bannerMessage: message, - bannerLink: link, - bannerColor: color, - bannerExpiration: expirationTimestamp, - }); -}; - -export const setLivePhases = async (phases: Array): Promise => { - await doSetConfigItem("/api/admin/config/phases", { phases: phases }); -}; - -export const setGraderShutdown = async ( - shutdownTimestamp: string, - shutdownWarningHours: number, -): Promise => { - if (shutdownWarningHours < 0) shutdownWarningHours = 0; - - await doSetConfigItem("/api/admin/config/phases/shutdown", { - shutdownTimestamp: shutdownTimestamp, - shutdownWarningMilliseconds: Math.trunc(shutdownWarningHours * 60 * 60 * 1000), // convert to milliseconds - }); -}; - -export const reloadCourseIds = async (): Promise => { - await doSetConfigItem("/api/admin/config/reloadCourseIds", {}); -}; - -export const setCourseId = async (courseNumber: number) => { - await doSetConfigItem("/api/admin/config/courseId", { courseId: courseNumber }); -}; - -export const updateHolidays = async (dates: string[]) => { - await doSetConfigItem("/api/admin/config/holidays", { - holidays: dates, - }); -}; - -export const setSlackLink = async (slackLink: string) => { - await doSetConfigItem("/api/admin/config/slackLink", { - slackLink: slackLink, - }); -}; - -const doSetConfigItem = async (path: string, body: Object): Promise => { - await ServerCommunicator.postRequest(path, body, false); - await useConfigStore().updateConfig(); -}; +import { type PrivateConfig, type PublicConfig, useConfigStore } from "@/stores/config"; +import { Phase } from "@/types/types"; +import { ServerCommunicator } from "@/network/ServerCommunicator"; + +export const getPublicConfig = async (): Promise => { + return await ServerCommunicator.getRequest("/api/config"); +}; + +export const getAdminConfig = async (): Promise => { + return await ServerCommunicator.getRequest("/api/admin/config"); +}; + +export const setPenalties = async ( + maxLateDaysPenalized: number, + gitCommitPenalty: number, + perDayLatePenalty: number, + linesChangedPerCommit: number, + clockForgivenessMinutes: number, + coveragePercent: number, + extraCoveragePercent: number, + coverageType: "LINE" | "BRANCH", +) => { + await doSetConfigItem("/api/admin/config/penalties", { + maxLateDaysPenalized: maxLateDaysPenalized, + gitCommitPenalty: gitCommitPenalty, + perDayLatePenalty: perDayLatePenalty, + linesChangedPerCommit: linesChangedPerCommit, + clockForgivenessMinutes: clockForgivenessMinutes, + coveragePercent: coveragePercent, + extraCoveragePercent: extraCoveragePercent, + coverageType: coverageType, + }); +}; + +export const setBanner = async ( + message: String, + link: String, + color: String, + expirationTimestamp: String, +): Promise => { + await doSetConfigItem("/api/admin/config/banner", { + bannerMessage: message, + bannerLink: link, + bannerColor: color, + bannerExpiration: expirationTimestamp, + }); +}; + +export const setLivePhases = async (phases: Array): Promise => { + await doSetConfigItem("/api/admin/config/phases", { phases: phases }); +}; + +export const setGraderShutdown = async ( + shutdownTimestamp: string, + shutdownWarningHours: number, +): Promise => { + if (shutdownWarningHours < 0) shutdownWarningHours = 0; + + await doSetConfigItem("/api/admin/config/phases/shutdown", { + shutdownTimestamp: shutdownTimestamp, + shutdownWarningMilliseconds: Math.trunc(shutdownWarningHours * 60 * 60 * 1000), // convert to milliseconds + }); +}; + +export const reloadCourseIds = async (): Promise => { + await doSetConfigItem("/api/admin/config/reloadCourseIds", {}); +}; + +export const setCourseId = async (courseNumber: number) => { + await doSetConfigItem("/api/admin/config/courseId", { courseId: courseNumber }); +}; + +export const updateHolidays = async (dates: string[]) => { + await doSetConfigItem("/api/admin/config/holidays", { + holidays: dates, + }); +}; + +export const setSlackLink = async (slackLink: string) => { + await doSetConfigItem("/api/admin/config/slackLink", { + slackLink: slackLink, + }); +}; + +const doSetConfigItem = async (path: string, body: Object): Promise => { + await ServerCommunicator.postRequest(path, body, false); + await useConfigStore().updateConfig(); +}; diff --git a/src/main/resources/frontend/src/stores/config.ts b/src/main/resources/frontend/src/stores/config.ts index 5a5807843..67813aabe 100644 --- a/src/main/resources/frontend/src/stores/config.ts +++ b/src/main/resources/frontend/src/stores/config.ts @@ -1,128 +1,128 @@ -import { reactive, readonly, ref } from "vue"; -import { defineStore } from "pinia"; -import { Phase, type RubricInfo, type RubricType } from "@/types/types"; -import { getAdminConfig, getPublicConfig } from "@/services/configService"; -import { useAuthStore } from "@/stores/auth"; - -type ImportMeta = { - VITE_APP_BACKEND_URL: string; -}; - -/** - * Config available to be read by any user - */ -export type PublicConfig = { - banner: { - message: string; - link: string; - color: string; - expiration: string; - }; - - shutdown: { - timestamp: string; - warningMilliseconds: number; - }; - - livePhases: string[]; - - slackLink: string; -}; - -/** - * Config available to be read only by admins - */ -export type PrivateConfig = { - penalty: { - perDayLatePenalty: number; - gitCommitPenalty: number; - maxLateDaysPenalized: number; - linesChangedPerCommit: number; - clockForgivenessMinutes: number; - coveragePercent: number; - extraCoveragePercent: number; - coverageType: "LINE" | "BRANCH"; - }; - - courseNumber: number; - assignments: { - phase: Phase; - assignmentId: number; - rubricItems: Map; - }[]; - - holidays: Date[]; -}; - -// @ts-ignore -const env: ImportMeta = import.meta.env; -export const useConfigStore = defineStore("config", () => { - const publicConfig = reactive({ - banner: { - message: "", - link: "", - color: "", - expiration: "", - }, - - shutdown: { - timestamp: "", - warningMilliseconds: 0, - }, - - livePhases: [], - - slackLink: "", - }); - - const privateConfig = reactive({ - penalty: { - perDayLatePenalty: -1, - gitCommitPenalty: -1, - maxLateDaysPenalized: -1, - linesChangedPerCommit: -1, - clockForgivenessMinutes: -1, - coveragePercent: -1, - extraCoveragePercent: -1, - coverageType: "LINE", - }, - courseNumber: -1, - assignments: [], - holidays: [], - }); - - const updateConfig = async () => { - if (useAuthStore().isLoggedIn) await updateAdminConfig(); - await updatePublicConfig(); - }; - - const updatePublicConfig = async () => { - const latestPublicConfig: PublicConfig = await getPublicConfig(); - - Object.assign(publicConfig, latestPublicConfig); - - // Backend lets the front end choose the default banner color - if (!publicConfig.banner.color) publicConfig.banner.color = "#4fa0ff"; - }; - - const updateAdminConfig = async () => { - const latestAdminConfig = await getAdminConfig(); - - console.log(latestAdminConfig); - - Object.assign(privateConfig, latestAdminConfig); - - console.log(privateConfig); - }; - - const backendUrl = ref(env.VITE_APP_BACKEND_URL); - - return { - updateConfig, - updatePublicConfig, - updateAdminConfig, - backendUrl: readonly(backendUrl), - public: readonly(publicConfig), - admin: readonly(privateConfig), - }; -}); +import { reactive, readonly, ref } from "vue"; +import { defineStore } from "pinia"; +import { Phase, type RubricInfo, type RubricType } from "@/types/types"; +import { getAdminConfig, getPublicConfig } from "@/services/configService"; +import { useAuthStore } from "@/stores/auth"; + +type ImportMeta = { + VITE_APP_BACKEND_URL: string; +}; + +/** + * Config available to be read by any user + */ +export type PublicConfig = { + banner: { + message: string; + link: string; + color: string; + expiration: string; + }; + + shutdown: { + timestamp: string; + warningMilliseconds: number; + }; + + livePhases: string[]; + + slackLink: string; +}; + +/** + * Config available to be read only by admins + */ +export type PrivateConfig = { + penalty: { + perDayLatePenalty: number; + gitCommitPenalty: number; + maxLateDaysPenalized: number; + linesChangedPerCommit: number; + clockForgivenessMinutes: number; + coveragePercent: number; + extraCoveragePercent: number; + coverageType: "LINE" | "BRANCH"; + }; + + courseNumber: number; + assignments: { + phase: Phase; + assignmentId: number; + rubricItems: Map; + }[]; + + holidays: Date[]; +}; + +// @ts-ignore +const env: ImportMeta = import.meta.env; +export const useConfigStore = defineStore("config", () => { + const publicConfig = reactive({ + banner: { + message: "", + link: "", + color: "", + expiration: "", + }, + + shutdown: { + timestamp: "", + warningMilliseconds: 0, + }, + + livePhases: [], + + slackLink: "", + }); + + const privateConfig = reactive({ + penalty: { + perDayLatePenalty: -1, + gitCommitPenalty: -1, + maxLateDaysPenalized: -1, + linesChangedPerCommit: -1, + clockForgivenessMinutes: -1, + coveragePercent: -1, + extraCoveragePercent: -1, + coverageType: "LINE", + }, + courseNumber: -1, + assignments: [], + holidays: [], + }); + + const updateConfig = async () => { + if (useAuthStore().isLoggedIn) await updateAdminConfig(); + await updatePublicConfig(); + }; + + const updatePublicConfig = async () => { + const latestPublicConfig: PublicConfig = await getPublicConfig(); + + Object.assign(publicConfig, latestPublicConfig); + + // Backend lets the front end choose the default banner color + if (!publicConfig.banner.color) publicConfig.banner.color = "#4fa0ff"; + }; + + const updateAdminConfig = async () => { + const latestAdminConfig = await getAdminConfig(); + + console.log(latestAdminConfig); + + Object.assign(privateConfig, latestAdminConfig); + + console.log(privateConfig); + }; + + const backendUrl = ref(env.VITE_APP_BACKEND_URL); + + return { + updateConfig, + updatePublicConfig, + updateAdminConfig, + backendUrl: readonly(backendUrl), + public: readonly(publicConfig), + admin: readonly(privateConfig), + }; +}); diff --git a/src/main/resources/frontend/src/views/AdminView/ConfigView.vue b/src/main/resources/frontend/src/views/AdminView/ConfigView.vue index c4075b25f..bd1bbcad4 100644 --- a/src/main/resources/frontend/src/views/AdminView/ConfigView.vue +++ b/src/main/resources/frontend/src/views/AdminView/ConfigView.vue @@ -1,228 +1,228 @@ - - - - - + + + + + From ad99d710ceb7bb1ee4641e59bd7b10e9537d4a59 Mon Sep 17 00:00:00 2001 From: mewilker Date: Fri, 22 May 2026 12:26:12 -0600 Subject: [PATCH 34/34] fix percent comparisons --- src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java b/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java index f8e647006..ce47645a0 100644 --- a/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java +++ b/src/main/java/edu/byu/cs/autograder/test/UnitTestGrader.java @@ -80,10 +80,10 @@ protected float getScore(TestOutput testOutput) throws GradingException { return 0; } - if (coveragePercent > extraCreditPercent) { + if (coveragePercent >= extraCreditPercent) { return 1.05F; } - if (coveragePercent > targetPercent){ + if (coveragePercent >= targetPercent){ return 1; }