Improve testing with stats and errors per test section (#498)

* Emojis are moves to the left to be aligned
* A summary line is added with test counts
* Facts and Examples are grouped under their own section
This commit is contained in:
Javier Maestro
2024-10-24 07:00:35 +01:00
committed by GitHub
parent 2040f14b07
commit 86d870ba09
8 changed files with 858 additions and 393 deletions

View File

@@ -60,13 +60,16 @@ constructor(
evaluator.use { evaluator.use {
var failed = false var failed = false
val moduleNames = mutableSetOf<String>() val moduleNames = mutableSetOf<String>()
for (moduleUri in sources) { for ((idx, moduleUri) in sources.withIndex()) {
try { try {
val results = evaluator.evaluateTest(uri(moduleUri), testOptions.overwrite) val results = evaluator.evaluateTest(uri(moduleUri), testOptions.overwrite)
if (!failed) { if (!failed) {
failed = results.failed() failed = results.failed()
} }
SimpleReport().report(results, consoleWriter) SimpleReport().report(results, consoleWriter)
if (sources.size > 1 && idx != sources.size - 1) {
consoleWriter.append('\n')
}
consoleWriter.flush() consoleWriter.flush()
val junitDir = testOptions.junitDir val junitDir = testOptions.junitDir
if (junitDir != null) { if (junitDir != null) {

View File

@@ -63,8 +63,10 @@ class CliTestRunnerTest {
.isEqualTo( .isEqualTo(
""" """
module test module test
succeed ✅ facts
✅ succeed
✅ 100.0% tests pass [1 passed], 100.0% asserts pass [2 passed]
""" """
.trimIndent() .trimIndent()
) )
@@ -80,7 +82,7 @@ class CliTestRunnerTest {
facts { facts {
["fail"] { ["fail"] {
4 == 9 4 == 9
"foo" == "bar" "foo" != "bar"
} }
} }
""" """
@@ -97,10 +99,11 @@ class CliTestRunnerTest {
.isEqualTo( .isEqualTo(
""" """
module test module test
fail ❌ facts
4 == 9 fail
"foo" == "bar" ❌ 4 == 9
❌ 0.0% tests pass [1/1 failed], 50.0% asserts pass [1/2 failed]
""" """
.trimIndent() .trimIndent()
) )
@@ -132,14 +135,15 @@ class CliTestRunnerTest {
.isEqualToNormalizingNewlines( .isEqualToNormalizingNewlines(
""" """
module test module test
fail ❌ facts
Error: ❌ fail
Pkl Error Pkl Error
uh oh uh oh
5 | throw("uh oh") 5 | throw("uh oh")
^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
at test#facts["fail"][#1] at test#facts["fail"][#1]
❌ 0.0% tests pass [1/1 failed], 0.0% asserts pass [1/1 failed]
""" """
.trimIndent() .trimIndent()
@@ -172,14 +176,15 @@ class CliTestRunnerTest {
.isEqualTo( .isEqualTo(
""" """
module test module test
fail ❌ examples
Error: ❌ fail
Pkl Error Pkl Error
uh oh uh oh
5 | throw("uh oh") 5 | throw("uh oh")
^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
at test#examples["fail"][#1] at test#examples["fail"][#1]
❌ 0.0% tests pass [1/1 failed], 0.0% asserts pass [1/1 failed]
""" """
.trimIndent() .trimIndent()
@@ -226,14 +231,15 @@ class CliTestRunnerTest {
.isEqualToNormalizingNewlines( .isEqualToNormalizingNewlines(
""" """
module test module test
fail ❌ examples
Error: ❌ fail
Pkl Error Pkl Error
uh oh uh oh
5 | throw("uh oh") 5 | throw("uh oh")
^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
at test#examples["fail"][#1] at test#examples["fail"][#1]
❌ 0.0% tests pass [1/1 failed], 0.0% asserts pass [1/1 failed]
""" """
.trimIndent() .trimIndent()
@@ -252,7 +258,8 @@ class CliTestRunnerTest {
9 == trace(9) 9 == trace(9)
"foo" == "foo" "foo" == "foo"
} }
["fail"] { ["bar"] {
"foo" == "foo"
5 == 9 5 == 9
} }
} }
@@ -270,9 +277,9 @@ class CliTestRunnerTest {
""" """
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<testsuite name="test" tests="2" failures="1"> <testsuite name="test" tests="2" failures="1">
<testcase classname="test" name="foo"></testcase> <testcase classname="test.facts" name="foo"></testcase>
<testcase classname="test" name="fail"> <testcase classname="test.facts" name="bar">
<failure message="Fact Failure">5 == 9</failure> <failure message="Fact Failure">5 == 9</failure>
</testcase> </testcase>
<system-err><![CDATA[9 = 9 <system-err><![CDATA[9 = 9
]]></system-err> ]]></system-err>
@@ -311,9 +318,9 @@ class CliTestRunnerTest {
.isEqualTo( .isEqualTo(
""" """
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<testsuite name="test" tests="2" failures="0"> <testsuite name="test" tests="2" failures="1">
<testcase classname="test" name="foo"></testcase> <testcase classname="test.facts" name="foo"></testcase>
<testcase classname="test" name="fail"> <testcase classname="test.facts" name="fail">
<error message="uh oh"> Pkl Error <error message="uh oh"> Pkl Error
uh oh uh oh

View File

@@ -20,61 +20,40 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import org.pkl.core.PklException; import org.pkl.core.PklException;
import org.pkl.core.runtime.TestResults.TestSectionResults.TestSection;
/** Aggregate test results for a module. Used to verify test failures and generate reports. */ /** Aggregate test results for a module. Used to verify test failures and generate reports. */
public final class TestResults { public final class TestResults {
public final String moduleName;
private final String module; public final String displayUri;
private final String displayUri; public final TestSectionResults module = new TestSectionResults(TestSection.MODULE);
private final List<TestResult> results = new ArrayList<>(); public final TestSectionResults facts = new TestSectionResults(TestSection.FACTS);
public final TestSectionResults examples = new TestSectionResults(TestSection.EXAMPLES);
private String err = ""; private String err = "";
public TestResults(String module, String displayUri) { public TestResults(String moduleName, String displayUri) {
this.module = module; this.moduleName = moduleName;
this.displayUri = displayUri; this.displayUri = displayUri;
} }
public String getModuleName() {
return module;
}
public String getDisplayUri() {
return displayUri;
}
public List<TestResult> getResults() {
return Collections.unmodifiableList(results);
}
public TestResult newResult(String name) {
var result = new TestResult(name);
results.add(result);
return result;
}
public void newResult(String name, Failure failure) {
var result = new TestResult(name);
result.addFailure(failure);
results.add(result);
}
public int totalTests() { public int totalTests() {
return results.size(); return module.totalTests() + facts.totalTests() + examples.totalTests();
} }
public int totalFailures() { public int totalFailures() {
int total = 0; return module.totalFailures() + facts.totalFailures() + examples.totalFailures();
for (var res : results) { }
total += res.getFailures().size();
} public int totalAsserts() {
return total; return module.totalAsserts() + facts.totalAsserts() + examples.totalAsserts();
}
public int totalAssertsFailed() {
return module.totalAssertsFailed() + facts.totalAssertsFailed() + examples.totalAssertsFailed();
} }
public boolean failed() { public boolean failed() {
for (var res : results) { return module.failed() || facts.failed() || examples.failed();
if (res.isFailure()) return true;
}
return false;
} }
public String getErr() { public String getErr() {
@@ -85,147 +64,295 @@ public final class TestResults {
this.err = err; this.err = err;
} }
public static class TestResult { public static class TestSectionResults {
public final TestSection name;
private final List<TestResult> results = new ArrayList<>();
private Error error;
private final String name; public TestSectionResults(TestSection name) {
private final List<Failure> failures = new ArrayList<>();
private final List<Error> errors = new ArrayList<>();
private boolean isExampleWritten = false;
public TestResult(String name) {
this.name = name; this.name = name;
} }
public boolean isSuccess() { public void setError(Error error) {
return failures.isEmpty() && errors.isEmpty(); this.error = error;
} }
boolean isFailure() { public Error getError() {
return !isSuccess(); return error;
} }
public String getName() { public boolean hasError() {
return name; return error != null;
} }
public boolean isExampleWritten() { public List<TestResult> getResults() {
return isExampleWritten; return Collections.unmodifiableList(results);
} }
public void setExampleWritten(boolean exampleWritten) { public TestResult newResult(String name) {
isExampleWritten = exampleWritten; var result = new TestResult(name, this.name == TestSection.EXAMPLES);
results.add(result);
return result;
} }
public List<Failure> getFailures() { public TestResult newResult(String name, Failure failure) {
return Collections.unmodifiableList(failures); var result = new TestResult(name, this.name == TestSection.EXAMPLES);
result.addFailure(failure);
results.add(result);
return result;
} }
public void addFailure(Failure description) { public TestResult newResult(String name, Error error) {
failures.add(description); var result = new TestResult(name, this.name == TestSection.EXAMPLES);
result.addError(error);
results.add(result);
return result;
} }
public List<Error> getErrors() { public int totalTests() {
return Collections.unmodifiableList(errors); var total = results.size();
return (hasError() ? ++total : total);
} }
public void addError(Error err) { public int totalAsserts() {
errors.add(err); int total = 0;
} for (var res : results) {
} total += res.totalAsserts();
public static class Failure {
private final String kind;
private final String rendered;
private Failure(String kind, String rendered) {
this.kind = kind;
this.rendered = rendered;
}
public String getKind() {
return kind;
}
public String getRendered() {
return rendered;
}
public static Failure buildFactFailure(SourceSection sourceSection, String description) {
return new Failure(
"Fact Failure", sourceSection.getCharacters() + " ❌ (" + description + ")");
}
public static Failure buildExampleLengthMismatchFailure(
String location, String property, int expectedLength, int actualLength) {
String builder =
"("
+ location
+ ")\n"
+ "Output mismatch: Expected \""
+ property
+ "\" to contain "
+ expectedLength
+ " examples, but found "
+ actualLength;
return new Failure("Output Mismatch (Length)", builder);
}
public static Failure buildExamplePropertyMismatchFailure(
String location, String property, boolean isMissingInExpected) {
var builder = new StringBuilder();
builder
.append("(")
.append(location)
.append(")\n")
.append("Output mismatch: \"")
.append(property);
if (isMissingInExpected) {
builder.append("\" exists in actual but not in expected output");
} else {
builder.append("\" exists in expected but not in actual output");
} }
return new Failure("Output Mismatch", builder.toString()); return (hasError() ? ++total : total);
} }
public static Failure buildExampleFailure( public int totalAssertsFailed() {
String location, int total = 0;
String expectedLocation, for (var res : results) {
String expectedValue, total += res.totalAssertsFailed();
String actualLocation, }
String actualValue) { return (hasError() ? ++total : total);
String builder =
"("
+ location
+ ")\n"
+ "Expected: ("
+ expectedLocation
+ ")\n"
+ expectedValue
+ "\nActual: ("
+ actualLocation
+ ")\n"
+ actualValue;
return new Failure("Example Failure", builder);
}
}
public static class Error {
private final String message;
private final PklException exception;
public Error(String message, PklException exception) {
this.message = message;
this.exception = exception;
} }
public String getMessage() { public int totalFailures() {
return message; int total = 0;
for (var res : results) {
if (res.isFailure()) total++;
}
return (hasError() ? ++total : total);
} }
public Exception getException() { public boolean failed() {
return exception; if (hasError()) return true;
for (var res : results) {
if (res.isFailure()) return true;
}
return false;
}
public static class TestResult {
public final String name;
private int totalAsserts = 0;
private int totalAssertsFailed = 0;
private final List<Failure> failures = new ArrayList<>();
private final List<Error> errors = new ArrayList<>();
public final boolean isExample;
private boolean isExampleWritten = false;
public TestResult(String name, boolean isExample) {
this.name = name;
this.isExample = isExample;
}
public boolean isSuccess() {
return failures.isEmpty() && errors.isEmpty();
}
public boolean isFailure() {
return !isSuccess();
}
public boolean isExampleWritten() {
return isExampleWritten;
}
public void setExampleWritten(boolean exampleWritten) {
isExampleWritten = exampleWritten;
}
public List<Failure> getFailures() {
return Collections.unmodifiableList(failures);
}
public void addFailure(Failure description) {
failures.add(description);
totalAssertsFailed++;
}
public List<Error> getErrors() {
return Collections.unmodifiableList(errors);
}
public void addError(Error err) {
errors.add(err);
totalAssertsFailed++;
}
public int totalAsserts() {
return totalAsserts;
}
public void countAssert() {
totalAsserts++;
}
public int totalAssertsFailed() {
return totalAssertsFailed;
}
}
public static class Failure {
private final String kind;
private final String failure;
private final String location;
private Failure(String kind, String failure, String location) {
this.kind = kind;
this.failure = failure;
this.location = location;
}
public String getKind() {
return kind;
}
public String getFailure() {
return failure;
}
public String getLocation() {
return location;
}
public static String renderLocation(String location) {
return "(" + location + ")";
}
public String getRendered() {
String rendered;
if (kind == "Fact Failure") {
rendered = failure + " " + renderLocation(getLocation());
} else {
rendered = renderLocation(getLocation()) + "\n" + failure;
}
return rendered;
}
public static Failure buildFactFailure(SourceSection sourceSection, String location) {
return new Failure("Fact Failure", sourceSection.getCharacters().toString(), location);
}
public static Failure buildExampleLengthMismatchFailure(
String location, String property, int expectedLength, int actualLength) {
var builder = new StringBuilder();
builder
.append("Output mismatch: Expected \"")
.append(property)
.append("\" to contain ")
.append(expectedLength)
.append(" examples, but found ")
.append(actualLength);
return new Failure("Output Mismatch (Length)", builder.toString(), location);
}
public static Failure buildExamplePropertyMismatchFailure(
String location, String property, boolean isMissingInExpected) {
String exists_in;
String missing_in;
if (isMissingInExpected) {
exists_in = "actual";
missing_in = "expected";
} else {
exists_in = "expected";
missing_in = "actual";
}
var builder = new StringBuilder();
builder
.append("Output mismatch: \"")
.append(property)
.append("\" exists in ")
.append(exists_in)
.append(" but not in ")
.append(missing_in)
.append(" output");
return new Failure("Output Mismatch", builder.toString(), location);
}
public static Failure buildExampleFailure(
String location,
String expectedLocation,
String expectedValue,
String actualLocation,
String actualValue) {
var builder = new StringBuilder();
builder
.append("Expected: ")
.append(renderLocation(expectedLocation))
.append("\n")
.append(expectedValue)
.append("\n")
.append("Actual: ")
.append(renderLocation(actualLocation))
.append("\n")
.append(actualValue);
return new Failure("Example Failure", builder.toString(), location);
}
}
public static class Error {
private final String message;
private final PklException exception;
public Error(String message, PklException exception) {
this.message = message;
this.exception = exception;
}
public String getMessage() {
return message;
}
public Exception getException() {
return exception;
}
public String getRendered() {
return exception.getMessage();
}
}
public enum TestSection {
MODULE("module"),
FACTS("facts"),
EXAMPLES("examples");
private final String name;
TestSection(final String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
} }
} }
} }

View File

@@ -23,8 +23,9 @@ import org.pkl.core.BufferedLogger;
import org.pkl.core.StackFrameTransformer; import org.pkl.core.StackFrameTransformer;
import org.pkl.core.ast.member.ObjectMember; import org.pkl.core.ast.member.ObjectMember;
import org.pkl.core.module.ModuleKeys; import org.pkl.core.module.ModuleKeys;
import org.pkl.core.runtime.TestResults.Error; import org.pkl.core.runtime.TestResults.TestSectionResults;
import org.pkl.core.runtime.TestResults.Failure; import org.pkl.core.runtime.TestResults.TestSectionResults.Error;
import org.pkl.core.runtime.TestResults.TestSectionResults.Failure;
import org.pkl.core.stdlib.PklConverter; import org.pkl.core.stdlib.PklConverter;
import org.pkl.core.stdlib.base.PcfRenderer; import org.pkl.core.stdlib.base.PcfRenderer;
import org.pkl.core.util.EconomicMaps; import org.pkl.core.util.EconomicMaps;
@@ -51,12 +52,25 @@ public final class TestRunner {
try { try {
checkAmendsPklTest(testModule); checkAmendsPklTest(testModule);
runFacts(testModule, results);
runExamples(testModule, info, results);
} catch (VmException v) { } catch (VmException v) {
var meta = results.newResult(info.getModuleName()); var error = new Error(v.getMessage(), v.toPklException(stackFrameTransformer));
meta.addError(new Error(v.getMessage(), v.toPklException(stackFrameTransformer))); results.module.setError(error);
} }
try {
runFacts(testModule, results.facts);
} catch (VmException v) {
var error = new Error(v.getMessage(), v.toPklException(stackFrameTransformer));
results.facts.setError(error);
}
try {
runExamples(testModule, info, results.examples);
} catch (VmException v) {
var error = new Error(v.getMessage(), v.toPklException(stackFrameTransformer));
results.examples.setError(error);
}
results.setErr(logger.getLogs()); results.setErr(logger.getLogs());
return results; return results;
} }
@@ -72,7 +86,7 @@ public final class TestRunner {
} }
} }
private void runFacts(VmTyped testModule, TestResults results) { private void runFacts(VmTyped testModule, TestSectionResults results) {
var facts = VmUtils.readMember(testModule, Identifier.FACTS); var facts = VmUtils.readMember(testModule, Identifier.FACTS);
if (facts instanceof VmNull) return; if (facts instanceof VmNull) return;
@@ -86,6 +100,9 @@ public final class TestRunner {
if (member.isLocalOrExternalOrHidden()) { if (member.isLocalOrExternalOrHidden()) {
return true; return true;
} }
result.countAssert();
try { try {
var factValue = VmUtils.readMember(listing, idx); var factValue = VmUtils.readMember(listing, idx);
if (factValue == Boolean.FALSE) { if (factValue == Boolean.FALSE) {
@@ -101,7 +118,7 @@ public final class TestRunner {
}); });
} }
private void runExamples(VmTyped testModule, ModuleInfo info, TestResults results) { private void runExamples(VmTyped testModule, ModuleInfo info, TestSectionResults results) {
var examples = VmUtils.readMember(testModule, Identifier.EXAMPLES); var examples = VmUtils.readMember(testModule, Identifier.EXAMPLES);
if (examples instanceof VmNull) return; if (examples instanceof VmNull) return;
@@ -144,7 +161,10 @@ public final class TestRunner {
} }
private void doRunAndValidateExamples( private void doRunAndValidateExamples(
VmMapping examples, Path expectedOutputFile, Path actualOutputFile, TestResults results) { VmMapping examples,
Path expectedOutputFile,
Path actualOutputFile,
TestSectionResults results) {
var expectedExampleOutputs = loadExampleOutputs(expectedOutputFile); var expectedExampleOutputs = loadExampleOutputs(expectedOutputFile);
var actualExampleOutputs = new MutableReference<VmDynamic>(null); var actualExampleOutputs = new MutableReference<VmDynamic>(null);
var allGroupsSucceeded = new MutableBoolean(true); var allGroupsSucceeded = new MutableBoolean(true);
@@ -155,23 +175,27 @@ public final class TestRunner {
var group = (VmListing) groupValue; var group = (VmListing) groupValue;
var expectedGroup = var expectedGroup =
(VmDynamic) VmUtils.readMemberOrNull(expectedExampleOutputs, groupKey); (VmDynamic) VmUtils.readMemberOrNull(expectedExampleOutputs, groupKey);
var result = results.newResult(testName);
if (expectedGroup == null) { if (expectedGroup == null) {
results.newResult( results
testName, .newResult(
Failure.buildExamplePropertyMismatchFailure( testName,
getDisplayUri(groupMember), String.valueOf(groupKey), true)); Failure.buildExamplePropertyMismatchFailure(
getDisplayUri(groupMember), testName, true))
.countAssert();
return true; return true;
} }
if (group.getLength() != expectedGroup.getLength()) { if (group.getLength() != expectedGroup.getLength()) {
result.addFailure( results
Failure.buildExampleLengthMismatchFailure( .newResult(
getDisplayUri(groupMember), testName,
String.valueOf(groupKey), Failure.buildExampleLengthMismatchFailure(
expectedGroup.getLength(), getDisplayUri(groupMember),
group.getLength())); testName,
expectedGroup.getLength(),
group.getLength()))
.countAssert();
return true; return true;
} }
@@ -181,13 +205,20 @@ public final class TestRunner {
if (exampleMember.isLocalOrExternalOrHidden()) { if (exampleMember.isLocalOrExternalOrHidden()) {
return true; return true;
} }
var exampleName =
group.getLength() == 1 ? testName : testName + " #" + exampleIndex;
Object exampleValue; Object exampleValue;
try { try {
exampleValue = VmUtils.readMember(group, exampleIndex); exampleValue = VmUtils.readMember(group, exampleIndex);
} catch (VmException err) { } catch (VmException err) {
errored.set(true); errored.set(true);
result.addError( results
new Error(err.getMessage(), err.toPklException(stackFrameTransformer))); .newResult(
exampleName,
new Error(err.getMessage(), err.toPklException(stackFrameTransformer)))
.countAssert();
groupSucceeded.set(false); groupSucceeded.set(false);
return true; return true;
} }
@@ -222,13 +253,18 @@ public final class TestRunner {
.build(); .build();
} }
result.addFailure( results
Failure.buildExampleFailure( .newResult(
getDisplayUri(exampleMember), exampleName,
getDisplayUri(expectedMember), Failure.buildExampleFailure(
expectedValuePcf, getDisplayUri(exampleMember),
getDisplayUri(actualMember), getDisplayUri(expectedMember),
exampleValuePcf)); expectedValuePcf,
getDisplayUri(actualMember),
exampleValuePcf))
.countAssert();
} else {
results.newResult(exampleName).countAssert();
} }
return true; return true;
@@ -247,12 +283,14 @@ public final class TestRunner {
return true; return true;
} }
if (examples.getCachedValue(groupKey) == null) { if (examples.getCachedValue(groupKey) == null) {
var testName = String.valueOf(groupKey);
allGroupsSucceeded.set(false); allGroupsSucceeded.set(false);
results results
.newResult(String.valueOf(groupKey)) .newResult(
.addFailure( testName,
Failure.buildExamplePropertyMismatchFailure( Failure.buildExamplePropertyMismatchFailure(
getDisplayUri(groupMember), String.valueOf(groupKey), false)); getDisplayUri(groupMember), testName, false))
.countAssert();
} }
return true; return true;
}); });
@@ -262,10 +300,12 @@ public final class TestRunner {
} }
} }
private void doRunAndWriteExamples(VmMapping examples, Path outputFile, TestResults results) { private void doRunAndWriteExamples(
VmMapping examples, Path outputFile, TestSectionResults results) {
var allSucceeded = var allSucceeded =
examples.forceAndIterateMemberValues( examples.forceAndIterateMemberValues(
(groupKey, groupMember, groupValue) -> { (groupKey, groupMember, groupValue) -> {
var testName = String.valueOf(groupKey);
var listing = (VmListing) groupValue; var listing = (VmListing) groupValue;
var success = var success =
listing.iterateMembers( listing.iterateMembers(
@@ -273,22 +313,29 @@ public final class TestRunner {
if (member.isLocalOrExternalOrHidden()) { if (member.isLocalOrExternalOrHidden()) {
return true; return true;
} }
var exampleName =
listing.getLength() == 1 ? testName : testName + " #" + idx;
try { try {
VmUtils.readMember(listing, idx); VmUtils.readMember(listing, idx);
return true; return true;
} catch (VmException err) { } catch (VmException err) {
results results
.newResult(String.valueOf(groupKey)) .newResult(
.addError( exampleName,
new Error( new Error(
err.getMessage(), err.toPklException(stackFrameTransformer))); err.getMessage(), err.toPklException(stackFrameTransformer)))
.countAssert();
return false; return false;
} }
}); });
if (!success) { if (!success) {
return false; return false;
} }
results.newResult(String.valueOf(groupKey)).setExampleWritten(true); var result = results.newResult(testName);
result.countAssert();
result.setExampleWritten(true);
return true; return true;
}); });
if (allSucceeded) { if (allSucceeded) {

View File

@@ -24,7 +24,9 @@ import org.pkl.core.ast.member.ObjectMember;
import org.pkl.core.runtime.BaseModule; import org.pkl.core.runtime.BaseModule;
import org.pkl.core.runtime.Identifier; import org.pkl.core.runtime.Identifier;
import org.pkl.core.runtime.TestResults; import org.pkl.core.runtime.TestResults;
import org.pkl.core.runtime.TestResults.TestResult; import org.pkl.core.runtime.TestResults.TestSectionResults;
import org.pkl.core.runtime.TestResults.TestSectionResults.Error;
import org.pkl.core.runtime.TestResults.TestSectionResults.TestResult;
import org.pkl.core.runtime.VmDynamic; import org.pkl.core.runtime.VmDynamic;
import org.pkl.core.runtime.VmMapping; import org.pkl.core.runtime.VmMapping;
import org.pkl.core.runtime.VmTyped; import org.pkl.core.runtime.VmTyped;
@@ -41,25 +43,45 @@ public final class JUnitReport implements TestReport {
writer.append(renderXML(" ", "1.0", buildSuite(results))); writer.append(renderXML(" ", "1.0", buildSuite(results)));
} }
private VmDynamic buildSuite(TestResults res) { private VmDynamic buildSuite(TestResults results) {
var testCases = testCases(res); var testCases = testCases(results.moduleName, results.facts);
if (!res.getErr().isBlank()) { testCases.addAll(testCases(results.moduleName, results.examples));
if (!results.getErr().isBlank()) {
var err = var err =
buildXmlElement( buildXmlElement(
"system-err", "system-err",
VmMapping.empty(), VmMapping.empty(),
members -> members.put("body", syntheticElement(makeCdata(res.getErr())))); members -> members.put("body", syntheticElement(makeCdata(results.getErr()))));
testCases.add(err); testCases.add(err);
} }
return buildXmlElement(
"testsuite", buildRootAttributes(res), testCases.toArray(new VmDynamic[0])); var attrs =
buildAttributes(
"name", results.moduleName,
"tests", (long) results.totalTests(),
"failures", (long) results.totalFailures());
return buildXmlElement("testsuite", attrs, testCases.toArray(new VmDynamic[0]));
} }
private ArrayList<VmDynamic> testCases(TestResults results) { private ArrayList<VmDynamic> testCases(String moduleName, TestSectionResults testSectionResults) {
var className = results.getModuleName(); var elements = new ArrayList<VmDynamic>(testSectionResults.totalTests());
var elements = new ArrayList<VmDynamic>(results.totalTests());
for (var res : results.getResults()) { if (testSectionResults.hasError()) {
var attrs = buildAttributes("classname", className, "name", res.getName()); var error = error(testSectionResults.getError());
var attrs =
buildAttributes("classname", moduleName + "." + testSectionResults.name, "name", "error");
var element = buildXmlElement("testcase", attrs, error.toArray(new VmDynamic[0]));
elements.add(element);
}
for (var res : testSectionResults.getResults()) {
var attrs =
buildAttributes(
"classname", moduleName + "." + testSectionResults.name, "name", res.name);
var failures = failures(res); var failures = failures(res);
failures.addAll(errors(res)); failures.addAll(errors(res));
var element = buildXmlElement("testcase", attrs, failures.toArray(new VmDynamic[0])); var element = buildXmlElement("testcase", attrs, failures.toArray(new VmDynamic[0]));
@@ -99,6 +121,17 @@ public final class JUnitReport implements TestReport {
return list; return list;
} }
private ArrayList<VmDynamic> error(Error error) {
var list = new ArrayList<VmDynamic>();
var attrs = buildAttributes("message", error.getMessage());
list.add(
buildXmlElement(
"error",
attrs,
members -> members.put(1, syntheticElement("\n" + error.getRendered()))));
return list;
}
private VmDynamic buildXmlElement(String name, VmMapping attributes, VmDynamic... elements) { private VmDynamic buildXmlElement(String name, VmMapping attributes, VmDynamic... elements) {
return buildXmlElement( return buildXmlElement(
name, name,
@@ -130,16 +163,6 @@ public final class JUnitReport implements TestReport {
members.size() - 4); members.size() - 4);
} }
private VmMapping buildRootAttributes(TestResults results) {
return buildAttributes(
"name",
results.getModuleName(),
"tests",
(long) results.totalTests(),
"failures",
(long) results.totalFailures());
}
private VmMapping buildAttributes(Object... attributes) { private VmMapping buildAttributes(Object... attributes) {
EconomicMap<Object, ObjectMember> attrs = EconomicMaps.create(attributes.length); EconomicMap<Object, ObjectMember> attrs = EconomicMaps.create(attributes.length);
for (int i = 0; i < attributes.length; i += 2) { for (int i = 0; i < attributes.length; i += 2) {

View File

@@ -19,8 +19,8 @@ import java.io.IOException;
import java.io.Writer; import java.io.Writer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.pkl.core.runtime.TestResults; import org.pkl.core.runtime.TestResults;
import org.pkl.core.runtime.TestResults.Failure; import org.pkl.core.runtime.TestResults.TestSectionResults;
import org.pkl.core.runtime.TestResults.TestResult; import org.pkl.core.runtime.TestResults.TestSectionResults.TestResult;
import org.pkl.core.util.StringUtils; import org.pkl.core.util.StringUtils;
public final class SimpleReport implements TestReport { public final class SimpleReport implements TestReport {
@@ -28,38 +28,66 @@ public final class SimpleReport implements TestReport {
@Override @Override
public void report(TestResults results, Writer writer) throws IOException { public void report(TestResults results, Writer writer) throws IOException {
var builder = new StringBuilder(); var builder = new StringBuilder();
builder.append("module ");
builder.append(results.getModuleName()); builder.append("module ").append(results.moduleName).append("\n");
builder.append(" (").append(results.getDisplayUri()).append(")\n");
StringUtils.joinToStringBuilder( reportResults(results.facts, builder);
builder, results.getResults(), "\n", res -> reportResult(res, builder)); reportResults(results.examples, builder);
builder.append(results.failed() ? "" : "");
var totalStatsLine =
makeStatsLine("tests", results.totalTests(), results.totalFailures(), results.failed());
builder.append(totalStatsLine);
var totalAssertsStatsLine =
makeStatsLine(
"asserts", results.totalAsserts(), results.totalAssertsFailed(), results.failed());
builder.append(", ").append(totalAssertsStatsLine);
builder.append("\n"); builder.append("\n");
writer.append(builder); writer.append(builder);
} }
private void reportResult(TestResult result, StringBuilder builder) { private void reportResults(TestSectionResults section, StringBuilder builder) {
builder.append(" ").append(result.getName()); if (!section.getResults().isEmpty()) {
if (result.isExampleWritten()) { builder.append(" ").append(section.name).append("\n");
builder.append(" ✍️");
} else if (result.isSuccess()) {
builder.append("");
} else {
builder.append("\n");
StringUtils.joinToStringBuilder( StringUtils.joinToStringBuilder(
builder, result.getFailures(), "\n", failure -> reportFailure(failure, builder)); builder, section.getResults(), "\n", res -> reportResult(res, builder));
StringUtils.joinToStringBuilder( builder.append("\n");
builder, } else if (section.hasError()) {
result.getErrors(), builder.append(" ").append(section.name).append("\n");
"\n", var error = section.getError().getRendered();
error -> { appendPadded(builder, error, " ");
builder.append(" Error:\n"); builder.append("\n");
appendPadded(builder, error.getException().getMessage(), " ");
});
} }
} }
public static void reportFailure(Failure failure, StringBuilder builder) { private void reportResult(TestResult result, StringBuilder builder) {
appendPadded(builder, failure.getRendered(), " "); builder.append(" ");
if (result.isExampleWritten()) {
builder.append(result.name).append(" ✍️");
} else {
builder.append(result.isFailure() ? "" : "").append(result.name);
if (result.isFailure()) {
var failurePadding = " ";
builder.append("\n");
StringUtils.joinToStringBuilder(
builder,
result.getFailures(),
"\n",
failure -> appendPadded(builder, failure.getRendered(), failurePadding));
StringUtils.joinToStringBuilder(
builder,
result.getErrors(),
"\n",
error -> appendPadded(builder, error.getException().getMessage(), failurePadding));
}
}
} }
private static void appendPadded(StringBuilder builder, String lines, String padding) { private static void appendPadded(StringBuilder builder, String lines, String padding) {
@@ -67,6 +95,23 @@ public final class SimpleReport implements TestReport {
builder, builder,
lines.lines().collect(Collectors.toList()), lines.lines().collect(Collectors.toList()),
"\n", "\n",
str -> builder.append(padding).append(str)); str -> {
if (str.length() > 0) builder.append(padding).append(str);
});
}
private String makeStatsLine(String kind, int total, int failed, boolean isFailed) {
var passed = total - failed;
var pct_passed = total > 0 ? 100.0 * passed / total : 0.0;
String line = String.format("%.1f%% %s pass", pct_passed, kind);
if (isFailed) {
line += String.format(" [%d/%d failed]", failed, total);
} else {
line += String.format(" [%d passed]", passed);
}
return line;
} }
} }

View File

@@ -54,7 +54,7 @@ class EvaluateTestsTest {
assertThat(results.displayUri).isEqualTo("repl:text") assertThat(results.displayUri).isEqualTo("repl:text")
assertThat(results.totalTests()).isEqualTo(1) assertThat(results.totalTests()).isEqualTo(1)
assertThat(results.failed()).isFalse assertThat(results.failed()).isFalse
assertThat(results.results[0].name).isEqualTo("should pass") assertThat(results.facts.results[0].name).isEqualTo("should pass")
assertThat(results.err.isBlank()).isTrue assertThat(results.err.isBlank()).isTrue
} }
@@ -79,18 +79,19 @@ class EvaluateTestsTest {
) )
assertThat(results.totalTests()).isEqualTo(1) assertThat(results.totalTests()).isEqualTo(1)
assertThat(results.totalFailures()).isEqualTo(2) assertThat(results.totalFailures()).isEqualTo(1)
assertThat(results.failed()).isTrue assertThat(results.failed()).isTrue
val res = results.results[0] val res = results.facts.results[0]
assertThat(res.name).isEqualTo("should fail") assertThat(res.name).isEqualTo("should fail")
assertThat(res.errors).isEmpty() assertThat(results.facts.hasError()).isFalse
assertThat(res.failures.size).isEqualTo(2)
val fail1 = res.failures[0] val fail1 = res.failures[0]
assertThat(fail1.rendered).isEqualTo("1 == 2 (repl:text)") assertThat(fail1.rendered).isEqualTo("1 == 2 (repl:text)")
val fail2 = res.failures[1] val fail2 = res.failures[1]
assertThat(fail2.rendered).isEqualTo(""""foo" == "bar" (repl:text)""") assertThat(fail2.rendered).isEqualTo(""""foo" == "bar" (repl:text)""")
} }
@Test @Test
@@ -117,7 +118,7 @@ class EvaluateTestsTest {
assertThat(results.totalFailures()).isEqualTo(1) assertThat(results.totalFailures()).isEqualTo(1)
assertThat(results.failed()).isTrue assertThat(results.failed()).isTrue
val res = results.results[0] val res = results.facts.results[0]
assertThat(res.name).isEqualTo("should fail") assertThat(res.name).isEqualTo("should fail")
assertThat(res.failures).hasSize(1) assertThat(res.failures).hasSize(1)
assertThat(res.errors).hasSize(1) assertThat(res.errors).hasSize(1)
@@ -179,7 +180,121 @@ class EvaluateTestsTest {
assertThat(results.displayUri).startsWith("file:///").endsWith(".pkl") assertThat(results.displayUri).startsWith("file:///").endsWith(".pkl")
assertThat(results.totalTests()).isEqualTo(1) assertThat(results.totalTests()).isEqualTo(1)
assertThat(results.failed()).isFalse assertThat(results.failed()).isFalse
assertThat(results.results[0].name).isEqualTo("user") assertThat(results.examples.results[0].name).isEqualTo("user")
}
@Test
fun `test fact failures with successful example`(@TempDir tempDir: Path) {
val file = tempDir.createTempFile(prefix = "example", suffix = ".pkl")
Files.writeString(
file,
"""
amends "pkl:test"
facts {
["should fail"] {
1 == 2
"foo" == "bar"
}
}
examples {
["user"] {
new {
name = "Bob"
age = 33
}
}
}
"""
.trimIndent()
)
Files.writeString(
createExpected(file),
"""
examples {
["user"] {
new {
name = "Bob"
age = 33
}
}
}
"""
.trimIndent()
)
val results = evaluator.evaluateTest(path(file), false)
assertThat(results.moduleName).startsWith("example")
assertThat(results.displayUri).startsWith("file:///").endsWith(".pkl")
assertThat(results.totalTests()).isEqualTo(2)
assertThat(results.totalFailures()).isEqualTo(1)
assertThat(results.failed()).isTrue
assertThat(results.facts.results[0].name).isEqualTo("should fail")
assertThat(results.facts.results[0].failures.size).isEqualTo(2)
assertThat(results.examples.results[0].name).isEqualTo("user")
}
@Test
fun `test fact error with successful example`(@TempDir tempDir: Path) {
val file = tempDir.createTempFile(prefix = "example", suffix = ".pkl")
Files.writeString(
file,
"""
amends "pkl:test"
facts {
["should fail"] {
throw("exception")
}
}
examples {
["user"] {
new {
name = "Bob"
age = 33
}
}
}
"""
.trimIndent()
)
Files.writeString(
createExpected(file),
"""
examples {
["user"] {
new {
name = "Bob"
age = 33
}
}
}
"""
.trimIndent()
)
val results = evaluator.evaluateTest(path(file), false)
assertThat(results.moduleName).startsWith("example")
assertThat(results.displayUri).startsWith("file:///").endsWith(".pkl")
assertThat(results.totalTests()).isEqualTo(2)
assertThat(results.totalFailures()).isEqualTo(1)
assertThat(results.failed()).isTrue
val res = results.facts.results[0]
assertThat(res.name).isEqualTo("should fail")
assertThat(res.failures).hasSize(0)
assertThat(res.errors).hasSize(1)
val error = res.errors[0]
assertThat(error.message).isEqualTo("exception")
assertThat(results.examples.results[0].name).isEqualTo("user")
} }
@Test @Test
@@ -224,9 +339,9 @@ class EvaluateTestsTest {
assertThat(results.failed()).isTrue assertThat(results.failed()).isTrue
assertThat(results.totalFailures()).isEqualTo(1) assertThat(results.totalFailures()).isEqualTo(1)
val res = results.results[0] val res = results.examples.results[0]
assertThat(res.name).isEqualTo("user") assertThat(res.name).isEqualTo("user")
assertThat(res.errors.isEmpty()).isTrue assertFalse(results.examples.hasError())
val fail1 = res.failures[0] val fail1 = res.failures[0]
assertThat(fail1.rendered.stripFileAndLines(tempDir)) assertThat(fail1.rendered.stripFileAndLines(tempDir))

View File

@@ -30,7 +30,7 @@ class TestsTest : AbstractTest() {
writePklFile() writePklFile()
val res = runTask("evalTest") val res = runTask("evalTest")
assertThat(res.output).contains("should pass") assertThat(res.output).contains("should pass")
} }
@Test @Test
@@ -49,9 +49,9 @@ class TestsTest : AbstractTest() {
) )
val res = runTask("evalTest", expectFailure = true) val res = runTask("evalTest", expectFailure = true)
assertThat(res.output).contains("should fail") assertThat(res.output).contains("should fail")
assertThat(res.output).contains("1 == 3") assertThat(res.output).contains("1 == 3")
assertThat(res.output).contains(""""foo" == "bar" ❌""") assertThat(res.output).contains(""""foo" == "bar"""")
} }
@Test @Test
@@ -68,22 +68,28 @@ class TestsTest : AbstractTest() {
.trimIndent() .trimIndent()
) )
val output = runTask("evalTest", expectFailure = true).output.stripFilesAndLines() val output =
runTask("evalTest", expectFailure = true)
.output
.stripFilesAndLines()
.lineSequence()
.joinToString("\n")
assertThat(output) assertThat(output)
.containsIgnoringNewLines( .containsIgnoringNewLines(
""" """
> Task :evalTest FAILED > Task :evalTest FAILED
module test (file:///file, line x) module test
should pass ✅ facts
error ❌ ✅ should pass
Error: ❌ error
Pkl Error Pkl Error
exception exception
9 | throw("exception") 9 | throw("exception")
^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^
at test#facts["error"][#1] (file:///file, line x) at test#facts["error"][#1] (file:///file, line x)
❌ 50.0% tests pass [1/2 failed], 66.7% asserts pass [1/3 failed]
""" """
.trimIndent() .trimIndent()
) )
@@ -96,41 +102,41 @@ class TestsTest : AbstractTest() {
writeBuildFile() writeBuildFile()
val output = runTask("evalTest", expectFailure = true).output.stripFilesAndLines() val output =
runTask("evalTest", expectFailure = true)
.output
.stripFilesAndLines()
.lineSequence()
.joinToString("\n")
assertThat(output.trimStart()) assertThat(output.trimStart())
.contains( .startsWith(
""" """
module test (file:///file, line x) > Task :evalTest FAILED
sum numbers ✅ pkl: TRACE: 8 = 8 (file:///file, line x)
divide numbers ✅ module test
fail ❌ facts
4 == 9 ❌ (file:///file, line x) ✅ sum numbers
"foo" == "bar" ❌ (file:///file, line x) ✅ divide numbers
user 0 ✅ ❌ fail
user 1 ❌ 4 == 9 (file:///file, line x)
(file:///file, line x) "foo" == "bar" (file:///file, line x)
Expected: (file:///file, line x) examples
new { ✅ user 0
name = "Pigeon" ✅ user 1 #0
age = 40 ❌ user 1 #1
} (file:///file, line x)
Actual: (file:///file, line x) Expected: (file:///file, line x)
new { new {
name = "Pigeon" name = "Parrot"
age = 41 age = 35
} }
(file:///file, line x) Actual: (file:///file, line x)
Expected: (file:///file, line x) new {
new { name = "Welma"
name = "Parrot" age = 35
age = 35 }
} ❌ 66.7% tests pass [2/6 failed], 66.7% asserts pass [3/9 failed]
Actual: (file:///file, line x)
new {
name = "Welma"
age = 35
}
""" """
.trimIndent() .trimIndent()
) )
@@ -138,28 +144,7 @@ class TestsTest : AbstractTest() {
@Test @Test
fun `overwrite expected examples`() { fun `overwrite expected examples`() {
writePklFile( writePklFile(additionalExamples = examples)
additionalExamples =
"""
["user 0"] {
new {
name = "Cool"
age = 11
}
}
["user 1"] {
new {
name = "Pigeon"
age = 41
}
new {
name = "Welma"
age = 35
}
}
"""
.trimIndent()
)
writeFile("test.pkl-expected.pcf", bigTestExpected) writeFile("test.pkl-expected.pcf", bigTestExpected)
writeBuildFile("overwrite = true") writeBuildFile("overwrite = true")
@@ -170,6 +155,64 @@ class TestsTest : AbstractTest() {
assertThat(output).contains("user 1 ✍️") assertThat(output).contains("user 1 ✍️")
} }
@Test
fun `full example with error`() {
writeBuildFile()
writePklFile(
additionalFacts =
"""
["error"] {
throw("exception")
}
"""
.trimIndent(),
additionalExamples = examples
)
writeFile("test.pkl-expected.pcf", bigTestExpected)
val output =
runTask("evalTest", expectFailure = true)
.output
.stripFilesAndLines()
.lineSequence()
.joinToString("\n")
assertThat(output.trimStart())
.startsWith(
"""
> Task :evalTest FAILED
module test
facts
✅ should pass
❌ error
Pkl Error
exception
9 | throw("exception")
^^^^^^^^^^^^^^^^^^
at test#facts["error"][#1] (file:///file, line x)
examples
✅ user 0
✅ user 1 #0
❌ user 1 #1
(file:///file, line x)
Expected: (file:///file, line x)
new {
name = "Parrot"
age = 35
}
Actual: (file:///file, line x)
new {
name = "Welma"
age = 35
}
❌ 60.0% tests pass [2/5 failed], 66.7% asserts pass [2/6 failed]
"""
.trimIndent()
)
}
@Test @Test
fun `JUnit reports`() { fun `JUnit reports`() {
val pklFile = writePklFile(contents = bigTest) val pklFile = writePklFile(contents = bigTest)
@@ -186,26 +229,16 @@ class TestsTest : AbstractTest() {
.isEqualTo( .isEqualTo(
""" """
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<testsuite name="test" tests="5" failures="4"> <testsuite name="test" tests="6" failures="2">
<testcase classname="test" name="sum numbers"></testcase> <testcase classname="test.facts" name="sum numbers"></testcase>
<testcase classname="test" name="divide numbers"></testcase> <testcase classname="test.facts" name="divide numbers"></testcase>
<testcase classname="test" name="fail"> <testcase classname="test.facts" name="fail">
<failure message="Fact Failure">4 == 9 (file:///file, line x)</failure> <failure message="Fact Failure">4 == 9 (file:///file, line x)</failure>
<failure message="Fact Failure">&quot;foo&quot; == &quot;bar&quot; (file:///file, line x)</failure> <failure message="Fact Failure">&quot;foo&quot; == &quot;bar&quot; (file:///file, line x)</failure>
</testcase> </testcase>
<testcase classname="test" name="user 0"></testcase> <testcase classname="test.examples" name="user 0"></testcase>
<testcase classname="test" name="user 1"> <testcase classname="test.examples" name="user 1 #0"></testcase>
<failure message="Example Failure">(file:///file, line x) <testcase classname="test.examples" name="user 1 #1">
Expected: (file:///file, line x)
new {
name = &quot;Pigeon&quot;
age = 40
}
Actual: (file:///file, line x)
new {
name = &quot;Pigeon&quot;
age = 41
}</failure>
<failure message="Example Failure">(file:///file, line x) <failure message="Example Failure">(file:///file, line x)
Expected: (file:///file, line x) Expected: (file:///file, line x)
new { new {
@@ -227,6 +260,86 @@ class TestsTest : AbstractTest() {
) )
} }
@Test
fun `JUnit reports with error`() {
val pklFile =
writePklFile(
additionalFacts =
"""
["error"] {
throw("exception")
}
"""
.trimIndent(),
additionalExamples = examples
)
writeFile("test.pkl-expected.pcf", bigTestExpected)
writeBuildFile("junitReportsDir = file('${pklFile.parent.toNormalizedPathString()}/build')")
runTask("evalTest", expectFailure = true)
val outputFile = testProjectDir.resolve("build/test.xml")
val report = outputFile.readText().stripFilesAndLines()
assertThat(report)
.isEqualTo(
"""
<?xml version="1.0" encoding="UTF-8"?>
<testsuite name="test" tests="5" failures="2">
<testcase classname="test.facts" name="should pass"></testcase>
<testcase classname="test.facts" name="error">
<error message="exception"> Pkl Error
exception
9 | throw(&quot;exception&quot;)
^^^^^^^^^^^^^^^^^^
at test#facts[&quot;error&quot;][#1] (file:///file, line x)
</error>
</testcase>
<testcase classname="test.examples" name="user 0"></testcase>
<testcase classname="test.examples" name="user 1 #0"></testcase>
<testcase classname="test.examples" name="user 1 #1">
<failure message="Example Failure">(file:///file, line x)
Expected: (file:///file, line x)
new {
name = &quot;Parrot&quot;
age = 35
}
Actual: (file:///file, line x)
new {
name = &quot;Welma&quot;
age = 35
}</failure>
</testcase>
</testsuite>
"""
.trimIndent()
)
}
private val examples =
"""
["user 0"] {
new {
name = "Cool"
age = 11
}
}
["user 1"] {
new {
name = "Pigeon"
age = 40
}
new {
name = "Welma"
age = 35
}
}
"""
.trimIndent()
private val bigTest = private val bigTest =
""" """
amends "pkl:test" amends "pkl:test"
@@ -249,22 +362,7 @@ class TestsTest : AbstractTest() {
} }
examples { examples {
["user 0"] { $examples
new {
name = "Cool"
age = 11
}
}
["user 1"] {
new {
name = "Pigeon"
age = 41
}
new {
name = "Welma"
age = 35
}
}
} }
""" """
.trimIndent() .trimIndent()