Skip to content

Commit

Permalink
Merge pull request #8 from espertus/group-ratings
Browse files Browse the repository at this point in the history
Improves output for JUnit tests, including grouping results
  • Loading branch information
espertus authored Jan 21, 2024
2 parents 13cbc4b + 46ba4d0 commit 3d1b495
Show file tree
Hide file tree
Showing 9 changed files with 309 additions and 18 deletions.
39 changes: 37 additions & 2 deletions src/main/java/com/spertus/jacquard/common/Result.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.spertus.jacquard.common;

import com.google.common.annotations.VisibleForTesting;

import java.util.*;
import java.util.stream.Collectors;

Expand All @@ -8,6 +10,9 @@
*/
@SuppressWarnings("PMD.TooManyMethods")
public class Result {
private static final int MAX_MESSAGE_LENGTH = 8192;
private static final String MESSAGE_OVERFLOW_INDICATOR = "...";

private final String name;
private final double score;
private final double maxScore;
Expand All @@ -32,10 +37,20 @@ public Result(
this.name = name;
this.score = score;
this.maxScore = maxScore;
this.message = message;
this.message = trimMessage(message, MAX_MESSAGE_LENGTH, MESSAGE_OVERFLOW_INDICATOR);
this.visibility = visibility;
}

@VisibleForTesting
static String trimMessage(final String message, int maxLength, String overflowIndicator) {
if (message.length() > maxLength) {
return message.substring(0, maxLength - overflowIndicator.length())
+ overflowIndicator;
} else {
return message;
}
}

/**
* Creates a result with the visibility level specified in
* {@link Autograder#visibility}.
Expand Down Expand Up @@ -146,7 +161,8 @@ public static Result makeError(final String name, final Throwable throwable) {
}

/**
* Makes a result with the provided score.
* Makes a result with the provided score and message with the visibility level specified in
* {@link Autograder#visibility}.
*
* @param name the name
* @param actualScore the number of points earned
Expand All @@ -162,6 +178,25 @@ public static Result makeResult(
return new Result(name, actualScore, maxScore, message);
}

/**
* Makes a result with the provided characteristics.
*
* @param name the name
* @param actualScore the number of points earned
* @param maxScore the number of points possible
* @param message any message
* @param visibility the visibility
* @return a result
*/
public static Result makeResult(
final String name,
final double actualScore,
final double maxScore,
final String message,
final Visibility visibility) {
return new Result(name, actualScore, maxScore, message, visibility);
}

/**
* Makes a result indicating a total success.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@
*/
String name() default "";

/**
* A description of the defect detected by this test.
*
* @return the description
*/
String description() default "";

/**
* The number of points the test is worth.
*
Expand Down
74 changes: 61 additions & 13 deletions src/main/java/com/spertus/jacquard/junittester/JUnitTester.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.spertus.jacquard.junittester;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.spertus.jacquard.common.*;

import org.junit.platform.engine.*;
Expand All @@ -11,6 +13,7 @@
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;

import static org.junit.platform.engine.discovery.DiscoverySelectors.selectPackage;
import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request;
Expand Down Expand Up @@ -63,11 +66,47 @@ public List<Result> run() {
}
launcher.execute(builder.build());
System.setOut(originalOut);
return listener.results;
return processResults(listener.results);
}

// Note that the merged result has the default visibility set when the autograder was constructed.
@VisibleForTesting
static Result mergeResults(List<Result> results) {
Preconditions.checkArgument(!results.isEmpty());
if (results.size() == 1) {
return results.get(0);
}
String name = results.get(0).getName();
Preconditions.checkArgument(
results.stream().allMatch((r) -> r.getName().equals(name)));

double score = results.stream().mapToDouble(Result::getScore).sum();
double maxScore = results.stream().mapToDouble(Result::getMaxScore).sum();
String message = results.stream()
.filter((r) -> r.getScore() < r.getMaxScore())
.map(r -> String.format(Locale.US,
"%.1f: %s",
r.getScore() - r.getMaxScore(),
r.getMessage()))
.collect(Collectors.joining("\n"));

return Result.makeResult(name, score, maxScore, message);
}

// Merge all the results having the same name. The merged result has the
// default visibility level specified when creating the autograder.
private static List<Result> processResults(List<Result> results) {
return results.stream()
.collect(Collectors.groupingBy(Result::getName))
.values()
.stream()
.map(JUnitTester::mergeResults)
.collect(Collectors.toList());
}

private static class Listener implements TestExecutionListener { // NOPMD
private final List<Result> results = new ArrayList<>();
// ArrayList is used so we know it's mutable.
private final ArrayList<Result> results = new ArrayList<>();
// These get set in executionStarted and used/closed in executionFinished.
private PrintStream ps;
private ByteArrayOutputStream baos;
Expand All @@ -82,16 +121,25 @@ public void executionStarted(final TestIdentifier testIdentifier) {
System.setOut(ps);
}

private String makeOutput(final TestExecutionResult teResult) {
final Optional<Throwable> throwable = teResult.getThrowable();
final String s = baos.toString();
if (throwable.isEmpty()) {
return s;
} else if (s.isEmpty()) {
return throwable.get().toString();
} else {
return s + "\n" + throwable.get();
private String makeMessage(final GradedTest gt, final TestExecutionResult teResult) {
final List<String> items = new ArrayList<>();

// First, use description, if present.
if (!gt.description().isEmpty()) {
items.add(gt.description());
}

// Second, use throwable, if present.
teResult.getThrowable().ifPresent(value -> items.add(value.toString()));

// Third, include output, if present and supposed to be shown.
final String output = baos.toString().trim();
if (gt.includeOutput() && !output.isEmpty()) {
items.add("OUTPUT");
items.add("======");
items.add(output);
}
return String.join("\n", items);
}

@Override
Expand All @@ -107,9 +155,9 @@ public void executionFinished(
try {
final Result result = switch (testExecutionResult.getStatus()) {
case SUCCESSFUL ->
Result.makeSuccess(name, gt.points(), baos.toString());
Result.makeSuccess(name, gt.points(), makeMessage(gt, testExecutionResult));
case FAILED, ABORTED ->
Result.makeFailure(name, gt.points(), makeOutput(testExecutionResult));
Result.makeFailure(name, gt.points(), makeMessage(gt, testExecutionResult));
};
results.add(result.changeVisibility(gt.visibility()));
ps.close();
Expand Down
28 changes: 27 additions & 1 deletion src/test/java/com/spertus/jacquard/JUnitTesterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.spertus.jacquard.common.*;
import com.spertus.jacquard.junittester.SampleTest;
import com.spertus.jacquard.junittester.JUnitTester;
import com.spertus.jacquard.junittester.group.GroupTest;
import com.spertus.jacquard.junittester.output.OutputTest;
import com.spertus.jacquard.junittester.visibility.VisibilityLevelsTest;
import org.junit.jupiter.api.*;

Expand Down Expand Up @@ -60,7 +62,12 @@ public void testPackageExcludingSubpackages() {
public void testPackageIncludingSubpackages() {
JUnitTester tester = new JUnitTester("com.spertus.jacquard.junittester", true);
List<Result> results = tester.run();
assertEquals(7, results.size());
// There should be:
// 1 result from GroupTest
// 2 from OutputTest
// 2 results from SampleTest
// 5 results from VisibilityTest
assertEquals(10, results.size());
}

@Test
Expand All @@ -81,4 +88,23 @@ public void testVisibility() {
"Visibility incorrect for " + result.getName());
}
}

@Test
public void testGrouping() {
JUnitTester tester = new JUnitTester(GroupTest.class);
List<Result> results = tester.run();
assertEquals(1, results.size());
}

@Test
public void testOutput() {
JUnitTester tester = new JUnitTester(OutputTest.class);
List<Result> results = tester.run();
assertEquals(2, results.size());
// The test with name1, description1 should have no output included.
Result result1 = results.get(0).getName().equals("name1") ? results.get(0) : results.get(1);
Result result2 = result1.equals(results.get(0)) ? results.get(1) : results.get(0);
assertEquals("description1", result1.getMessage());
assertEquals("description2\nOUTPUT\n======\noutput2", result2.getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.spertus.jacquard;
package com.spertus.jacquard.common;

import com.spertus.jacquard.common.*;
import org.junit.jupiter.api.*;

import java.util.List;
Expand Down Expand Up @@ -67,4 +66,18 @@ public void testReorderDecreasingMaxScore() {
public void testReorderIncreasingMaxScore() {
assertEquals(List.of(rMixed, rBigSuccess, rHugeFailure), Result.reorderResults(results, Result.Order.INCREASING_MAX_SCORE));
}

@Test
public void testTrimMessage() {
String message = "Hello, world!";
assertEquals(message, Result.trimMessage(message, message.length(), "..."));
assertEquals(
"Hello, wo...",
Result.trimMessage(message, message.length() - 1, "...")
);
assertEquals(
"Hello!",
Result.trimMessage(message, 6, "!")
);
}
}
113 changes: 113 additions & 0 deletions src/test/java/com/spertus/jacquard/junittester/MergeResultsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package com.spertus.jacquard.junittester;

import com.google.common.collect.Sets;
import com.spertus.jacquard.JUnitTesterTest;
import com.spertus.jacquard.common.*;
import org.junit.jupiter.api.*;

import java.util.*;

import static org.junit.jupiter.api.Assertions.*;

public class MergeResultsTest {
public static final String NAME = "name";
private static Result result1;
private static Result result2;
private static Result result3;

@BeforeEach
public void setup() {
Autograder.initForTest();
result1 = Result.makeSuccess(NAME, 1.0, "message1");
result2 = Result.makeFailure(NAME, 2.0, "message2");
result3 = Result.makeResult(NAME, 3.5, 4.0, "message3");
}

@Test
public void testMerge0() {
assertThrows(IllegalArgumentException.class, () -> JUnitTester.mergeResults(List.of()));
}

@Test
public void testMergeDifferentNames() {
List<Result> mismatchedResults = List.of(
result1,
Result.makeFailure("different name", 5.0, "ignored message")
);
assertThrows(IllegalArgumentException.class, () -> JUnitTester.mergeResults(mismatchedResults));
}

// Checks if strings have same lines, possibly in different order.
private void assertSameLines(String s1, String s2) {
Set<String> lines1 = Sets.newHashSet(s1.split("\n"));
Set<String> lines2 = Sets.newHashSet(s2.split("\n"));
if (lines1.equals(lines2)) {
return;
}
// This provides better messages than just failing.
for (String s : lines1) {
assertTrue(lines2.contains(s));
}
for (String s : lines2) {
assertTrue(lines1.contains(s));
}
}

// Checks if all resultLists produce results that match the arguments.
private void assertMatching(
String name,
double score,
double maxScore,
String message,
List<Result>... resultLists
) {
for (List<Result> resultList : resultLists) {
Result result = JUnitTester.mergeResults(resultList);
assertEquals(name, result.getName());
assertEquals(score, result.getScore());
assertEquals(maxScore, result.getMaxScore());
assertSameLines(message, result.getMessage());
}
}

// single results don't get merged
@Test
public void testMerge1() {
// Success
assertMatching(NAME, 1.0, 1.0, "message1", List.of(result1));

// Failure
assertMatching(NAME, 0.0, 2.0, "message2", List.of(result2));

// Partial success
assertMatching(NAME, 3.5, 4.0, "message3", List.of(result3));
}

@Test
public void testMerge2() {
// One success, one failure
assertMatching(NAME, 1.0, 3.0, "-2.0: message2",
List.of(result1, result2),
List.of(result2, result1));

// One success, one partial
assertMatching(NAME, 4.5, 5.0, "-0.5: message3",
List.of(result1, result3),
List.of(result3, result1));

// One partial, one failure
assertMatching(NAME, 3.5, 6.0, "-2.0: message2\n-0.5: message3",
List.of(result2, result3), List.of(result3, result2));
}

@Test
public void testMerge3() {
assertMatching(NAME, 4.5, 7.0, "-2.0: message2\n-0.5: message3",
List.of(result1, result2, result3),
List.of(result1, result3, result2),
List.of(result2, result1, result3),
List.of(result2, result3, result1),
List.of(result3, result1, result2),
List.of(result3, result2, result1));
}
}
Loading

0 comments on commit 3d1b495

Please sign in to comment.