Use ANSI colors for test results; more polish (#771)

Any thrown Pkl Errors are colored in the simple test report!

Also:
* Refactor `TextFormatter` to be more generic; rename to `TextFormattingStringBuilder`
* Adjust test report slightly (no emojis, add more spacing).
* Introduce `ColorTheme` class.
* Make stack frame descriptors colored as "faint"

Also: this changes the summary so it summarizes _all_ modules, rather than a summary per module.

---------

Co-authored-by: Islon Scherer <islonscherer@gmail.com>
Co-authored-by: Philip K.F. Hölzenspies <holzensp@gmail.com>
This commit is contained in:
Daniel Chao
2024-11-04 14:14:19 -08:00
committed by GitHub
parent 4b4d81ba93
commit 40a08affa6
18 changed files with 688 additions and 381 deletions

View File

@@ -20,6 +20,7 @@ import org.pkl.commons.cli.*
import org.pkl.core.Closeables import org.pkl.core.Closeables
import org.pkl.core.EvaluatorBuilder import org.pkl.core.EvaluatorBuilder
import org.pkl.core.ModuleSource.uri import org.pkl.core.ModuleSource.uri
import org.pkl.core.TestResults
import org.pkl.core.stdlib.test.report.JUnitReport import org.pkl.core.stdlib.test.report.JUnitReport
import org.pkl.core.stdlib.test.report.SimpleReport import org.pkl.core.stdlib.test.report.SimpleReport
import org.pkl.core.util.ErrorMessages import org.pkl.core.util.ErrorMessages
@@ -62,14 +63,17 @@ constructor(
var failed = false var failed = false
var isExampleWrittenFailure = true var isExampleWrittenFailure = true
val moduleNames = mutableSetOf<String>() val moduleNames = mutableSetOf<String>()
val reporter = SimpleReport(useColor)
val allTestResults = mutableListOf<TestResults>()
for ((idx, moduleUri) in sources.withIndex()) { for ((idx, moduleUri) in sources.withIndex()) {
try { try {
val results = evaluator.evaluateTest(uri(moduleUri), testOptions.overwrite) val results = evaluator.evaluateTest(uri(moduleUri), testOptions.overwrite)
allTestResults.add(results)
if (!failed) { if (!failed) {
failed = results.failed() failed = results.failed()
isExampleWrittenFailure = results.isExampleWrittenFailure.and(isExampleWrittenFailure) isExampleWrittenFailure = results.isExampleWrittenFailure.and(isExampleWrittenFailure)
} }
SimpleReport().report(results, consoleWriter) reporter.report(results, consoleWriter)
if (sources.size > 1 && idx != sources.size - 1) { if (sources.size > 1 && idx != sources.size - 1) {
consoleWriter.append('\n') consoleWriter.append('\n')
} }
@@ -102,6 +106,9 @@ constructor(
failed = true failed = true
} }
} }
consoleWriter.append('\n')
reporter.summarize(allTestResults, consoleWriter)
consoleWriter.flush()
if (failed) { if (failed) {
val exitCode = if (isExampleWrittenFailure) 10 else 1 val exitCode = if (isExampleWrittenFailure) 10 else 1
throw CliTestException(ErrorMessages.create("testsFailed"), exitCode) throw CliTestException(ErrorMessages.create("testsFailed"), exitCode)

View File

@@ -37,7 +37,6 @@ import org.pkl.commons.writeString
import org.pkl.core.Release import org.pkl.core.Release
class CliTestRunnerTest { class CliTestRunnerTest {
@Test @Test
fun `CliTestRunner succeed test`(@TempDir tempDir: Path) { fun `CliTestRunner succeed test`(@TempDir tempDir: Path) {
val code = val code =
@@ -65,8 +64,9 @@ class CliTestRunnerTest {
""" """
module test module test
facts facts
succeed succeed
✅ 100.0% tests pass [1 passed], 100.0% asserts pass [2 passed]
100.0% tests pass [1 passed], 100.0% asserts pass [2 passed]
""" """
.trimIndent() .trimIndent()
@@ -101,9 +101,10 @@ class CliTestRunnerTest {
""" """
module test module test
facts facts
fail fail
4 == 9 (/tempDir/test.pkl, line xx) 4 == 9 (/tempDir/test.pkl, line xx)
❌ 0.0% tests pass [1/1 failed], 50.0% asserts pass [1/2 failed]
0.0% tests pass [1/1 failed], 50.0% asserts pass [1/2 failed]
""" """
.trimIndent() .trimIndent()
@@ -137,14 +138,15 @@ class CliTestRunnerTest {
""" """
module test module test
facts facts
fail fail
Pkl Error Pkl Error
uh oh uh oh
5 | throw("uh oh") 5 | throw("uh oh")
^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
at test#facts["fail"][#1] (/tempDir/test.pkl, line xx) at test#facts["fail"][#1] (/tempDir/test.pkl, line xx)
❌ 0.0% tests pass [1/1 failed], 0.0% asserts pass [1/1 failed]
0.0% tests pass [1/1 failed], 0.0% asserts pass [1/1 failed]
""" """
.trimIndent() .trimIndent()
@@ -178,14 +180,15 @@ class CliTestRunnerTest {
""" """
module test module test
examples examples
fail fail
Pkl Error Pkl Error
uh oh uh oh
5 | throw("uh oh") 5 | throw("uh oh")
^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
at test#examples["fail"][#1] (/tempDir/test.pkl, line xx) at test#examples["fail"][#1] (/tempDir/test.pkl, line xx)
❌ 0.0% tests pass [1/1 failed], 0.0% asserts pass [1/1 failed]
0.0% tests pass [1/1 failed], 0.0% asserts pass [1/1 failed]
""" """
.trimIndent() .trimIndent()
@@ -233,14 +236,15 @@ class CliTestRunnerTest {
""" """
module test module test
examples examples
fail fail
Pkl Error Pkl Error
uh oh uh oh
5 | throw("uh oh") 5 | throw("uh oh")
^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
at test#examples["fail"][#1] (/tempDir/test.pkl, line xx) at test#examples["fail"][#1] (/tempDir/test.pkl, line xx)
❌ 0.0% tests pass [1/1 failed], 0.0% asserts pass [1/1 failed]
0.0% tests pass [1/1 failed], 0.0% asserts pass [1/1 failed]
""" """
.trimIndent() .trimIndent()
@@ -435,10 +439,11 @@ class CliTestRunnerTest {
""" """
module test module test
examples examples
nums nums
(/tempDir/test.pkl, line xx) (/tempDir/test.pkl, line xx)
Output mismatch: Expected "nums" to contain 1 examples, but found 2 Output mismatch: Expected "nums" to contain 1 examples, but found 2
❌ 0.0% tests pass [1/1 failed], 0.0% asserts pass [1/1 failed]
0.0% tests pass [1/1 failed], 0.0% asserts pass [1/1 failed]
""" """
.trimIndent() .trimIndent()
@@ -474,6 +479,7 @@ class CliTestRunnerTest {
module test module test
examples examples
✍️ nums ✍️ nums
1 examples written 1 examples written
""" """

View File

@@ -164,6 +164,8 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) {
) )
} }
protected val useColor: Boolean by lazy { cliOptions.color?.hasColor() ?: false }
private val proxyAddress by lazy { private val proxyAddress by lazy {
cliOptions.httpProxy cliOptions.httpProxy
?: project?.evaluatorSettings?.http?.proxy?.address ?: settings.http?.proxy?.address ?: project?.evaluatorSettings?.http?.proxy?.address ?: settings.http?.proxy?.address
@@ -284,7 +286,7 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) {
.setEnvironmentVariables(environmentVariables) .setEnvironmentVariables(environmentVariables)
.addModuleKeyFactories(moduleKeyFactories(modulePathResolver)) .addModuleKeyFactories(moduleKeyFactories(modulePathResolver))
.addResourceReaders(resourceReaders(modulePathResolver)) .addResourceReaders(resourceReaders(modulePathResolver))
.setColor(cliOptions.color?.hasColor() ?: false) .setColor(useColor)
.setLogger(Loggers.stdErr()) .setLogger(Loggers.stdErr())
.setTimeout(cliOptions.timeout) .setTimeout(cliOptions.timeout)
.setModuleCacheDir(moduleCacheDir) .setModuleCacheDir(moduleCacheDir)

View File

@@ -235,7 +235,7 @@ public class EvaluatorImpl implements Evaluator {
return doEvaluate( return doEvaluate(
moduleSource, moduleSource,
(module) -> { (module) -> {
var testRunner = new TestRunner(logger, frameTransformer, overwrite); var testRunner = new TestRunner(logger, frameTransformer, overwrite, color);
return testRunner.run(module); return testRunner.run(module);
}); });
} }

View File

@@ -92,7 +92,7 @@ public record TestResults(
* being written. * being written.
*/ */
public boolean isExampleWrittenFailure() { public boolean isExampleWrittenFailure() {
if (!failed() || !examples.failed()) return false; if (!failed() || facts.failed() || !examples.failed()) return false;
for (var testResult : examples.results) { for (var testResult : examples.results) {
if (!testResult.isExampleWritten) { if (!testResult.isExampleWritten) {
return false; return false;
@@ -295,7 +295,19 @@ public record TestResults(
} }
} }
/**
* Indicates that an exception was thrown when evaluating the assertion.
*
* @param message The message of the underlying exception.
* @param exception The exception thrown by Pkl
*/
public record Error(String message, PklException exception) {} public record Error(String message, PklException exception) {}
/**
* Indicates that an assertion failed.
*
* @param kind The type of assertion failure.
* @param message The detailed message for the failure.
*/
public record Failure(String kind, String message) {} public record Failure(String kind, String message) {}
} }

View File

@@ -0,0 +1,277 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core.runtime;
import java.io.PrintWriter;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;
import org.pkl.core.util.StringBuilderWriter;
@SuppressWarnings("DuplicatedCode")
public final class AnsiCodingStringBuilder {
private final StringBuilder builder = new StringBuilder();
private final boolean usingColor;
/** The set of ansi codes currently applied. */
private Set<AnsiCode> currentCodes = Collections.emptySet();
/** The set of ansi codes intended to be applied the next time text is written. */
private Set<AnsiCode> declaredCodes = Collections.emptySet();
public AnsiCodingStringBuilder(boolean usingColor) {
this.usingColor = usingColor;
}
/** Append {@code value} to the string, ensuring it is formatted with {@code codes}. */
public AnsiCodingStringBuilder append(Set<AnsiCode> codes, String value) {
if (!usingColor) {
builder.append(value);
return this;
}
var prevDeclaredCodes = declaredCodes;
declaredCodes = EnumSet.copyOf(codes);
declaredCodes.addAll(prevDeclaredCodes);
append(value);
declaredCodes = prevDeclaredCodes;
return this;
}
/** Append {@code value} to the string, ensuring it is formatted with {@code codes}. */
public AnsiCodingStringBuilder append(AnsiCode code, int value) {
if (!usingColor) {
builder.append(value);
return this;
}
var prevDeclaredCodes = declaredCodes;
declaredCodes = EnumSet.of(code);
declaredCodes.addAll(prevDeclaredCodes);
append(value);
declaredCodes = prevDeclaredCodes;
return this;
}
/** Append {@code value} to the string, ensuring it is formatted with {@code codes}. */
public AnsiCodingStringBuilder append(AnsiCode code, String value) {
if (!usingColor) {
builder.append(value);
return this;
}
var prevDeclaredCodes = declaredCodes;
declaredCodes = EnumSet.of(code);
declaredCodes.addAll(prevDeclaredCodes);
append(value);
declaredCodes = prevDeclaredCodes;
return this;
}
/**
* Apply {@code code} to every appended element within {@code runnable}.
*
* <p>This is a helper method. With this:
*
* <ul>
* <li>There is no need to repeat the same style for multiple appends in a row.
* <li>The parent style is added to any styles added applied in the children.
* <p>For example, in the following snippet, {@code "hello"} is formatted in both bold and
* red:
* <pre>{@code
* var sb = new AnsiCodingStringBuilder(true);
* sb.append(AnsiCode.RED, () -> {
* sb.append(AnsiCode.BOLD, "hello");
* });
*
* }</pre>
* </ul>
*/
public AnsiCodingStringBuilder append(AnsiCode code, Runnable runnable) {
if (!usingColor) {
runnable.run();
return this;
}
var prevDeclaredCodes = declaredCodes;
declaredCodes = EnumSet.of(code);
declaredCodes.addAll(prevDeclaredCodes);
runnable.run();
declaredCodes = prevDeclaredCodes;
return this;
}
/**
* Append a string whose contents are unknown, and might contain ANSI color codes.
*
* <p>Always add a reset and re-apply all colors after appending the string.
*/
public AnsiCodingStringBuilder appendUntrusted(String value) {
appendCodes();
builder.append(value);
if (usingColor) {
doReset();
doAppendCodes(currentCodes);
}
return this;
}
/**
* Append {@code value} to the string.
*
* <p>If called within {@link #append(AnsiCode, Runnable)}, applies any styles in the current
* context.
*/
public AnsiCodingStringBuilder append(String value) {
appendCodes();
builder.append(value);
return this;
}
/**
* Append the string representation of {@code value} to the string.
*
* <p>If called within {@link #append(AnsiCode, Runnable)}, applies any styles in the current
* context.
*/
public AnsiCodingStringBuilder append(char value) {
appendCodes();
builder.append(value);
return this;
}
/**
* Append the string representation of {@code value} to the string.
*
* <p>If called within {@link #append(AnsiCode, Runnable)}, applies any styles in the current
* context.
*/
public AnsiCodingStringBuilder append(int value) {
appendCodes();
builder.append(value);
return this;
}
/**
* Append the string representation of {@code value} to the string.
*
* <p>If called within {@link #append(AnsiCode, Runnable)}, applies any styles in the current
* context.
*/
public AnsiCodingStringBuilder append(Object value) {
appendCodes();
builder.append(value);
return this;
}
/** Returns a fresh instance of this string builder. */
public AnsiCodingStringBuilder newInstance() {
return new AnsiCodingStringBuilder(usingColor);
}
public PrintWriter toPrintWriter() {
return new PrintWriter(new StringBuilderWriter(builder));
}
/** Builds the data represented by this builder into a {@link String}. */
public String toString() {
// be a good citizen and unset any ansi escape codes currently set.
reset();
return builder.toString();
}
private void doAppendCodes(Set<AnsiCode> codes) {
if (codes.isEmpty()) return;
builder.append("\033[");
var isFirst = true;
for (var code : codes) {
if (isFirst) {
isFirst = false;
} else {
builder.append(';');
}
builder.append(code.value);
}
builder.append('m');
}
private void appendCodes() {
if (!usingColor || currentCodes.equals(declaredCodes)) return;
if (declaredCodes.containsAll(currentCodes)) {
var newCodes = EnumSet.copyOf(declaredCodes);
newCodes.removeAll(currentCodes);
doAppendCodes(newCodes);
} else {
reset();
doAppendCodes(declaredCodes);
}
currentCodes = declaredCodes;
}
private void reset() {
if (!usingColor || currentCodes.isEmpty()) return;
doReset();
currentCodes = Collections.emptySet();
}
private void doReset() {
builder.append("\033[0m");
}
public enum AnsiCode {
RESET(0),
BOLD(1),
FAINT(2),
BLACK(30),
RED(31),
GREEN(32),
YELLOW(33),
BLUE(34),
MAGENTA(35),
CYAN(36),
WHITE(37),
BG_BLACK(40),
BG_RED(41),
BG_GREEN(42),
BG_YELLOW(43),
BG_BLUE(44),
BG_MAGENTA(45),
BG_CYAN(46),
BG_WHITE(47),
BRIGHT_BLACK(90),
BRIGHT_RED(91),
BRIGHT_GREEN(92),
BRIGHT_YELLOW(93),
BRIGHT_BLUE(94),
BRIGHT_MAGENTA(95),
BRIGHT_CYAN(96),
BRIGHT_WHITE(97),
BG_BRIGHT_BLACK(100),
BG_BRIGHT_RED(101),
BG_BRIGHT_GREEN(102),
BG_BRIGHT_YELLOW(103),
BG_BRIGHT_BLUE(104),
BG_BRIGHT_MAGENTA(105),
BG_BRIGHT_CYAN(106),
BG_BRIGHT_WHITE(107);
private final int value;
AnsiCode(int value) {
this.value = value;
}
}
}

View File

@@ -19,7 +19,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.function.Function; import java.util.function.Function;
import org.pkl.core.StackFrame; import org.pkl.core.StackFrame;
import org.pkl.core.runtime.TextFormatter.Element; import org.pkl.core.util.AnsiTheme;
import org.pkl.core.util.Nullable; import org.pkl.core.util.Nullable;
public final class StackTraceRenderer { public final class StackTraceRenderer {
@@ -29,7 +29,7 @@ public final class StackTraceRenderer {
this.frameTransformer = frameTransformer; this.frameTransformer = frameTransformer;
} }
public void render(List<StackFrame> frames, @Nullable String hint, TextFormatter out) { public void render(List<StackFrame> frames, @Nullable String hint, AnsiCodingStringBuilder out) {
var compressed = compressFrames(frames); var compressed = compressFrames(frames);
doRender(compressed, hint, out, "", true); doRender(compressed, hint, out, "", true);
} }
@@ -38,7 +38,7 @@ public final class StackTraceRenderer {
void doRender( void doRender(
List<Object /*StackFrame|StackFrameLoop*/> frames, List<Object /*StackFrame|StackFrameLoop*/> frames,
@Nullable String hint, @Nullable String hint,
TextFormatter out, AnsiCodingStringBuilder out,
String leftMargin, String leftMargin,
boolean isFirstElement) { boolean isFirstElement) {
for (var frame : frames) { for (var frame : frames) {
@@ -48,13 +48,11 @@ public final class StackTraceRenderer {
doRender(loop.frames, null, out, leftMargin, isFirstElement); doRender(loop.frames, null, out, leftMargin, isFirstElement);
} else { } else {
if (!isFirstElement) { if (!isFirstElement) {
out.margin(leftMargin).newline(); out.append(AnsiTheme.STACK_TRACE_MARGIN, leftMargin).append('\n');
} }
out.margin(leftMargin) out.append(AnsiTheme.STACK_TRACE_MARGIN, leftMargin)
.margin("┌─ ") .append(AnsiTheme.STACK_TRACE_MARGIN, "┌─ ")
.style(Element.STACK_OVERFLOW_LOOP_COUNT) .append(AnsiTheme.STACK_TRACE_LOOP_COUNT, loop.count)
.append(loop.count)
.style(Element.TEXT)
.append(" repetitions of:\n"); .append(" repetitions of:\n");
var newLeftMargin = leftMargin + ""; var newLeftMargin = leftMargin + "";
doRender(loop.frames, null, out, newLeftMargin, isFirstElement); doRender(loop.frames, null, out, newLeftMargin, isFirstElement);
@@ -62,11 +60,11 @@ public final class StackTraceRenderer {
renderHint(hint, out, newLeftMargin); renderHint(hint, out, newLeftMargin);
isFirstElement = false; isFirstElement = false;
} }
out.margin(leftMargin).margin("└─").newline(); out.append(AnsiTheme.STACK_TRACE_MARGIN, leftMargin + "└─").append('\n');
} }
} else { } else {
if (!isFirstElement) { if (!isFirstElement) {
out.margin(leftMargin).newline(); out.append(AnsiTheme.STACK_TRACE_MARGIN, leftMargin).append('\n');
} }
renderFrame((StackFrame) frame, out, leftMargin); renderFrame((StackFrame) frame, out, leftMargin);
} }
@@ -78,19 +76,22 @@ public final class StackTraceRenderer {
} }
} }
private void renderFrame(StackFrame frame, TextFormatter out, String leftMargin) { private void renderFrame(StackFrame frame, AnsiCodingStringBuilder out, String leftMargin) {
var transformed = frameTransformer.apply(frame); var transformed = frameTransformer.apply(frame);
renderSourceLine(transformed, out, leftMargin); renderSourceLine(transformed, out, leftMargin);
renderSourceLocation(transformed, out, leftMargin); renderSourceLocation(transformed, out, leftMargin);
} }
private void renderHint(@Nullable String hint, TextFormatter out, String leftMargin) { private void renderHint(@Nullable String hint, AnsiCodingStringBuilder out, String leftMargin) {
if (hint == null || hint.isEmpty()) return; if (hint == null || hint.isEmpty()) return;
out.newline().margin(leftMargin).style(Element.HINT).append(hint).newline(); out.append('\n')
.append(AnsiTheme.STACK_TRACE_MARGIN, leftMargin)
.append(AnsiTheme.ERROR_MESSAGE_HINT, hint)
.append('\n');
} }
private void renderSourceLine(StackFrame frame, TextFormatter out, String leftMargin) { private void renderSourceLine(StackFrame frame, AnsiCodingStringBuilder out, String leftMargin) {
var originalSourceLine = frame.getSourceLines().get(0); var originalSourceLine = frame.getSourceLines().get(0);
var leadingWhitespace = VmUtils.countLeadingWhitespace(originalSourceLine); var leadingWhitespace = VmUtils.countLeadingWhitespace(originalSourceLine);
var sourceLine = originalSourceLine.strip(); var sourceLine = originalSourceLine.strip();
@@ -101,28 +102,28 @@ public final class StackTraceRenderer {
: sourceLine.length(); : sourceLine.length();
var prefix = frame.getStartLine() + " | "; var prefix = frame.getStartLine() + " | ";
out.margin(leftMargin) out.append(AnsiTheme.STACK_TRACE_MARGIN, leftMargin)
.style(Element.LINE_NUMBER) .append(AnsiTheme.STACK_TRACE_LINE_NUMBER, prefix)
.append(prefix)
.style(Element.TEXT)
.append(sourceLine) .append(sourceLine)
.newline() .append('\n')
.margin(leftMargin) .append(AnsiTheme.STACK_TRACE_MARGIN, leftMargin)
.repeat(prefix.length() + startColumn - 1, ' ') .append(" ".repeat(prefix.length() + startColumn - 1))
.style(Element.ERROR) .append(AnsiTheme.STACK_TRACE_CARET, "^".repeat(endColumn - startColumn + 1))
.repeat(endColumn - startColumn + 1, '^') .append('\n');
.newline();
} }
private void renderSourceLocation(StackFrame frame, TextFormatter out, String leftMargin) { private void renderSourceLocation(
out.margin(leftMargin) StackFrame frame, AnsiCodingStringBuilder out, String leftMargin) {
.style(Element.TEXT) out.append(AnsiTheme.STACK_TRACE_MARGIN, leftMargin)
.append("at ") .append(
.append(frame.getMemberName() != null ? frame.getMemberName() : "<unknown>") AnsiTheme.STACK_FRAME,
.append(" (") () ->
.append(frame.getModuleUri()) out.append("at ")
.append(")") .append(frame.getMemberName() != null ? frame.getMemberName() : "<unknown>")
.newline(); .append(" (")
.appendUntrusted(frame.getModuleUri())
.append(")")
.append('\n'));
} }
/** /**

View File

@@ -34,6 +34,7 @@ import org.pkl.core.ast.member.ObjectMember;
import org.pkl.core.module.ModuleKeys; import org.pkl.core.module.ModuleKeys;
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.AnsiTheme;
import org.pkl.core.util.EconomicMaps; import org.pkl.core.util.EconomicMaps;
import org.pkl.core.util.MutableBoolean; import org.pkl.core.util.MutableBoolean;
import org.pkl.core.util.MutableReference; import org.pkl.core.util.MutableReference;
@@ -41,15 +42,20 @@ import org.pkl.core.util.MutableReference;
/** Runs test results examples and facts. */ /** Runs test results examples and facts. */
public final class TestRunner { public final class TestRunner {
private static final PklConverter converter = new PklConverter(VmMapping.empty()); private static final PklConverter converter = new PklConverter(VmMapping.empty());
private final boolean overwrite;
private final StackFrameTransformer stackFrameTransformer;
private final BufferedLogger logger; private final BufferedLogger logger;
private final StackFrameTransformer stackFrameTransformer;
private final boolean overwrite;
private final boolean useColor;
public TestRunner( public TestRunner(
BufferedLogger logger, StackFrameTransformer stackFrameTransformer, boolean overwrite) { BufferedLogger logger,
StackFrameTransformer stackFrameTransformer,
boolean overwrite,
boolean useColor) {
this.logger = logger; this.logger = logger;
this.stackFrameTransformer = stackFrameTransformer; this.stackFrameTransformer = stackFrameTransformer;
this.overwrite = overwrite; this.overwrite = overwrite;
this.useColor = useColor;
} }
public TestResults run(VmTyped testModule) { public TestResults run(VmTyped testModule) {
@@ -60,7 +66,7 @@ public final class TestRunner {
checkAmendsPklTest(testModule); checkAmendsPklTest(testModule);
} catch (VmException v) { } catch (VmException v) {
var error = var error =
new TestResults.Error(v.getMessage(), v.toPklException(stackFrameTransformer, false)); new TestResults.Error(v.getMessage(), v.toPklException(stackFrameTransformer, useColor));
return resultsBuilder.setError(error).build(); return resultsBuilder.setError(error).build();
} }
@@ -109,7 +115,7 @@ public final class TestRunner {
} catch (VmException err) { } catch (VmException err) {
var error = var error =
new TestResults.Error( new TestResults.Error(
err.getMessage(), err.toPklException(stackFrameTransformer, false)); err.getMessage(), err.toPklException(stackFrameTransformer, useColor));
resultBuilder.addError(error); resultBuilder.addError(error);
} }
return true; return true;
@@ -209,7 +215,7 @@ public final class TestRunner {
errored.set(true); errored.set(true);
testResultBuilder.addError( testResultBuilder.addError(
new TestResults.Error( new TestResults.Error(
err.getMessage(), err.toPklException(stackFrameTransformer, false))); err.getMessage(), err.toPklException(stackFrameTransformer, useColor)));
return true; return true;
} }
var expectedValue = VmUtils.readMember(expectedGroup, exampleIndex); var expectedValue = VmUtils.readMember(expectedGroup, exampleIndex);
@@ -306,7 +312,7 @@ public final class TestRunner {
} catch (VmException err) { } catch (VmException err) {
testResultBuilder.addError( testResultBuilder.addError(
new TestResults.Error( new TestResults.Error(
err.getMessage(), err.toPklException(stackFrameTransformer, false))); err.getMessage(), err.toPklException(stackFrameTransformer, useColor)));
allSucceeded.set(false); allSucceeded.set(false);
success.set(false); success.set(false);
return true; return true;
@@ -388,27 +394,32 @@ public final class TestRunner {
moduleInfo.getModuleKey().getUri(), VmContext.get(null).getFrameTransformer()); moduleInfo.getModuleKey().getUri(), VmContext.get(null).getFrameTransformer());
} }
private static Failure factFailure(SourceSection sourceSection, String location) { private Failure factFailure(SourceSection sourceSection, String location) {
String message = sourceSection.getCharacters().toString() + " " + renderLocation(location); var sb = new AnsiCodingStringBuilder(useColor);
return new Failure("Fact Failure", message); sb.append(AnsiTheme.TEST_FACT_SOURCE, sourceSection.getCharacters().toString()).append(" ");
appendLocation(sb, location);
return new Failure("Fact Failure", sb.toString());
} }
private static Failure exampleLengthMismatchFailure( private Failure exampleLengthMismatchFailure(
String location, String property, int expectedLength, int actualLength) { String location, String property, int expectedLength, int actualLength) {
String msg = var sb = new AnsiCodingStringBuilder(useColor);
renderLocation(location) appendLocation(sb, location);
+ "\n"
+ "Output mismatch: Expected \""
+ property
+ "\" to contain "
+ expectedLength
+ " examples, but found "
+ actualLength;
return new Failure("Output Mismatch (Length)", msg); sb.append('\n')
.append(
AnsiTheme.TEST_FAILURE_MESSAGE,
() ->
sb.append("Output mismatch: Expected \"")
.append(property)
.append("\" to contain ")
.append(expectedLength)
.append(" examples, but found ")
.append(actualLength));
return new Failure("Output Mismatch (Length)", sb.toString());
} }
private static Failure examplePropertyMismatchFailure( private Failure examplePropertyMismatchFailure(
String location, String property, boolean isMissingInExpected) { String location, String property, boolean isMissingInExpected) {
String existsIn; String existsIn;
@@ -422,52 +433,58 @@ public final class TestRunner {
missingIn = "actual"; missingIn = "actual";
} }
String message = var sb = new AnsiCodingStringBuilder(useColor);
renderLocation(location) appendLocation(sb, location);
+ "\n"
+ "Output mismatch: \""
+ property
+ "\" exists in "
+ existsIn
+ " but not in "
+ missingIn
+ " output";
return new Failure("Output Mismatch", message); sb.append('\n')
.append(
AnsiTheme.TEST_FAILURE_MESSAGE,
() ->
sb.append("Output mismatch: \"")
.append(property)
.append("\" exists in ")
.append(existsIn)
.append(" but not in ")
.append(missingIn)
.append(" output"));
return new Failure("Output Mismatch", sb.toString());
} }
private static Failure exampleFailure( private Failure exampleFailure(
String location, String location,
String expectedLocation, String expectedLocation,
String expectedValue, String expectedValue,
String actualLocation, String actualLocation,
String actualValue, String actualValue,
int exampleNumber) { int exampleNumber) {
String err = var sb = new AnsiCodingStringBuilder(useColor);
"#" sb.append(AnsiTheme.TEST_NAME, "#" + exampleNumber + ": ");
+ exampleNumber sb.append(
+ " " AnsiTheme.TEST_FAILURE_MESSAGE,
+ renderLocation(location) () -> {
+ ":\n " appendLocation(sb, location);
+ "Expected: " sb.append("\n Expected: ");
+ renderLocation(expectedLocation) appendLocation(sb, expectedLocation);
+ "\n " sb.append("\n ");
+ expectedValue.replaceAll("\n", "\n ") sb.append(AnsiTheme.TEST_EXAMPLE_OUTPUT, expectedValue.replaceAll("\n", "\n "));
+ "\n " sb.append("\n Actual: ");
+ "Actual: " appendLocation(sb, actualLocation);
+ renderLocation(actualLocation) sb.append("\n ");
+ "\n " sb.append(AnsiTheme.TEST_EXAMPLE_OUTPUT, actualValue.replaceAll("\n", "\n "));
+ actualValue.replaceAll("\n", "\n "); });
return new Failure("Example Failure", sb.toString());
return new Failure("Example Failure", err);
} }
private static String renderLocation(String location) { private void appendLocation(AnsiCodingStringBuilder stringBuilder, String location) {
return "(" + location + ")"; stringBuilder.append(
AnsiTheme.STACK_FRAME,
() -> stringBuilder.append("(").appendUntrusted(location).append(")"));
} }
private static Failure writtenExampleOutputFailure(String testName, String location) { private Failure writtenExampleOutputFailure(String testName, String location) {
var message = renderLocation(location) + "\n" + "Wrote expected output for test " + testName; var sb = new AnsiCodingStringBuilder(useColor);
return new Failure("Example Output Written", message); appendLocation(sb, location);
sb.append(AnsiTheme.TEST_FAILURE_MESSAGE, "\nWrote expected output for test ").append(testName);
return new Failure("Example Output Written", sb.toString());
} }
} }

View File

@@ -1,185 +0,0 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core.runtime;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import org.pkl.core.util.Nullable;
import org.pkl.core.util.StringBuilderWriter;
/*
TODO:
* Make "margin matter" a facility of the formatter, managing margins in e.g. `newline()`.
- `pushMargin(String matter)` / `popMargin()`
* Replace implementation methods `repeat()` with more semantic equivalents.
- `underline(int startColumn, int endColumn)`
* Replace `newInstance()` with an alternative that doesn't require instance management,
i.e. better composition (currently only used for pre-rendering `hint`s).
* Assert assumed invariants (e.g. `append(String text)` checking there are no newlines).
* Replace `THEME_ANSI` with one read from `pkl:settings`.
*/
public final class TextFormatter {
public static final Map<Element, @Nullable Styling> THEME_PLAIN = new HashMap<>();
public static final Map<Element, @Nullable Styling> THEME_ANSI;
static {
THEME_ANSI =
Map.of(
Element.MARGIN, new Styling(Color.YELLOW, true, false),
Element.HINT, new Styling(Color.YELLOW, true, true),
Element.STACK_OVERFLOW_LOOP_COUNT, new Styling(Color.MAGENTA, false, false),
Element.LINE_NUMBER, new Styling(Color.BLUE, false, false),
Element.ERROR_HEADER, new Styling(Color.RED, false, false),
Element.ERROR, new Styling(Color.RED, false, true));
}
private final Map<Element, @Nullable Styling> theme;
private final StringBuilder builder = new StringBuilder();
private @Nullable Styling currentStyle;
private TextFormatter(Map<Element, Styling> theme) {
this.theme = theme;
this.currentStyle = theme.getOrDefault(Element.PLAIN, null);
}
public static TextFormatter create(boolean usingColor) {
return new TextFormatter(usingColor ? THEME_ANSI : THEME_PLAIN);
}
public PrintWriter toPrintWriter() {
return new PrintWriter(new StringBuilderWriter(builder));
}
public String toString() {
return builder.toString();
}
public TextFormatter newline() {
return newlines(1);
}
public TextFormatter newInstance() {
return new TextFormatter(theme);
}
public TextFormatter newlines(int count) {
return repeat(count, '\n');
}
public TextFormatter margin(String marginMatter) {
return style(Element.MARGIN).append(marginMatter);
}
public TextFormatter style(Element element) {
var style = theme.getOrDefault(element, null);
if (currentStyle == style) {
return this;
}
if (style == null) {
append("\033[0m");
currentStyle = style;
return this;
}
var colorCode =
style.bright() ? style.foreground().fgBrightCode() : style.foreground().fgCode();
append('\033');
append('[');
append(colorCode);
if (style.bold() && (currentStyle == null || !currentStyle.bold())) {
append(";1");
} else if (!style.bold() && currentStyle != null && currentStyle.bold()) {
append(";22");
}
append('m');
currentStyle = style;
return this;
}
public TextFormatter repeat(int width, char ch) {
for (var i = 0; i < width; i++) {
append(ch);
}
return this;
}
public TextFormatter append(String s) {
builder.append(s);
return this;
}
public TextFormatter append(char ch) {
builder.append(ch);
return this;
}
public TextFormatter append(int i) {
builder.append(i);
return this;
}
public TextFormatter append(Object obj) {
builder.append(obj);
return this;
}
public enum Element {
PLAIN,
MARGIN,
HINT,
STACK_OVERFLOW_LOOP_COUNT,
LINE_NUMBER,
TEXT,
ERROR_HEADER,
ERROR
}
public record Styling(Color foreground, boolean bold, boolean bright) {}
public enum Color {
BLACK(30),
RED(31),
GREEN(32),
YELLOW(33),
BLUE(34),
MAGENTA(35),
CYAN(36),
WHITE(37);
private final int code;
Color(int code) {
this.code = code;
}
public int fgCode() {
return code;
}
public int bgCode() {
return code + 10;
}
public int fgBrightCode() {
return code + 60;
}
public int bgBrightCode() {
return code + 70;
}
}
}

View File

@@ -20,7 +20,7 @@ import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.pkl.core.Release; import org.pkl.core.Release;
import org.pkl.core.runtime.TextFormatter.Element; import org.pkl.core.util.AnsiTheme;
import org.pkl.core.util.ErrorMessages; import org.pkl.core.util.ErrorMessages;
import org.pkl.core.util.Nullable; import org.pkl.core.util.Nullable;
@@ -39,12 +39,12 @@ public final class VmExceptionRenderer {
@TruffleBoundary @TruffleBoundary
public String render(VmException exception) { public String render(VmException exception) {
var formatter = TextFormatter.create(color); var formatter = new AnsiCodingStringBuilder(color);
render(exception, formatter); render(exception, formatter);
return formatter.toString(); return formatter.toString();
} }
private void render(VmException exception, TextFormatter out) { private void render(VmException exception, AnsiCodingStringBuilder out) {
if (exception instanceof VmBugException bugException) { if (exception instanceof VmBugException bugException) {
renderBugException(bugException, out); renderBugException(bugException, out);
} else { } else {
@@ -52,31 +52,31 @@ public final class VmExceptionRenderer {
} }
} }
private void renderBugException(VmBugException exception, TextFormatter out) { private void renderBugException(VmBugException exception, AnsiCodingStringBuilder out) {
// if a cause exists, it's more useful to report just that // if a cause exists, it's more useful to report just that
var exceptionToReport = exception.getCause() != null ? exception.getCause() : exception; var exceptionToReport = exception.getCause() != null ? exception.getCause() : exception;
var exceptionUrl = URLEncoder.encode(exceptionToReport.toString(), StandardCharsets.UTF_8); var exceptionUrl = URLEncoder.encode(exceptionToReport.toString(), StandardCharsets.UTF_8);
out.style(Element.TEXT) out.append("An unexpected error has occurred. Would you mind filing a bug report?")
.append("An unexpected error has occurred. Would you mind filing a bug report?") .append('\n')
.newline()
.append("Cmd+Double-click the link below to open an issue.") .append("Cmd+Double-click the link below to open an issue.")
.newline() .append('\n')
.append("Please copy and paste the entire error output into the issue's description.") .append("Please copy and paste the entire error output into the issue's description.")
.newlines(2) .append("\n".repeat(2))
.append("https://github.com/apple/pkl/issues/new") .append("https://github.com/apple/pkl/issues/new")
.newlines(2) .append("\n".repeat(2))
.append(exceptionUrl.replaceAll("\\+", "%20")) .append(exceptionUrl.replaceAll("\\+", "%20"))
.newlines(2); .append("\n\n");
renderException(exception, out, true); renderException(exception, out, true);
out.newline().style(Element.TEXT).append(Release.current().versionInfo()).newlines(2); out.append('\n').append(Release.current().versionInfo()).append("\n".repeat(2));
exceptionToReport.printStackTrace(out.toPrintWriter()); exceptionToReport.printStackTrace(out.toPrintWriter());
} }
private void renderException(VmException exception, TextFormatter out, boolean withHeader) { private void renderException(
VmException exception, AnsiCodingStringBuilder out, boolean withHeader) {
String message; String message;
var hint = exception.getHint(); var hint = exception.getHint();
if (exception.isExternalMessage()) { if (exception.isExternalMessage()) {
@@ -97,9 +97,9 @@ public final class VmExceptionRenderer {
} }
if (withHeader) { if (withHeader) {
out.style(Element.ERROR_HEADER).append(" Pkl Error ").newline(); out.append(AnsiTheme.ERROR_HEADER, " Pkl Error ").append('\n');
} }
out.style(Element.ERROR).append(message).newline(); out.append(AnsiTheme.ERROR_MESSAGE, message).append('\n');
// include cause's message unless it's the same as this exception's message // include cause's message unless it's the same as this exception's message
if (exception.getCause() != null) { if (exception.getCause() != null) {
@@ -107,11 +107,7 @@ public final class VmExceptionRenderer {
var causeMessage = cause.getMessage(); var causeMessage = cause.getMessage();
// null for Truffle's LazyStackTrace // null for Truffle's LazyStackTrace
if (causeMessage != null && !causeMessage.equals(message)) { if (causeMessage != null && !causeMessage.equals(message)) {
out.style(Element.TEXT) out.append(cause.getClass().getSimpleName()).append(": ").append(causeMessage).append('\n');
.append(cause.getClass().getSimpleName())
.append(": ")
.append(causeMessage)
.newline();
} }
} }
@@ -119,12 +115,11 @@ public final class VmExceptionRenderer {
exception.getProgramValues().stream().mapToInt(v -> v.name.length()).max().orElse(0); exception.getProgramValues().stream().mapToInt(v -> v.name.length()).max().orElse(0);
for (var value : exception.getProgramValues()) { for (var value : exception.getProgramValues()) {
out.style(Element.TEXT) out.append(value.name)
.append(value.name) .append(" ".repeat(Math.max(0, maxNameLength - value.name.length())))
.repeat(Math.max(0, maxNameLength - value.name.length()), ' ')
.append(": ") .append(": ")
.append(value) .append(value)
.newline(); .append('\n');
} }
if (stackTraceRenderer != null) { if (stackTraceRenderer != null) {
@@ -137,10 +132,10 @@ public final class VmExceptionRenderer {
} }
if (!frames.isEmpty()) { if (!frames.isEmpty()) {
stackTraceRenderer.render(frames, hint, out.newline()); stackTraceRenderer.render(frames, hint, out.append('\n'));
} else if (hint != null) { } else if (hint != null) {
// render hint if there are no stack frames // render hint if there are no stack frames
out.newline().style(Element.HINT).append(hint); out.append('\n').append(AnsiTheme.ERROR_MESSAGE_HINT, hint);
} }
} }
} }

View File

@@ -105,7 +105,9 @@ public final class JUnitReport implements TestReport {
long element = i++; long element = i++;
list.add( list.add(
buildXmlElement( buildXmlElement(
"failure", attrs, members -> members.put(element, syntheticElement(fail.message())))); "failure",
attrs,
members -> members.put(element, syntheticElement(stripColors(fail.message())))));
} }
return list; return list;
} }
@@ -120,7 +122,9 @@ public final class JUnitReport implements TestReport {
buildXmlElement( buildXmlElement(
"error", "error",
attrs, attrs,
members -> members.put(element, syntheticElement(error.exception().getMessage())))); members ->
members.put(
element, syntheticElement(stripColors(error.exception().getMessage())))));
} }
return list; return list;
} }
@@ -132,7 +136,9 @@ public final class JUnitReport implements TestReport {
buildXmlElement( buildXmlElement(
"error", "error",
attrs, attrs,
members -> members.put(1, syntheticElement("\n" + error.exception().getMessage())))); members ->
members.put(
1, syntheticElement(stripColors("\n" + error.exception().getMessage())))));
return list; return list;
} }
@@ -191,7 +197,11 @@ public final class JUnitReport implements TestReport {
return new VmTyped(VmUtils.createEmptyMaterializedFrame(), clazz.getPrototype(), clazz, attrs); return new VmTyped(VmUtils.createEmptyMaterializedFrame(), clazz.getPrototype(), clazz, attrs);
} }
public static String renderXML(String indent, String version, VmDynamic value) { private String stripColors(String str) {
return str.replaceAll("\033\\[[;\\d]*m", "");
}
private static String renderXML(String indent, String version, VmDynamic value) {
var builder = new StringBuilder(); var builder = new StringBuilder();
var converter = new PklConverter(VmMapping.empty()); var converter = new PklConverter(VmMapping.empty());
var renderer = new Renderer(builder, indent, version, "", VmMapping.empty(), converter); var renderer = new Renderer(builder, indent, version, "", VmMapping.empty(), converter);

View File

@@ -17,19 +17,32 @@ package org.pkl.core.stdlib.test.report;
import java.io.IOException; import java.io.IOException;
import java.io.Writer; import java.io.Writer;
import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.pkl.core.TestResults; import org.pkl.core.TestResults;
import org.pkl.core.TestResults.TestResult; import org.pkl.core.TestResults.TestResult;
import org.pkl.core.TestResults.TestSectionResults; import org.pkl.core.TestResults.TestSectionResults;
import org.pkl.core.runtime.AnsiCodingStringBuilder;
import org.pkl.core.runtime.AnsiCodingStringBuilder.AnsiCode;
import org.pkl.core.util.AnsiTheme;
import org.pkl.core.util.StringUtils; import org.pkl.core.util.StringUtils;
public final class SimpleReport implements TestReport { public final class SimpleReport implements TestReport {
private static final String passingMark = "";
private static final String failingMark = "";
private final boolean useColor;
public SimpleReport(boolean useColor) {
this.useColor = useColor;
}
@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 AnsiCodingStringBuilder(useColor);
builder.append("module ").append(results.moduleName()).append("\n"); builder.append("module ").append(results.moduleName()).append('\n');
if (results.error() != null) { if (results.error() != null) {
var rendered = results.error().exception().getMessage(); var rendered = results.error().exception().getMessage();
@@ -40,45 +53,61 @@ public final class SimpleReport implements TestReport {
reportResults(results.examples(), builder); reportResults(results.examples(), builder);
} }
if (results.isExampleWrittenFailure()) { writer.append(builder.toString());
builder.append(results.examples().totalFailures()).append(" examples written\n");
writer.append(builder);
return;
}
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");
writer.append(builder);
} }
private void reportResults(TestSectionResults section, StringBuilder builder) { public void summarize(List<TestResults> allTestResults, Writer writer) throws IOException {
if (!section.results().isEmpty()) { var totalTests = 0;
builder.append(" ").append(section.name()).append("\n"); var totalFailedTests = 0;
var totalAsserts = 0;
var totalFailedAsserts = 0;
var isFailed = false;
var isExampleWrittenFailure = true;
for (var testResults : allTestResults) {
if (!isFailed) {
isFailed = testResults.failed();
}
if (testResults.failed()) {
isExampleWrittenFailure = testResults.isExampleWrittenFailure() & isExampleWrittenFailure;
}
totalTests += testResults.totalTests();
totalFailedTests += testResults.totalFailures();
totalAsserts += testResults.totalAsserts();
totalFailedAsserts += testResults.totalAssertsFailed();
}
var builder = new AnsiCodingStringBuilder(useColor);
if (isFailed && isExampleWrittenFailure) {
builder.append(totalFailedTests).append(" examples written");
} else {
makeStatsLine(builder, "tests", totalTests, totalFailedTests, isFailed);
builder.append(", ");
makeStatsLine(builder, "asserts", totalAsserts, totalFailedAsserts, isFailed);
}
builder.append('\n');
writer.append(builder.toString());
}
private void reportResults(TestSectionResults section, AnsiCodingStringBuilder builder) {
if (!section.results().isEmpty()) {
builder.append(" ").append(section.name()).append('\n');
StringUtils.joinToStringBuilder( StringUtils.joinToStringBuilder(
builder, section.results(), "\n", res -> reportResult(res, builder)); builder, section.results(), "\n", res -> reportResult(res, builder));
builder.append("\n"); builder.append('\n');
} }
} }
private void reportResult(TestResult result, StringBuilder builder) { private void reportResult(TestResult result, AnsiCodingStringBuilder builder) {
builder.append(" "); builder.append(" ");
if (result.isExampleWritten()) { if (result.isExampleWritten()) {
builder.append("✍️ ").append(result.name()); builder.append("✍️ ").append(result.name());
} else { } else {
builder.append(result.isFailure() ? "" : "").append(result.name()); if (result.isFailure()) {
builder.append(AnsiTheme.FAILING_TEST_MARK, failingMark);
} else {
builder.append(AnsiTheme.PASSING_TEST_MARK, passingMark);
}
builder.append(AnsiTheme.TEST_NAME, result.name());
if (result.isFailure()) { if (result.isFailure()) {
var failurePadding = " "; var failurePadding = " ";
builder.append("\n"); builder.append("\n");
@@ -96,7 +125,7 @@ public final class SimpleReport implements TestReport {
} }
} }
private static void appendPadded(StringBuilder builder, String lines, String padding) { private static void appendPadded(AnsiCodingStringBuilder builder, String lines, String padding) {
StringUtils.joinToStringBuilder( StringUtils.joinToStringBuilder(
builder, builder,
lines.lines().collect(Collectors.toList()), lines.lines().collect(Collectors.toList()),
@@ -106,18 +135,21 @@ public final class SimpleReport implements TestReport {
}); });
} }
private String makeStatsLine(String kind, int total, int failed, boolean isFailed) { private void makeStatsLine(
AnsiCodingStringBuilder sb, String kind, int total, int failed, boolean isFailed) {
var passed = total - failed; var passed = total - failed;
var passRate = total > 0 ? 100.0 * passed / total : 0.0; var passRate = total > 0 ? 100.0 * passed / total : 0.0;
String line = String.format("%.1f%% %s pass", passRate, kind); var color = isFailed ? AnsiCode.RED : AnsiCode.GREEN;
sb.append(
color,
() ->
sb.append(String.format("%.1f%%", passRate)).append(" ").append(kind).append(" pass"));
if (isFailed) { if (isFailed) {
line += String.format(" [%d/%d failed]", failed, total); sb.append(" [").append(failed).append('/').append(total).append(" failed]");
} else { } else {
line += String.format(" [%d passed]", passed); sb.append(" [").append(passed).append(" passed]");
} }
return line;
} }
} }

View File

@@ -0,0 +1,41 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core.util;
import java.util.EnumSet;
import java.util.Set;
import org.pkl.core.runtime.AnsiCodingStringBuilder.AnsiCode;
public final class AnsiTheme {
private AnsiTheme() {}
public static final AnsiCode ERROR_MESSAGE_HINT = AnsiCode.YELLOW;
public static final AnsiCode ERROR_HEADER = AnsiCode.RED;
public static final Set<AnsiCode> ERROR_MESSAGE = EnumSet.of(AnsiCode.RED, AnsiCode.BOLD);
public static final AnsiCode STACK_FRAME = AnsiCode.FAINT;
public static final AnsiCode STACK_TRACE_MARGIN = AnsiCode.YELLOW;
public static final AnsiCode STACK_TRACE_LINE_NUMBER = AnsiCode.BLUE;
public static final AnsiCode STACK_TRACE_LOOP_COUNT = AnsiCode.MAGENTA;
public static final AnsiCode STACK_TRACE_CARET = AnsiCode.RED;
public static final AnsiCode FAILING_TEST_MARK = AnsiCode.RED;
public static final AnsiCode PASSING_TEST_MARK = AnsiCode.GREEN;
public static final AnsiCode TEST_NAME = AnsiCode.FAINT;
public static final AnsiCode TEST_FACT_SOURCE = AnsiCode.RED;
public static final AnsiCode TEST_FAILURE_MESSAGE = AnsiCode.RED;
public static final Set<AnsiCode> TEST_EXAMPLE_OUTPUT = EnumSet.of(AnsiCode.RED, AnsiCode.BOLD);
}

View File

@@ -16,6 +16,7 @@
package org.pkl.core.util; package org.pkl.core.util;
import java.util.function.Consumer; import java.util.function.Consumer;
import org.pkl.core.runtime.AnsiCodingStringBuilder;
// Some code in this class was taken from the following Google Guava classes: // Some code in this class was taken from the following Google Guava classes:
// * com.google.common.base.CharMatcher // * com.google.common.base.CharMatcher
@@ -76,7 +77,7 @@ public final class StringUtils {
public static <T> void joinToStringBuilder( public static <T> void joinToStringBuilder(
StringBuilder builder, Iterable<T> coll, String delimiter, Consumer<T> eachFn) { StringBuilder builder, Iterable<T> coll, String delimiter, Consumer<T> eachFn) {
int i = 0; var i = 0;
for (var v : coll) { for (var v : coll) {
if (i++ != 0) { if (i++ != 0) {
builder.append(delimiter); builder.append(delimiter);
@@ -85,6 +86,18 @@ public final class StringUtils {
} }
} }
public static <T> void joinToStringBuilder(
AnsiCodingStringBuilder builder, Iterable<T> coll, String delimiter, Consumer<T> eachFn) {
var i = 0;
for (var v : coll) {
if (i != 0) {
builder.append(delimiter);
}
eachFn.accept(v);
i++;
}
}
public static <T> void joinToStringBuilder( public static <T> void joinToStringBuilder(
StringBuilder builder, Iterable<T> coll, String delimiter) { StringBuilder builder, Iterable<T> coll, String delimiter) {
joinToStringBuilder(builder, coll, delimiter, builder::append); joinToStringBuilder(builder, coll, delimiter, builder::append);

View File

@@ -125,7 +125,7 @@ class EvaluateTestsTest {
val error = res.errors[0] val error = res.errors[0]
assertThat(error.message).isEqualTo("got an error") assertThat(error.message).isEqualTo("got an error")
assertThat(error.exception.message) assertThat(error.exception().message)
.isEqualTo( .isEqualTo(
""" """
Pkl Error Pkl Error
@@ -347,7 +347,7 @@ class EvaluateTestsTest {
assertThat(fail1.message.stripFileAndLines(tempDir)) assertThat(fail1.message.stripFileAndLines(tempDir))
.isEqualTo( .isEqualTo(
""" """
#0 (/tempDir/example.pkl): #0: (/tempDir/example.pkl)
Expected: (/tempDir/example.pkl-expected.pcf) Expected: (/tempDir/example.pkl-expected.pcf)
new { new {
name = "Alice" name = "Alice"

View File

@@ -0,0 +1,76 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core.runtime
import java.util.*
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.pkl.core.runtime.AnsiCodingStringBuilder.AnsiCode
class AnsiCodingStringBuilderTest {
@Test
fun `no formatting`() {
val result = AnsiCodingStringBuilder(false).append(AnsiCode.RED, "hello").toString()
assertThat(result).isEqualTo("hello")
}
private val red = "\u001b[31m"
private val redBold = "\u001b[1;31m"
private val reset = "\u001b[0m"
private val bold = "\u001b[1m"
// make test failures easier to debug
private val String.escaped
get() = replace("\u001b", "[ESC]")
@Test
fun `don't emit same color code`() {
val result =
AnsiCodingStringBuilder(true).append(AnsiCode.RED, "hi").append(AnsiCode.RED, "hi").toString()
assertThat(result.escaped).isEqualTo("${red}hihi${reset}".escaped)
}
@Test
fun `only add needed codes`() {
val result =
AnsiCodingStringBuilder(true)
.append(AnsiCode.RED, "hi")
.append(EnumSet.of(AnsiCode.RED, AnsiCode.BOLD), "hi")
.toString()
assertThat(result.escaped).isEqualTo("${red}hi${bold}hi${reset}".escaped)
}
@Test
fun `reset if need to subtract`() {
val result =
AnsiCodingStringBuilder(true)
.append(EnumSet.of(AnsiCode.RED, AnsiCode.BOLD), "hi")
.append(AnsiCode.RED, "hi")
.toString()
assertThat(result.escaped).isEqualTo("${redBold}hi${reset}${red}hi${reset}".escaped)
}
@Test
fun `plain text in between`() {
val result =
AnsiCodingStringBuilder(true)
.append(AnsiCode.RED, "hi")
.append("hi")
.append(AnsiCode.RED, "hi")
.toString()
assertThat(result.escaped).isEqualTo("${red}hi${reset}hi${red}hi${reset}".escaped)
}
}

View File

@@ -190,7 +190,7 @@ class StackTraceRendererTest {
} }
val loop = StackTraceRenderer.StackFrameLoop(loopFrames, 1) val loop = StackTraceRenderer.StackFrameLoop(loopFrames, 1)
val frames = listOf(createFrame("bar", 1), createFrame("baz", 2), loop) val frames = listOf(createFrame("bar", 1), createFrame("baz", 2), loop)
val formatter = TextFormatter.create(false) val formatter = AnsiCodingStringBuilder(false)
renderer.doRender(frames, null, formatter, "", true) renderer.doRender(frames, null, formatter, "", true)
val renderedFrames = formatter.toString() val renderedFrames = formatter.toString()
assertThat(renderedFrames) assertThat(renderedFrames)

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,7 +49,7 @@ 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"""")
} }
@@ -81,15 +81,16 @@ class TestsTest : AbstractTest() {
> Task :evalTest FAILED > Task :evalTest FAILED
module test module test
facts facts
should pass 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]
50.0% tests pass [1/2 failed], 66.7% asserts pass [1/3 failed]
""" """
.trimIndent() .trimIndent()
) )
@@ -118,15 +119,15 @@ class TestsTest : AbstractTest() {
pkl: TRACE: 8 = 8 (file:///file, line x) pkl: TRACE: 8 = 8 (file:///file, line x)
module test module test
facts facts
sum numbers sum numbers
divide numbers divide numbers
fail fail
4 == 9 (file:///file, line x) 4 == 9 (file:///file, line x)
"foo" == "bar" (file:///file, line x) "foo" == "bar" (file:///file, line x)
examples examples
user 0 user 0
user 1 user 1
#1 (file:///file, line x): #1: (file:///file, line x)
Expected: (file:///file, line x) Expected: (file:///file, line x)
new { new {
name = "Parrot" name = "Parrot"
@@ -137,7 +138,8 @@ class TestsTest : AbstractTest() {
name = "Welma" name = "Welma"
age = 35 age = 35
} }
❌ 60.0% tests pass [2/5 failed], 66.7% asserts pass [3/9 failed]
60.0% tests pass [2/5 failed], 66.7% asserts pass [3/9 failed]
""" """
.trimIndent() .trimIndent()
) )
@@ -187,8 +189,8 @@ class TestsTest : AbstractTest() {
> Task :evalTest FAILED > Task :evalTest FAILED
module test module test
facts facts
should pass should pass
error error
Pkl Error Pkl Error
exception exception
@@ -196,9 +198,9 @@ class TestsTest : AbstractTest() {
^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^
at test#facts["error"][#1] (file:///file, line x) at test#facts["error"][#1] (file:///file, line x)
examples examples
user 0 user 0
user 1 user 1
#1 (file:///file, line x): #1: (file:///file, line x)
Expected: (file:///file, line x) Expected: (file:///file, line x)
new { new {
name = "Parrot" name = "Parrot"
@@ -209,7 +211,8 @@ class TestsTest : AbstractTest() {
name = "Welma" name = "Welma"
age = 35 age = 35
} }
❌ 50.0% tests pass [2/4 failed], 66.7% asserts pass [2/6 failed]
50.0% tests pass [2/4 failed], 66.7% asserts pass [2/6 failed]
""" """
.trimIndent() .trimIndent()
@@ -241,7 +244,7 @@ class TestsTest : AbstractTest() {
</testcase> </testcase>
<testcase classname="test.examples" name="user 0"></testcase> <testcase classname="test.examples" name="user 0"></testcase>
<testcase classname="test.examples" name="user 1"> <testcase classname="test.examples" name="user 1">
<failure message="Example Failure">#1 (file:///file, line x): <failure message="Example Failure">#1: (file:///file, line x)
Expected: (file:///file, line x) Expected: (file:///file, line x)
new { new {
name = &quot;Parrot&quot; name = &quot;Parrot&quot;
@@ -301,7 +304,7 @@ class TestsTest : AbstractTest() {
</testcase> </testcase>
<testcase classname="test.examples" name="user 0"></testcase> <testcase classname="test.examples" name="user 0"></testcase>
<testcase classname="test.examples" name="user 1"> <testcase classname="test.examples" name="user 1">
<failure message="Example Failure">#1 (file:///file, line x): <failure message="Example Failure">#1: (file:///file, line x)
Expected: (file:///file, line x) Expected: (file:///file, line x)
new { new {
name = &quot;Parrot&quot; name = &quot;Parrot&quot;