mirror of
https://github.com/apple/pkl.git
synced 2026-04-23 16:58:37 +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:
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.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'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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;
|
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);
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 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)
|
||||||
|
|||||||
@@ -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 = "Parrot"
|
name = "Parrot"
|
||||||
@@ -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 = "Parrot"
|
name = "Parrot"
|
||||||
|
|||||||
Reference in New Issue
Block a user