Skip to content

Commit 77f0704

Browse files
committed
Add BugFixes stage for marking bug fixes using specified formula
1 parent 040ed13 commit 77f0704

5 files changed

Lines changed: 279 additions & 2 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package chalkbox.commands;
2+
3+
import chalkbox.config.Config;
4+
import chalkbox.stages.StageException;
5+
import com.google.common.flogger.FluentLogger;
6+
import picocli.CommandLine.Command;
7+
import picocli.CommandLine.Mixin;
8+
9+
import java.nio.file.Path;
10+
import java.nio.file.Paths;
11+
12+
@Command(name = "bugfixes",
13+
description = "Runs bug fixing marking")
14+
public class BugFixes implements Runnable {
15+
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
16+
17+
@Mixin Shared shared = new Shared();
18+
19+
@Override
20+
public void run() {
21+
Path configFile = Paths.get(shared.configFile);
22+
var config = new Config(configFile);
23+
24+
var solution = config.toSolution();
25+
var submission = config.toSubmission();
26+
var stage = config.toBugFixes();
27+
try {
28+
var result = stage.run(submission, solution);
29+
logger.atInfo().log("Bug Fixes Run");
30+
logger.atInfo().log(result.overview().getOutput());
31+
for (var inner : result.results()) {
32+
logger.atInfo().log(inner.getOutput());
33+
}
34+
} catch (StageException e) {
35+
logger.atSevere().log(e.toString());
36+
System.exit(0);
37+
} catch (Exception e) {
38+
throw new RuntimeException(e);
39+
}
40+
}
41+
}

src/main/java/chalkbox/commands/Grade.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import chalkbox.config.Config;
44
import chalkbox.stages.*;
5-
import chalkbox.stages.ai.AI;
65
import chalkbox.stages.header.Header;
76
import com.google.common.flogger.FluentLogger;
87
import com.google.gson.GsonBuilder;
@@ -85,6 +84,7 @@ private Stage getStage(String name, Config config) {
8584
case "conformance" -> config.toConformance();
8685
case "functionality" -> config.toFunctionality();
8786
case "pracdemo" -> config.toPracDemo();
87+
case "bugfixes" -> config.toBugFixes();
8888
case "mutation" -> config.toMutation();
8989
case "tlc" -> config.toTLC();
9090
default -> null;

src/main/java/chalkbox/commands/Run.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
@Command(name = "run",
77
description = "Run one of the supported commands: CheckStyle, Conformance, Functionality, Mutation",
8-
subcommands = { Grade.class, Conformance.class, CodeStyle.class, Functionality.class, PracDemo.class, Mutation.class, TLC.class, CommandLine.HelpCommand.class })
8+
subcommands = { Grade.class, Conformance.class, CodeStyle.class, Functionality.class, PracDemo.class, BugFixes.class, Mutation.class, TLC.class, CommandLine.HelpCommand.class })
99
public class Run implements Runnable {
1010

1111
@Override

src/main/java/chalkbox/config/Config.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import chalkbox.source.Solution;
44
import chalkbox.source.Submission;
55
import chalkbox.stages.ai.AI;
6+
import chalkbox.stages.bugfixes.BugFixes;
67
import chalkbox.stages.functionality.Functionality;
78
import chalkbox.stages.codestyle.CodeStyle;
89
import chalkbox.stages.conformance.Conformance;
@@ -85,6 +86,18 @@ public PracDemo toPracDemo() throws ConfigException {
8586
);
8687
}
8788

89+
public BugFixes toBugFixes() throws ConfigException {
90+
try {
91+
return new BugFixes(
92+
gestalt.getConfig("bugfixes.weighting", Double.class),
93+
gestalt.getConfig("bugfixes.providedPassing", Double.class),
94+
gestalt.getConfig("bugfixes.providedFailing", Double.class)
95+
);
96+
} catch (GestaltException e) {
97+
throw new ConfigException(e.toString());
98+
}
99+
}
100+
88101
public Mutation toMutation() throws ConfigException {
89102
try {
90103
return new Mutation(
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
package chalkbox.stages.bugfixes;
2+
3+
import chalkbox.api.common.java.JUnitIndividualResult;
4+
import chalkbox.api.common.java.JUnitRunner;
5+
import chalkbox.source.Solution;
6+
import chalkbox.source.Submission;
7+
import chalkbox.stages.*;
8+
import chalkbox.stages.conformance.SourceLoader;
9+
import chalkbox.stages.functionality.ClassResult;
10+
11+
import java.io.File;
12+
import java.io.IOException;
13+
import java.nio.file.Files;
14+
import java.nio.file.Path;
15+
import java.util.*;
16+
17+
/**
18+
* Bug fixes differs from functionality in that there is a baseline
19+
* number of passing tests so the formula is
20+
* M = Max(0, (P - Bp)/Bf)
21+
* where Bp is the number of passing tests in the provided code,
22+
* Bf is the number of failing tests in the provided code, and
23+
* P is the number of tests that pass in the submission.
24+
*/
25+
// TODO: This should really share as much of Functionality as possible
26+
public class BugFixes implements Stage {
27+
28+
public final static String name = "BugFixes";
29+
30+
private final double weighting;
31+
private final double providedPassing;
32+
private final double providedFailing;
33+
34+
public BugFixes(double weighting, double providedPassing, double providedFailing) {
35+
this.weighting = weighting;
36+
this.providedPassing = providedPassing;
37+
this.providedFailing = providedFailing;
38+
}
39+
40+
@Override
41+
public String getName() {
42+
return name;
43+
}
44+
45+
@Override
46+
public Type getType() {
47+
return Type.SUBMISSION_AND_SOLUTION;
48+
}
49+
50+
@Override
51+
public StageResult run(Submission submission) throws StageException {
52+
// Not implemented
53+
return null;
54+
}
55+
56+
@Override
57+
public StageResult run(Submission submission, List<Solution> solutions) throws StageException {
58+
// Not implemented
59+
return null;
60+
}
61+
62+
/**
63+
* Run the tests on a submission.
64+
* <p>
65+
* If there were issues compiling the sample solution or the tests, or
66+
* the submission did not compile successfully, no action is taken.
67+
* <p>
68+
* Uses a JUnit listener to observe the passed/failed tests for each test
69+
* class. One Gradescope test is created for each JUnit test method, with
70+
* a mark of zero if the test failed, or a mark of
71+
* <code>stageWeighting / numTests</code> if the test passed, where
72+
* <code>stageWeighting</code> is the number of marks allocated to this
73+
* stage, and <code>numTests</code> is the total number of JUnit test
74+
* methods in all test classes.
75+
*/
76+
@Override
77+
public StageResult run(Submission submission, Solution solution) throws StageException {
78+
// Compile the solution, tests and the submission
79+
try {
80+
var compilation = solution.compileSrc();
81+
if (!compilation.success()) {
82+
throw new StageException("Unable to compile solution: " + compilation.output());
83+
}
84+
compilation = solution.compileTest();
85+
if (!compilation.success()) {
86+
throw new StageException("Unable to compile tests: " + compilation.output());
87+
}
88+
compilation = submission.compileSrc();
89+
if (!compilation.success()) {
90+
throw new StageException("Unable to compile submission: " + compilation.output());
91+
}
92+
} catch (IOException e) {
93+
throw new StageException(e);
94+
}
95+
96+
List<String> tests = null;
97+
try {
98+
tests = solution.getTestClasses();
99+
} catch (IOException e) {
100+
throw new StageException(e.toString());
101+
}
102+
103+
// Run tests against the solution
104+
var classPath = solution.getClassPath() +
105+
File.pathSeparator + solution.getSrcBuildPath() +
106+
File.pathSeparator + solution.getTestBuildPath();
107+
var baselineResults = this.runTests(tests, classPath);
108+
109+
// Path contains dependencies and the compile submission
110+
classPath = solution.getClassPath() +
111+
File.pathSeparator + submission.getSrcBuildPath() +
112+
File.pathSeparator + solution.getTestBuildPath();
113+
var submissionResults = this.runTests(tests, classPath);
114+
115+
var totalNumTests = 0;
116+
var innerResults = new ArrayList<Result>();
117+
var classResults = new ArrayList<ClassResult>();
118+
119+
for (String className : tests) {
120+
if (!className.endsWith("Test")) {
121+
continue;
122+
}
123+
124+
int classPassing = 0;
125+
126+
// Use test summaries to collect information even if test fails to compile
127+
var classTests = baselineResults.get(className).size();
128+
var classWeighting = baselineResults.get(className).getFirst().classWeight();
129+
130+
for (JUnitIndividualResult unit : submissionResults.get(className)) {
131+
var isPassing = unit.passes() == 1;
132+
var visibility = Visibility.VISIBLE;
133+
var unitResult = new Result("Provided Tests: " + unit.name())
134+
.setVisibility(visibility)
135+
.setStatus(isPassing ? Status.PASSED : Status.FAILED);
136+
137+
if (!isPassing) {
138+
unitResult.appendOutput("❌ Test scenario fails\n");
139+
140+
// Get Test class JavaDoc
141+
var testDescription = getTestJavaDoc(solution.getTestBuildPath(), className, unit.name());
142+
if (!testDescription.isEmpty()) {
143+
unitResult.appendOutput("### Scenario\n");
144+
unitResult.appendOutput(testDescription);
145+
}
146+
147+
unitResult.appendOutput("### Details\n");
148+
unitResult.appendOutput(unit.output());
149+
}
150+
151+
var testMultiplier = (Integer) unit.weight();
152+
// e.g. a test worth 5 "units" will increase the total number of tests by 5
153+
totalNumTests += testMultiplier;
154+
innerResults.add(unitResult);
155+
classPassing += unit.passes() == 1 ? 1 : 0;
156+
}
157+
classResults.add(new ClassResult(className, classTests, classPassing, classWeighting, submissionResults.get(className).size()));
158+
}
159+
160+
if (totalNumTests == 0) {
161+
// todo(mh): Do something better here
162+
return null;
163+
}
164+
165+
double total = 0;
166+
double possible = 0;
167+
for (var classResult : classResults) {
168+
if (classResult.count() <= 0) {
169+
continue;
170+
}
171+
total += classResult.passing();
172+
possible += classResult.count();
173+
}
174+
double scaled = Math.max(0, (total - providedPassing) / providedPassing);
175+
176+
String message = "When provided, " + providedPassing + " tests passed and " + providedFailing + " tests failed.\n";
177+
message += "Now " + total + " tests pass and " + (possible - total) + " tests fail.";
178+
179+
var equation = "\n$$\nresult = \\dfrac{" + total + " - " + providedPassing + "}{" + providedFailing + "} = " + scaled + "\n$$";
180+
var overview = new Result(name);
181+
overview.setScore(scaled * (weighting/100.0))
182+
.setMaxScore(weighting)
183+
.appendOutput(message + equation)
184+
.setOutputFormat("md")
185+
.setVisibility(Visibility.AFTER_PUBLISHED);
186+
187+
return new StageResult(overview, innerResults);
188+
}
189+
190+
private Map<String, List<JUnitIndividualResult>> runTests(List<String> tests, String classPath) {
191+
var collection = new HashMap<String, List<JUnitIndividualResult>>();
192+
for (String className : tests) {
193+
// Ignore any that dont end in TEST
194+
if (!className.endsWith("Test")) {
195+
continue;
196+
}
197+
198+
var results = JUnitRunner.runTests(className, classPath);
199+
if (results.isEmpty()) {
200+
continue;
201+
}
202+
results.sort(Comparator.comparing(JUnitIndividualResult::name));
203+
collection.put(className, results);
204+
}
205+
return collection;
206+
}
207+
208+
private String getTestJavaDoc(String folder, String className, String methodName) {
209+
try {
210+
var testDescription = new StringBuilder();
211+
var javaDoc = new SourceLoader(folder).getTestJavadoc(className);
212+
for (var method : javaDoc.getMethods()) {
213+
if (method.getName().equals(methodName.split("\\.")[1])) {
214+
testDescription.append(method.getComment()).append("\n");
215+
}
216+
}
217+
return testDescription.toString();
218+
} catch (IOException ignored) {
219+
// Do Nothing
220+
}
221+
return "";
222+
}
223+
}

0 commit comments

Comments
 (0)