-
Notifications
You must be signed in to change notification settings - Fork 280
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use ANSI colors for test results; more polish #771
Conversation
ac7c0a5
to
f1270f1
Compare
Maybe the bodies of Are the colors user-configurable? (I'm on mobile so I haven't been able to check the code changes yet). |
No, but we can add that in the future; that requires some design work that we don't quite have time for (we're trying to get this in before 0.27 ships on Monday). |
} | ||
|
||
public TextFormattingStringBuilder append(String value) { | ||
appendCodes(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optimization: only append the delta that's needed.
new TextFormattingStringBuilder(true)
.append(AnsiColor.RED, "Hi")
.append(AnsiColor.RED, "hi")
.append(EnumSet.of(AnsiColor.RED, AnsiColor.BOLD), "hi"))
.toString()
Results in:
\033[31m;Hihi\033[1mhi[\0m
Also: * Refactor `TextFormatter` to be more generic (less indirection). * Adjust test report slightly (no emojis, add more spacing).
697c11e
to
b349bc4
Compare
out.append("at ") | ||
.append(frame.getMemberName() != null ? frame.getMemberName() : "<unknown>") | ||
.append(" (") | ||
.appendUntrusted(frame.getModuleUri()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These values might get transformed by ~/.pkl/settings.pkl
, might inject ANSI escape codes.
Protect against that by adding reset, and re-applying all ansi codes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At this point, the "formatter" does so little beyond StringBuilder
; isn't it better to just use a normal StringBuilder
, define some static final
constants for the ANSI codes and put a if (!color) { output.replaceAll("\u001B\\[[;\\d]*m", ""); }
at the point of StringBuilder::toString
?
Approving to unblock for the release.
sb.append( | ||
ColorTheme.TEST_FAILURE_MESSAGE, | ||
() -> { | ||
appendLocation(sb, location); | ||
sb.append("\n Expected: "); | ||
appendLocation(sb, expectedLocation); | ||
sb.append("\n "); | ||
sb.append(ColorTheme.TEST_EXAMPLE_OUTPUT, expectedValue.replaceAll("\n", "\n ")); | ||
sb.append("\n Actual: "); | ||
appendLocation(sb, actualLocation); | ||
sb.append("\n "); | ||
sb.append(ColorTheme.TEST_EXAMPLE_OUTPUT, actualValue.replaceAll("\n", "\n ")); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mixing different abstractions patterns; StringBuilder, state parentheses, functional string building... is the closure-encapsulation purely to protect against the optimization built into the string builder? If so, that seems a leaky abstraction.
sb.append( | |
ColorTheme.TEST_FAILURE_MESSAGE, | |
() -> { | |
appendLocation(sb, location); | |
sb.append("\n Expected: "); | |
appendLocation(sb, expectedLocation); | |
sb.append("\n "); | |
sb.append(ColorTheme.TEST_EXAMPLE_OUTPUT, expectedValue.replaceAll("\n", "\n ")); | |
sb.append("\n Actual: "); | |
appendLocation(sb, actualLocation); | |
sb.append("\n "); | |
sb.append(ColorTheme.TEST_EXAMPLE_OUTPUT, actualValue.replaceAll("\n", "\n ")); | |
}); | |
sb | |
.append(ColorTheme.TEST_FAILURE_MESSAGE, "(") | |
.appendUntrusted(ColorTheme.TEST_FAILURE_MESSAGE, location) | |
.append(ColorTheme.TEST_FAILURE_MESSAGE, ")\n Expected: (") | |
.appendUntrusted(ColorTheme.TEST_FAILURE_MESSAGE, expectedLocation) | |
.append(ColorTheme.TEST_FAILURE_MESSAGE, ")\n ") | |
.append(ColorTheme.TEST_EXAMPLE_OUTPUT, expectedValue.replaceAll("\n", "\n ")) | |
.append(ColorTheme.TEST_FAILURE_MESSAGE, "(") | |
.appendUntrusted(ColorTheme.TEST_FAILURE_MESSAGE, actualLocation) | |
.append(ColorTheme.TEST_FAILURE_MESSAGE, ")\n ") | |
.append(ColorTheme.TEST_EXAMPLE_OUTPUT, actualValue.replaceAll("\n", "\n ")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is the closure-encapsulation purely to protect against the optimization built into the string builder
No; the purpose of the runnable is so you can have a a block where:
- You don't need to keep repeating the same style for each append.
- The parent style applies to every element appended within, including styles applied on the children. For example:
This results in "hello" being both red and bold.
sb.append(AnsiCode.RED, () -> { sb.append(AnsiCold.BOLD, "Hello"); });
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On not repeating; that also holds true for setting a style with a separate call, as in the previous approach.
Considering the other discussions where this AnsiCodingStringBuilder
ties the user into using formulations of the ANSI model, why not just fully commit to that and avoid ad hoc primitives like this closure-nesting and appendUntrusted
, that live neither in ANSI, nor in StringBuilder
? That way, the user only needs to know those two models. What I'm suggesting is:
- Provide a
append(EnumSet<AnsiCode> codes)
and/orappend(AnsiCode code)
that inserts escape codes. - Optionally provide a
EnumSet<AnsiCode> getCurrentAnsiCodes()
that helps locally revert to an old state. - Implement
useColor
by either making saidappend
overloads conditional, or doing a regex replacement intoString
to fish all escapes out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This API, in its current form, lets us abstract and introduce another formatter later on (for example, HTML colors).
Your suggestion ties us much more strongly to ANSI (reset
is a not a thing in HTML). It's also harder to use (users have to remember how reset works and how to apply previous colors).
var prevColors = sb.getCurrentAnsiCodes();
sb.append(AnsiCode.RED);
sb.append("hello, ");
for (var name : names) {
sb.append(name).append(" ");
}
sb.reset();
sb.append(prevColors);
Compared to:
sb.append(AnsiCode.RED, () -> {
sb.append("hello, ");
for (var name : names) {
sb.append(name).append(" ");
}
});
To build on my abstraction point: if we wanted to later have an HtmlCodingStringBuilder
, this would look something like:
class HtmlCodingStringBuilder implements ColorCodingStringBuilder [
public ColorCodingStringBuilder append(Color color, Runnable runnable) {
builder.append("<span style=\"color: ").append(getHex(color)).append("\">");
runnable.run();
builder.append("</span>");
}
}
pkl-core/src/main/java/org/pkl/core/runtime/TextFormattingStringBuilder.java
Outdated
Show resolved
Hide resolved
pkl-core/src/main/java/org/pkl/core/runtime/TextFormattingStringBuilder.java
Outdated
Show resolved
Hide resolved
public TextFormattingStringBuilder appendLine(int count) { | ||
builder.append("\n".repeat(count)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
^^ inline
pkl-core/src/main/java/org/pkl/core/stdlib/test/report/SimpleReport.java
Outdated
Show resolved
Hide resolved
public enum Element { | ||
PLAIN, | ||
MARGIN, | ||
HINT, | ||
STACK_OVERFLOW_LOOP_COUNT, | ||
LINE_NUMBER, | ||
TEXT, | ||
ERROR_HEADER, | ||
ERROR, | ||
RESET, | ||
FAILING_TEST_MARK, | ||
PASSING_TEST_MARK, | ||
TEST_NAME, | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove
@@ -191,7 +197,11 @@ private VmTyped makeCdata(String text) { | |||
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("\u001B\\[[;\\d]*m", ""); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use either \033
or \u001b
return str.replaceAll("\u001B\\[[;\\d]*m", ""); | |
return str.replaceAll("\u033\\[[;\\d]*m", ""); |
pkl-core/src/main/java/org/pkl/core/stdlib/test/report/SimpleReport.java
Outdated
Show resolved
Hide resolved
private static final String passingMark = "✔ "; | ||
private static final String failingMark = "✘ "; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the naming style deviation? If for readability (which I tend to agree with, because I'm alright with my tooling telling me something is static final
), might we also convert the members of ColorTheme
- which are not final
- to camelCase?
sb.append( | ||
ColorTheme.TEST_FAILURE_MESSAGE, | ||
() -> { | ||
appendLocation(sb, location); | ||
sb.append("\n Expected: "); | ||
appendLocation(sb, expectedLocation); | ||
sb.append("\n "); | ||
sb.append(ColorTheme.TEST_EXAMPLE_OUTPUT, expectedValue.replaceAll("\n", "\n ")); | ||
sb.append("\n Actual: "); | ||
appendLocation(sb, actualLocation); | ||
sb.append("\n "); | ||
sb.append(ColorTheme.TEST_EXAMPLE_OUTPUT, actualValue.replaceAll("\n", "\n ")); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On not repeating; that also holds true for setting a style with a separate call, as in the previous approach.
Considering the other discussions where this AnsiCodingStringBuilder
ties the user into using formulations of the ANSI model, why not just fully commit to that and avoid ad hoc primitives like this closure-nesting and appendUntrusted
, that live neither in ANSI, nor in StringBuilder
? That way, the user only needs to know those two models. What I'm suggesting is:
- Provide a
append(EnumSet<AnsiCode> codes)
and/orappend(AnsiCode code)
that inserts escape codes. - Optionally provide a
EnumSet<AnsiCode> getCurrentAnsiCodes()
that helps locally revert to an old state. - Implement
useColor
by either making saidappend
overloads conditional, or doing a regex replacement intoString
to fish all escapes out.
public static AnsiCode ERROR_MESSAGE_HINT = AnsiCode.YELLOW; | ||
public static AnsiCode ERROR_HEADER = AnsiCode.RED; | ||
public static Set<AnsiCode> ERROR_MESSAGE = EnumSet.of(AnsiCode.RED, AnsiCode.BOLD); | ||
|
||
public static AnsiCode STACK_FRAME = AnsiCode.FAINT; | ||
public static AnsiCode STACK_TRACE_MARGIN = AnsiCode.YELLOW; | ||
public static AnsiCode STACK_TRACE_LINE_NUMBER = AnsiCode.BLUE; | ||
public static AnsiCode STACK_TRACE_LOOP_COUNT = AnsiCode.MAGENTA; | ||
public static AnsiCode STACK_TRACE_CARET = AnsiCode.RED; | ||
|
||
public static AnsiCode FAILING_TEST_MARK = AnsiCode.RED; | ||
public static AnsiCode PASSING_TEST_MARK = AnsiCode.GREEN; | ||
public static AnsiCode TEST_NAME = AnsiCode.FAINT; | ||
public static AnsiCode TEST_FACT_SOURCE = AnsiCode.RED; | ||
public static AnsiCode TEST_FAILURE_MESSAGE = AnsiCode.RED; | ||
public static Set<AnsiCode> TEST_EXAMPLE_OUTPUT = EnumSet.of(AnsiCode.RED, AnsiCode.BOLD); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Considering none of these are final
, I presume these are intended to become configurable, in which case, shouldn't they all be Set<AnsiCode>
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't meant to be an API. I'll go ahead and make this final
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good. Left some comments. Nothing big.
@@ -39,44 +39,44 @@ public VmExceptionRenderer(@Nullable StackTraceRenderer stackTraceRenderer, bool | |||
|
|||
@TruffleBoundary | |||
public String render(VmException exception) { | |||
var formatter = TextFormatter.create(color); | |||
var formatter = new AnsiCodingStringBuilder(color); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var formatter = new AnsiCodingStringBuilder(color); | |
var formatter = new AnsiCodingStringBuilder(hasColor); |
I'd rename the attribute here. color
gives the idea this is a color, not a boolean flag.
pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionRenderer.java
Outdated
Show resolved
Hide resolved
pkl-core/src/main/java/org/pkl/core/runtime/AnsiCodingStringBuilder.java
Show resolved
Hide resolved
Co-authored-by: Islon Scherer <[email protected]>
To make ANSI codes work reliably across platforms, you should:
Have you considered to use Jansi's formatter in pkl-core instead of rolling your own? |
We did consider that. IMO: I don't think Jansi adds too much value for formatting escape codes; writing those are pretty trivial and we don't need a library for that. However, the one valuable thing that Jansi provides is a better way to determine whether a stream is connected to a TTY or not; Java's AFAICT, Jansi doesn't have an API for "is stdout/stderr/stdin a tty?". If they did, I think we'd be happy to use it. A follow-up step here is to get rid of Jansi as a dependency for formatting in pkl-cli and just use the formatter we've defined in pkl-core. |
TL/DR: Replacing jansi's formatter is possible, but you'll need jansi for cross-platform console support in any case. To solve the issues I listed, you'll either need to use Jansi or implement your own replacement. As long as pkl-cli uses jansi, you should definitely call PS: jline, which is used in pkl-cli, also depends on jansi. |
Any thrown Pkl Errors are colored in the simple test report!
Also:
TextFormatter
to be more generic; rename toTextFormattingStringBuilder
ColorTheme
class.Also: this changes the summary so it summarizes all modules, rather than a summary per module.
Note: I borrowed some of this styling from mocha; I've always liked how they style their test results.
Screenshots:
FYI @jjmaestro