mirror of
https://github.com/apple/pkl.git
synced 2026-03-29 13:21:58 +02:00
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:
@@ -235,7 +235,7 @@ public class EvaluatorImpl implements Evaluator {
|
||||
return doEvaluate(
|
||||
moduleSource,
|
||||
(module) -> {
|
||||
var testRunner = new TestRunner(logger, frameTransformer, overwrite);
|
||||
var testRunner = new TestRunner(logger, frameTransformer, overwrite, color);
|
||||
return testRunner.run(module);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ public record TestResults(
|
||||
* being written.
|
||||
*/
|
||||
public boolean isExampleWrittenFailure() {
|
||||
if (!failed() || !examples.failed()) return false;
|
||||
if (!failed() || facts.failed() || !examples.failed()) return false;
|
||||
for (var testResult : examples.results) {
|
||||
if (!testResult.isExampleWritten) {
|
||||
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) {}
|
||||
|
||||
/**
|
||||
* 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) {}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import org.pkl.core.StackFrame;
|
||||
import org.pkl.core.runtime.TextFormatter.Element;
|
||||
import org.pkl.core.util.AnsiTheme;
|
||||
import org.pkl.core.util.Nullable;
|
||||
|
||||
public final class StackTraceRenderer {
|
||||
@@ -29,7 +29,7 @@ public final class StackTraceRenderer {
|
||||
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);
|
||||
doRender(compressed, hint, out, "", true);
|
||||
}
|
||||
@@ -38,7 +38,7 @@ public final class StackTraceRenderer {
|
||||
void doRender(
|
||||
List<Object /*StackFrame|StackFrameLoop*/> frames,
|
||||
@Nullable String hint,
|
||||
TextFormatter out,
|
||||
AnsiCodingStringBuilder out,
|
||||
String leftMargin,
|
||||
boolean isFirstElement) {
|
||||
for (var frame : frames) {
|
||||
@@ -48,13 +48,11 @@ public final class StackTraceRenderer {
|
||||
doRender(loop.frames, null, out, leftMargin, isFirstElement);
|
||||
} else {
|
||||
if (!isFirstElement) {
|
||||
out.margin(leftMargin).newline();
|
||||
out.append(AnsiTheme.STACK_TRACE_MARGIN, leftMargin).append('\n');
|
||||
}
|
||||
out.margin(leftMargin)
|
||||
.margin("┌─ ")
|
||||
.style(Element.STACK_OVERFLOW_LOOP_COUNT)
|
||||
.append(loop.count)
|
||||
.style(Element.TEXT)
|
||||
out.append(AnsiTheme.STACK_TRACE_MARGIN, leftMargin)
|
||||
.append(AnsiTheme.STACK_TRACE_MARGIN, "┌─ ")
|
||||
.append(AnsiTheme.STACK_TRACE_LOOP_COUNT, loop.count)
|
||||
.append(" repetitions of:\n");
|
||||
var newLeftMargin = leftMargin + "│ ";
|
||||
doRender(loop.frames, null, out, newLeftMargin, isFirstElement);
|
||||
@@ -62,11 +60,11 @@ public final class StackTraceRenderer {
|
||||
renderHint(hint, out, newLeftMargin);
|
||||
isFirstElement = false;
|
||||
}
|
||||
out.margin(leftMargin).margin("└─").newline();
|
||||
out.append(AnsiTheme.STACK_TRACE_MARGIN, leftMargin + "└─").append('\n');
|
||||
}
|
||||
} else {
|
||||
if (!isFirstElement) {
|
||||
out.margin(leftMargin).newline();
|
||||
out.append(AnsiTheme.STACK_TRACE_MARGIN, leftMargin).append('\n');
|
||||
}
|
||||
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);
|
||||
renderSourceLine(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;
|
||||
|
||||
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 leadingWhitespace = VmUtils.countLeadingWhitespace(originalSourceLine);
|
||||
var sourceLine = originalSourceLine.strip();
|
||||
@@ -101,28 +102,28 @@ public final class StackTraceRenderer {
|
||||
: sourceLine.length();
|
||||
|
||||
var prefix = frame.getStartLine() + " | ";
|
||||
out.margin(leftMargin)
|
||||
.style(Element.LINE_NUMBER)
|
||||
.append(prefix)
|
||||
.style(Element.TEXT)
|
||||
out.append(AnsiTheme.STACK_TRACE_MARGIN, leftMargin)
|
||||
.append(AnsiTheme.STACK_TRACE_LINE_NUMBER, prefix)
|
||||
.append(sourceLine)
|
||||
.newline()
|
||||
.margin(leftMargin)
|
||||
.repeat(prefix.length() + startColumn - 1, ' ')
|
||||
.style(Element.ERROR)
|
||||
.repeat(endColumn - startColumn + 1, '^')
|
||||
.newline();
|
||||
.append('\n')
|
||||
.append(AnsiTheme.STACK_TRACE_MARGIN, leftMargin)
|
||||
.append(" ".repeat(prefix.length() + startColumn - 1))
|
||||
.append(AnsiTheme.STACK_TRACE_CARET, "^".repeat(endColumn - startColumn + 1))
|
||||
.append('\n');
|
||||
}
|
||||
|
||||
private void renderSourceLocation(StackFrame frame, TextFormatter out, String leftMargin) {
|
||||
out.margin(leftMargin)
|
||||
.style(Element.TEXT)
|
||||
.append("at ")
|
||||
.append(frame.getMemberName() != null ? frame.getMemberName() : "<unknown>")
|
||||
.append(" (")
|
||||
.append(frame.getModuleUri())
|
||||
.append(")")
|
||||
.newline();
|
||||
private void renderSourceLocation(
|
||||
StackFrame frame, AnsiCodingStringBuilder out, String leftMargin) {
|
||||
out.append(AnsiTheme.STACK_TRACE_MARGIN, leftMargin)
|
||||
.append(
|
||||
AnsiTheme.STACK_FRAME,
|
||||
() ->
|
||||
out.append("at ")
|
||||
.append(frame.getMemberName() != null ? frame.getMemberName() : "<unknown>")
|
||||
.append(" (")
|
||||
.appendUntrusted(frame.getModuleUri())
|
||||
.append(")")
|
||||
.append('\n'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.pkl.core.ast.member.ObjectMember;
|
||||
import org.pkl.core.module.ModuleKeys;
|
||||
import org.pkl.core.stdlib.PklConverter;
|
||||
import org.pkl.core.stdlib.base.PcfRenderer;
|
||||
import org.pkl.core.util.AnsiTheme;
|
||||
import org.pkl.core.util.EconomicMaps;
|
||||
import org.pkl.core.util.MutableBoolean;
|
||||
import org.pkl.core.util.MutableReference;
|
||||
@@ -41,15 +42,20 @@ import org.pkl.core.util.MutableReference;
|
||||
/** Runs test results examples and facts. */
|
||||
public final class TestRunner {
|
||||
private static final PklConverter converter = new PklConverter(VmMapping.empty());
|
||||
private final boolean overwrite;
|
||||
private final StackFrameTransformer stackFrameTransformer;
|
||||
private final BufferedLogger logger;
|
||||
private final StackFrameTransformer stackFrameTransformer;
|
||||
private final boolean overwrite;
|
||||
private final boolean useColor;
|
||||
|
||||
public TestRunner(
|
||||
BufferedLogger logger, StackFrameTransformer stackFrameTransformer, boolean overwrite) {
|
||||
BufferedLogger logger,
|
||||
StackFrameTransformer stackFrameTransformer,
|
||||
boolean overwrite,
|
||||
boolean useColor) {
|
||||
this.logger = logger;
|
||||
this.stackFrameTransformer = stackFrameTransformer;
|
||||
this.overwrite = overwrite;
|
||||
this.useColor = useColor;
|
||||
}
|
||||
|
||||
public TestResults run(VmTyped testModule) {
|
||||
@@ -60,7 +66,7 @@ public final class TestRunner {
|
||||
checkAmendsPklTest(testModule);
|
||||
} catch (VmException v) {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -109,7 +115,7 @@ public final class TestRunner {
|
||||
} catch (VmException err) {
|
||||
var error =
|
||||
new TestResults.Error(
|
||||
err.getMessage(), err.toPklException(stackFrameTransformer, false));
|
||||
err.getMessage(), err.toPklException(stackFrameTransformer, useColor));
|
||||
resultBuilder.addError(error);
|
||||
}
|
||||
return true;
|
||||
@@ -209,7 +215,7 @@ public final class TestRunner {
|
||||
errored.set(true);
|
||||
testResultBuilder.addError(
|
||||
new TestResults.Error(
|
||||
err.getMessage(), err.toPklException(stackFrameTransformer, false)));
|
||||
err.getMessage(), err.toPklException(stackFrameTransformer, useColor)));
|
||||
return true;
|
||||
}
|
||||
var expectedValue = VmUtils.readMember(expectedGroup, exampleIndex);
|
||||
@@ -306,7 +312,7 @@ public final class TestRunner {
|
||||
} catch (VmException err) {
|
||||
testResultBuilder.addError(
|
||||
new TestResults.Error(
|
||||
err.getMessage(), err.toPklException(stackFrameTransformer, false)));
|
||||
err.getMessage(), err.toPklException(stackFrameTransformer, useColor)));
|
||||
allSucceeded.set(false);
|
||||
success.set(false);
|
||||
return true;
|
||||
@@ -388,27 +394,32 @@ public final class TestRunner {
|
||||
moduleInfo.getModuleKey().getUri(), VmContext.get(null).getFrameTransformer());
|
||||
}
|
||||
|
||||
private static Failure factFailure(SourceSection sourceSection, String location) {
|
||||
String message = sourceSection.getCharacters().toString() + " " + renderLocation(location);
|
||||
return new Failure("Fact Failure", message);
|
||||
private Failure factFailure(SourceSection sourceSection, String location) {
|
||||
var sb = new AnsiCodingStringBuilder(useColor);
|
||||
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 msg =
|
||||
renderLocation(location)
|
||||
+ "\n"
|
||||
+ "Output mismatch: Expected \""
|
||||
+ property
|
||||
+ "\" to contain "
|
||||
+ expectedLength
|
||||
+ " examples, but found "
|
||||
+ actualLength;
|
||||
var sb = new AnsiCodingStringBuilder(useColor);
|
||||
appendLocation(sb, location);
|
||||
|
||||
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 existsIn;
|
||||
@@ -422,52 +433,58 @@ public final class TestRunner {
|
||||
missingIn = "actual";
|
||||
}
|
||||
|
||||
String message =
|
||||
renderLocation(location)
|
||||
+ "\n"
|
||||
+ "Output mismatch: \""
|
||||
+ property
|
||||
+ "\" exists in "
|
||||
+ existsIn
|
||||
+ " but not in "
|
||||
+ missingIn
|
||||
+ " output";
|
||||
var sb = new AnsiCodingStringBuilder(useColor);
|
||||
appendLocation(sb, location);
|
||||
|
||||
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 expectedLocation,
|
||||
String expectedValue,
|
||||
String actualLocation,
|
||||
String actualValue,
|
||||
int exampleNumber) {
|
||||
String err =
|
||||
"#"
|
||||
+ exampleNumber
|
||||
+ " "
|
||||
+ renderLocation(location)
|
||||
+ ":\n "
|
||||
+ "Expected: "
|
||||
+ renderLocation(expectedLocation)
|
||||
+ "\n "
|
||||
+ expectedValue.replaceAll("\n", "\n ")
|
||||
+ "\n "
|
||||
+ "Actual: "
|
||||
+ renderLocation(actualLocation)
|
||||
+ "\n "
|
||||
+ actualValue.replaceAll("\n", "\n ");
|
||||
|
||||
return new Failure("Example Failure", err);
|
||||
var sb = new AnsiCodingStringBuilder(useColor);
|
||||
sb.append(AnsiTheme.TEST_NAME, "#" + exampleNumber + ": ");
|
||||
sb.append(
|
||||
AnsiTheme.TEST_FAILURE_MESSAGE,
|
||||
() -> {
|
||||
appendLocation(sb, location);
|
||||
sb.append("\n Expected: ");
|
||||
appendLocation(sb, expectedLocation);
|
||||
sb.append("\n ");
|
||||
sb.append(AnsiTheme.TEST_EXAMPLE_OUTPUT, expectedValue.replaceAll("\n", "\n "));
|
||||
sb.append("\n Actual: ");
|
||||
appendLocation(sb, actualLocation);
|
||||
sb.append("\n ");
|
||||
sb.append(AnsiTheme.TEST_EXAMPLE_OUTPUT, actualValue.replaceAll("\n", "\n "));
|
||||
});
|
||||
return new Failure("Example Failure", sb.toString());
|
||||
}
|
||||
|
||||
private static String renderLocation(String location) {
|
||||
return "(" + location + ")";
|
||||
private void appendLocation(AnsiCodingStringBuilder stringBuilder, String location) {
|
||||
stringBuilder.append(
|
||||
AnsiTheme.STACK_FRAME,
|
||||
() -> stringBuilder.append("(").appendUntrusted(location).append(")"));
|
||||
}
|
||||
|
||||
private static Failure writtenExampleOutputFailure(String testName, String location) {
|
||||
var message = renderLocation(location) + "\n" + "Wrote expected output for test " + testName;
|
||||
return new Failure("Example Output Written", message);
|
||||
private Failure writtenExampleOutputFailure(String testName, String location) {
|
||||
var sb = new AnsiCodingStringBuilder(useColor);
|
||||
appendLocation(sb, location);
|
||||
sb.append(AnsiTheme.TEST_FAILURE_MESSAGE, "\nWrote expected output for test ").append(testName);
|
||||
return new Failure("Example Output Written", sb.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.stream.Collectors;
|
||||
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.Nullable;
|
||||
|
||||
@@ -39,12 +39,12 @@ public final class VmExceptionRenderer {
|
||||
|
||||
@TruffleBoundary
|
||||
public String render(VmException exception) {
|
||||
var formatter = TextFormatter.create(color);
|
||||
var formatter = new AnsiCodingStringBuilder(color);
|
||||
render(exception, formatter);
|
||||
return formatter.toString();
|
||||
}
|
||||
|
||||
private void render(VmException exception, TextFormatter out) {
|
||||
private void render(VmException exception, AnsiCodingStringBuilder out) {
|
||||
if (exception instanceof VmBugException bugException) {
|
||||
renderBugException(bugException, out);
|
||||
} 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
|
||||
var exceptionToReport = exception.getCause() != null ? exception.getCause() : exception;
|
||||
var exceptionUrl = URLEncoder.encode(exceptionToReport.toString(), StandardCharsets.UTF_8);
|
||||
|
||||
out.style(Element.TEXT)
|
||||
.append("An unexpected error has occurred. Would you mind filing a bug report?")
|
||||
.newline()
|
||||
out.append("An unexpected error has occurred. Would you mind filing a bug report?")
|
||||
.append('\n')
|
||||
.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.")
|
||||
.newlines(2)
|
||||
.append("\n".repeat(2))
|
||||
.append("https://github.com/apple/pkl/issues/new")
|
||||
.newlines(2)
|
||||
.append("\n".repeat(2))
|
||||
.append(exceptionUrl.replaceAll("\\+", "%20"))
|
||||
.newlines(2);
|
||||
.append("\n\n");
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
private void renderException(VmException exception, TextFormatter out, boolean withHeader) {
|
||||
private void renderException(
|
||||
VmException exception, AnsiCodingStringBuilder out, boolean withHeader) {
|
||||
String message;
|
||||
var hint = exception.getHint();
|
||||
if (exception.isExternalMessage()) {
|
||||
@@ -97,9 +97,9 @@ public final class VmExceptionRenderer {
|
||||
}
|
||||
|
||||
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
|
||||
if (exception.getCause() != null) {
|
||||
@@ -107,11 +107,7 @@ public final class VmExceptionRenderer {
|
||||
var causeMessage = cause.getMessage();
|
||||
// null for Truffle's LazyStackTrace
|
||||
if (causeMessage != null && !causeMessage.equals(message)) {
|
||||
out.style(Element.TEXT)
|
||||
.append(cause.getClass().getSimpleName())
|
||||
.append(": ")
|
||||
.append(causeMessage)
|
||||
.newline();
|
||||
out.append(cause.getClass().getSimpleName()).append(": ").append(causeMessage).append('\n');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,12 +115,11 @@ public final class VmExceptionRenderer {
|
||||
exception.getProgramValues().stream().mapToInt(v -> v.name.length()).max().orElse(0);
|
||||
|
||||
for (var value : exception.getProgramValues()) {
|
||||
out.style(Element.TEXT)
|
||||
.append(value.name)
|
||||
.repeat(Math.max(0, maxNameLength - value.name.length()), ' ')
|
||||
out.append(value.name)
|
||||
.append(" ".repeat(Math.max(0, maxNameLength - value.name.length())))
|
||||
.append(": ")
|
||||
.append(value)
|
||||
.newline();
|
||||
.append('\n');
|
||||
}
|
||||
|
||||
if (stackTraceRenderer != null) {
|
||||
@@ -137,10 +132,10 @@ public final class VmExceptionRenderer {
|
||||
}
|
||||
|
||||
if (!frames.isEmpty()) {
|
||||
stackTraceRenderer.render(frames, hint, out.newline());
|
||||
stackTraceRenderer.render(frames, hint, out.append('\n'));
|
||||
} else if (hint != null) {
|
||||
// render hint if there are no stack frames
|
||||
out.newline().style(Element.HINT).append(hint);
|
||||
out.append('\n').append(AnsiTheme.ERROR_MESSAGE_HINT, hint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,9 @@ public final class JUnitReport implements TestReport {
|
||||
long element = i++;
|
||||
list.add(
|
||||
buildXmlElement(
|
||||
"failure", attrs, members -> members.put(element, syntheticElement(fail.message()))));
|
||||
"failure",
|
||||
attrs,
|
||||
members -> members.put(element, syntheticElement(stripColors(fail.message())))));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
@@ -120,7 +122,9 @@ public final class JUnitReport implements TestReport {
|
||||
buildXmlElement(
|
||||
"error",
|
||||
attrs,
|
||||
members -> members.put(element, syntheticElement(error.exception().getMessage()))));
|
||||
members ->
|
||||
members.put(
|
||||
element, syntheticElement(stripColors(error.exception().getMessage())))));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
@@ -132,7 +136,9 @@ public final class JUnitReport implements TestReport {
|
||||
buildXmlElement(
|
||||
"error",
|
||||
attrs,
|
||||
members -> members.put(1, syntheticElement("\n" + error.exception().getMessage()))));
|
||||
members ->
|
||||
members.put(
|
||||
1, syntheticElement(stripColors("\n" + error.exception().getMessage())))));
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -191,7 +197,11 @@ public final class JUnitReport implements TestReport {
|
||||
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 converter = new PklConverter(VmMapping.empty());
|
||||
var renderer = new Renderer(builder, indent, version, "", VmMapping.empty(), converter);
|
||||
|
||||
@@ -17,19 +17,32 @@ package org.pkl.core.stdlib.test.report;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import org.pkl.core.TestResults;
|
||||
import org.pkl.core.TestResults.TestResult;
|
||||
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;
|
||||
|
||||
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
|
||||
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) {
|
||||
var rendered = results.error().exception().getMessage();
|
||||
@@ -40,45 +53,61 @@ public final class SimpleReport implements TestReport {
|
||||
reportResults(results.examples(), builder);
|
||||
}
|
||||
|
||||
if (results.isExampleWrittenFailure()) {
|
||||
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);
|
||||
writer.append(builder.toString());
|
||||
}
|
||||
|
||||
private void reportResults(TestSectionResults section, StringBuilder builder) {
|
||||
if (!section.results().isEmpty()) {
|
||||
builder.append(" ").append(section.name()).append("\n");
|
||||
public void summarize(List<TestResults> allTestResults, Writer writer) throws IOException {
|
||||
var totalTests = 0;
|
||||
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(
|
||||
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(" ");
|
||||
|
||||
if (result.isExampleWritten()) {
|
||||
builder.append("✍️ ").append(result.name());
|
||||
} 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()) {
|
||||
var failurePadding = " ";
|
||||
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(
|
||||
builder,
|
||||
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 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) {
|
||||
line += String.format(" [%d/%d failed]", failed, total);
|
||||
sb.append(" [").append(failed).append('/').append(total).append(" failed]");
|
||||
} else {
|
||||
line += String.format(" [%d passed]", passed);
|
||||
sb.append(" [").append(passed).append(" passed]");
|
||||
}
|
||||
|
||||
return line;
|
||||
}
|
||||
}
|
||||
|
||||
41
pkl-core/src/main/java/org/pkl/core/util/AnsiTheme.java
Normal file
41
pkl-core/src/main/java/org/pkl/core/util/AnsiTheme.java
Normal 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);
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
package org.pkl.core.util;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import org.pkl.core.runtime.AnsiCodingStringBuilder;
|
||||
|
||||
// Some code in this class was taken from the following Google Guava classes:
|
||||
// * com.google.common.base.CharMatcher
|
||||
@@ -76,7 +77,7 @@ public final class StringUtils {
|
||||
|
||||
public static <T> void joinToStringBuilder(
|
||||
StringBuilder builder, Iterable<T> coll, String delimiter, Consumer<T> eachFn) {
|
||||
int i = 0;
|
||||
var i = 0;
|
||||
for (var v : coll) {
|
||||
if (i++ != 0) {
|
||||
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(
|
||||
StringBuilder builder, Iterable<T> coll, String delimiter) {
|
||||
joinToStringBuilder(builder, coll, delimiter, builder::append);
|
||||
|
||||
@@ -125,7 +125,7 @@ class EvaluateTestsTest {
|
||||
|
||||
val error = res.errors[0]
|
||||
assertThat(error.message).isEqualTo("got an error")
|
||||
assertThat(error.exception.message)
|
||||
assertThat(error.exception().message)
|
||||
.isEqualTo(
|
||||
"""
|
||||
–– Pkl Error ––
|
||||
@@ -347,7 +347,7 @@ class EvaluateTestsTest {
|
||||
assertThat(fail1.message.stripFileAndLines(tempDir))
|
||||
.isEqualTo(
|
||||
"""
|
||||
#0 (/tempDir/example.pkl):
|
||||
#0: (/tempDir/example.pkl)
|
||||
Expected: (/tempDir/example.pkl-expected.pcf)
|
||||
new {
|
||||
name = "Alice"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -190,7 +190,7 @@ class StackTraceRendererTest {
|
||||
}
|
||||
val loop = StackTraceRenderer.StackFrameLoop(loopFrames, 1)
|
||||
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)
|
||||
val renderedFrames = formatter.toString()
|
||||
assertThat(renderedFrames)
|
||||
|
||||
Reference in New Issue
Block a user