diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/CliTestRunner.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/CliTestRunner.kt index 77f6dd7ea..15dfff84a 100644 --- a/pkl-cli/src/main/kotlin/org/pkl/cli/CliTestRunner.kt +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/CliTestRunner.kt @@ -20,6 +20,7 @@ import org.pkl.commons.cli.* import org.pkl.core.Closeables import org.pkl.core.EvaluatorBuilder 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.SimpleReport import org.pkl.core.util.ErrorMessages @@ -62,14 +63,17 @@ constructor( var failed = false var isExampleWrittenFailure = true val moduleNames = mutableSetOf() + val reporter = SimpleReport(useColor) + val allTestResults = mutableListOf() for ((idx, moduleUri) in sources.withIndex()) { try { val results = evaluator.evaluateTest(uri(moduleUri), testOptions.overwrite) + allTestResults.add(results) if (!failed) { failed = results.failed() isExampleWrittenFailure = results.isExampleWrittenFailure.and(isExampleWrittenFailure) } - SimpleReport().report(results, consoleWriter) + reporter.report(results, consoleWriter) if (sources.size > 1 && idx != sources.size - 1) { consoleWriter.append('\n') } @@ -102,6 +106,9 @@ constructor( failed = true } } + consoleWriter.append('\n') + reporter.summarize(allTestResults, consoleWriter) + consoleWriter.flush() if (failed) { val exitCode = if (isExampleWrittenFailure) 10 else 1 throw CliTestException(ErrorMessages.create("testsFailed"), exitCode) diff --git a/pkl-cli/src/test/kotlin/org/pkl/cli/CliTestRunnerTest.kt b/pkl-cli/src/test/kotlin/org/pkl/cli/CliTestRunnerTest.kt index 975406020..7bbdd42af 100644 --- a/pkl-cli/src/test/kotlin/org/pkl/cli/CliTestRunnerTest.kt +++ b/pkl-cli/src/test/kotlin/org/pkl/cli/CliTestRunnerTest.kt @@ -37,7 +37,6 @@ import org.pkl.commons.writeString import org.pkl.core.Release class CliTestRunnerTest { - @Test fun `CliTestRunner succeed test`(@TempDir tempDir: Path) { val code = @@ -65,8 +64,9 @@ class CliTestRunnerTest { """ module test facts - ✅ succeed - ✅ 100.0% tests pass [1 passed], 100.0% asserts pass [2 passed] + ✔ succeed + + 100.0% tests pass [1 passed], 100.0% asserts pass [2 passed] """ .trimIndent() @@ -101,9 +101,10 @@ class CliTestRunnerTest { """ module test facts - ❌ fail + ✘ fail 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() @@ -137,14 +138,15 @@ class CliTestRunnerTest { """ module test facts - ❌ fail + ✘ fail –– Pkl Error –– uh oh 5 | throw("uh oh") ^^^^^^^^^^^^^^ 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() @@ -178,14 +180,15 @@ class CliTestRunnerTest { """ module test examples - ❌ fail + ✘ fail –– Pkl Error –– uh oh 5 | throw("uh oh") ^^^^^^^^^^^^^^ 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() @@ -233,14 +236,15 @@ class CliTestRunnerTest { """ module test examples - ❌ fail + ✘ fail –– Pkl Error –– uh oh 5 | throw("uh oh") ^^^^^^^^^^^^^^ 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() @@ -435,10 +439,11 @@ class CliTestRunnerTest { """ module test examples - ❌ nums + ✘ nums (/tempDir/test.pkl, line xx) 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() @@ -474,6 +479,7 @@ class CliTestRunnerTest { module test examples ✍️ nums + 1 examples written """ diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliCommand.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliCommand.kt index 7539a11cf..bde2a0fa2 100644 --- a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliCommand.kt +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliCommand.kt @@ -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 { cliOptions.httpProxy ?: project?.evaluatorSettings?.http?.proxy?.address ?: settings.http?.proxy?.address @@ -284,7 +286,7 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) { .setEnvironmentVariables(environmentVariables) .addModuleKeyFactories(moduleKeyFactories(modulePathResolver)) .addResourceReaders(resourceReaders(modulePathResolver)) - .setColor(cliOptions.color?.hasColor() ?: false) + .setColor(useColor) .setLogger(Loggers.stdErr()) .setTimeout(cliOptions.timeout) .setModuleCacheDir(moduleCacheDir) diff --git a/pkl-core/src/main/java/org/pkl/core/EvaluatorImpl.java b/pkl-core/src/main/java/org/pkl/core/EvaluatorImpl.java index 52f2ae26d..336b321df 100644 --- a/pkl-core/src/main/java/org/pkl/core/EvaluatorImpl.java +++ b/pkl-core/src/main/java/org/pkl/core/EvaluatorImpl.java @@ -235,7 +235,7 @@ public TestResults evaluateTest(ModuleSource moduleSource, boolean overwrite) { return doEvaluate( moduleSource, (module) -> { - var testRunner = new TestRunner(logger, frameTransformer, overwrite); + var testRunner = new TestRunner(logger, frameTransformer, overwrite, color); return testRunner.run(module); }); } diff --git a/pkl-core/src/main/java/org/pkl/core/TestResults.java b/pkl-core/src/main/java/org/pkl/core/TestResults.java index 7b1cc097d..9827fdd82 100644 --- a/pkl-core/src/main/java/org/pkl/core/TestResults.java +++ b/pkl-core/src/main/java/org/pkl/core/TestResults.java @@ -92,7 +92,7 @@ public boolean failed() { * 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 TestResult build() { } } + /** + * 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) {} } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/AnsiCodingStringBuilder.java b/pkl-core/src/main/java/org/pkl/core/runtime/AnsiCodingStringBuilder.java new file mode 100644 index 000000000..847cff32c --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/AnsiCodingStringBuilder.java @@ -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 currentCodes = Collections.emptySet(); + + /** The set of ansi codes intended to be applied the next time text is written. */ + private Set 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 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}. + * + *

This is a helper method. With this: + * + *

    + *
  • There is no need to repeat the same style for multiple appends in a row. + *
  • The parent style is added to any styles added applied in the children. + *

    For example, in the following snippet, {@code "hello"} is formatted in both bold and + * red: + *

    {@code
    +   * var sb = new AnsiCodingStringBuilder(true);
    +   * sb.append(AnsiCode.RED, () -> {
    +   *   sb.append(AnsiCode.BOLD, "hello");
    +   * });
    +   *
    +   * }
    + *
+ */ + 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. + * + *

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. + * + *

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. + * + *

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. + * + *

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. + * + *

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 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; + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/StackTraceRenderer.java b/pkl-core/src/main/java/org/pkl/core/runtime/StackTraceRenderer.java index 56aa84862..fae805b17 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/StackTraceRenderer.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/StackTraceRenderer.java @@ -19,7 +19,7 @@ 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 StackTraceRenderer(Function frameTransformer) { this.frameTransformer = frameTransformer; } - public void render(List frames, @Nullable String hint, TextFormatter out) { + public void render(List frames, @Nullable String hint, AnsiCodingStringBuilder out) { var compressed = compressFrames(frames); doRender(compressed, hint, out, "", true); } @@ -38,7 +38,7 @@ public void render(List frames, @Nullable String hint, TextFormatter void doRender( List frames, @Nullable String hint, - TextFormatter out, + AnsiCodingStringBuilder out, String leftMargin, boolean isFirstElement) { for (var frame : frames) { @@ -48,13 +48,11 @@ void doRender( 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 @@ void doRender( 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 @@ void doRender( } } - 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 @@ private void renderSourceLine(StackFrame frame, TextFormatter out, String leftMa : 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() : "") - .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() : "") + .append(" (") + .appendUntrusted(frame.getModuleUri()) + .append(")") + .append('\n')); } /** diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/TestRunner.java b/pkl-core/src/main/java/org/pkl/core/runtime/TestRunner.java index 2660c99a6..bfc7d5297 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/TestRunner.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/TestRunner.java @@ -34,6 +34,7 @@ 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 @@ /** 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 TestResults run(VmTyped testModule) { 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 @@ private TestSectionResults runFacts(VmTyped testModule) { } 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 @@ private TestSectionResults doRunAndValidateExamples( 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 @@ private TestSectionResults doRunAndWriteExamples(VmMapping examples, Path output } 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 @@ private static String getDisplayUri(ModuleInfo moduleInfo) { 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; - - return new Failure("Output Mismatch (Length)", msg); + var sb = new AnsiCodingStringBuilder(useColor); + appendLocation(sb, location); + + 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 @@ private static Failure examplePropertyMismatchFailure( missingIn = "actual"; } - String message = - renderLocation(location) - + "\n" - + "Output mismatch: \"" - + property - + "\" exists in " - + existsIn - + " but not in " - + missingIn - + " output"; - - return new Failure("Output Mismatch", message); + var sb = new AnsiCodingStringBuilder(useColor); + appendLocation(sb, location); + + 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()); } } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/TextFormatter.java b/pkl-core/src/main/java/org/pkl/core/runtime/TextFormatter.java deleted file mode 100644 index 7e414128e..000000000 --- a/pkl-core/src/main/java/org/pkl/core/runtime/TextFormatter.java +++ /dev/null @@ -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 THEME_PLAIN = new HashMap<>(); - public static final Map 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 theme; - private final StringBuilder builder = new StringBuilder(); - - private @Nullable Styling currentStyle; - - private TextFormatter(Map 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; - } - } -} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionRenderer.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionRenderer.java index e6aade01e..a61db7464 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionRenderer.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionRenderer.java @@ -20,7 +20,7 @@ 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 VmExceptionRenderer(@Nullable StackTraceRenderer stackTraceRenderer, bool @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 @@ private void render(VmException exception, TextFormatter out) { } } - 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 @@ private void renderException(VmException exception, TextFormatter out, boolean w } 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 @@ private void renderException(VmException exception, TextFormatter out, boolean w 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 @@ private void renderException(VmException exception, TextFormatter out, boolean w 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 @@ private void renderException(VmException exception, TextFormatter out, boolean w } 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); } } } diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/test/report/JUnitReport.java b/pkl-core/src/main/java/org/pkl/core/stdlib/test/report/JUnitReport.java index 862699f5d..84a82ec5a 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/test/report/JUnitReport.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/test/report/JUnitReport.java @@ -105,7 +105,9 @@ private ArrayList failures(TestResult res) { 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 @@ private ArrayList errors(TestResult res) { 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 @@ private ArrayList error(Error error) { 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 @@ 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("\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); diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/test/report/SimpleReport.java b/pkl-core/src/main/java/org/pkl/core/stdlib/test/report/SimpleReport.java index 53234a366..276622992 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/test/report/SimpleReport.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/test/report/SimpleReport.java @@ -17,19 +17,32 @@ 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 void report(TestResults results, Writer writer) throws IOException { 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.toString()); + } - writer.append(builder); + public void summarize(List 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, StringBuilder builder) { + private void reportResults(TestSectionResults section, AnsiCodingStringBuilder builder) { if (!section.results().isEmpty()) { - builder.append(" ").append(section.name()).append("\n"); - + 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 @@ private void reportResult(TestResult result, StringBuilder builder) { } } - 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 @@ private static void appendPadded(StringBuilder builder, String lines, String pad }); } - 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; } } diff --git a/pkl-core/src/main/java/org/pkl/core/util/AnsiTheme.java b/pkl-core/src/main/java/org/pkl/core/util/AnsiTheme.java new file mode 100644 index 000000000..b48b4fda0 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/util/AnsiTheme.java @@ -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 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 TEST_EXAMPLE_OUTPUT = EnumSet.of(AnsiCode.RED, AnsiCode.BOLD); +} diff --git a/pkl-core/src/main/java/org/pkl/core/util/StringUtils.java b/pkl-core/src/main/java/org/pkl/core/util/StringUtils.java index 3fa445d3b..57f70d354 100644 --- a/pkl-core/src/main/java/org/pkl/core/util/StringUtils.java +++ b/pkl-core/src/main/java/org/pkl/core/util/StringUtils.java @@ -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 static String trimEnd(String str) { public static void joinToStringBuilder( StringBuilder builder, Iterable coll, String delimiter, Consumer eachFn) { - int i = 0; + var i = 0; for (var v : coll) { if (i++ != 0) { builder.append(delimiter); @@ -85,6 +86,18 @@ public static void joinToStringBuilder( } } + public static void joinToStringBuilder( + AnsiCodingStringBuilder builder, Iterable coll, String delimiter, Consumer eachFn) { + var i = 0; + for (var v : coll) { + if (i != 0) { + builder.append(delimiter); + } + eachFn.accept(v); + i++; + } + } + public static void joinToStringBuilder( StringBuilder builder, Iterable coll, String delimiter) { joinToStringBuilder(builder, coll, delimiter, builder::append); diff --git a/pkl-core/src/test/kotlin/org/pkl/core/EvaluateTestsTest.kt b/pkl-core/src/test/kotlin/org/pkl/core/EvaluateTestsTest.kt index 0fae84b78..83e42816f 100644 --- a/pkl-core/src/test/kotlin/org/pkl/core/EvaluateTestsTest.kt +++ b/pkl-core/src/test/kotlin/org/pkl/core/EvaluateTestsTest.kt @@ -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" diff --git a/pkl-core/src/test/kotlin/org/pkl/core/runtime/AnsiCodingStringBuilderTest.kt b/pkl-core/src/test/kotlin/org/pkl/core/runtime/AnsiCodingStringBuilderTest.kt new file mode 100644 index 000000000..1197afe81 --- /dev/null +++ b/pkl-core/src/test/kotlin/org/pkl/core/runtime/AnsiCodingStringBuilderTest.kt @@ -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) + } +} diff --git a/pkl-core/src/test/kotlin/org/pkl/core/runtime/StackTraceRendererTest.kt b/pkl-core/src/test/kotlin/org/pkl/core/runtime/StackTraceRendererTest.kt index 755060899..43d3ad130 100644 --- a/pkl-core/src/test/kotlin/org/pkl/core/runtime/StackTraceRendererTest.kt +++ b/pkl-core/src/test/kotlin/org/pkl/core/runtime/StackTraceRendererTest.kt @@ -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) diff --git a/pkl-gradle/src/test/kotlin/org/pkl/gradle/TestsTest.kt b/pkl-gradle/src/test/kotlin/org/pkl/gradle/TestsTest.kt index 31a0d647f..22fcecd5d 100644 --- a/pkl-gradle/src/test/kotlin/org/pkl/gradle/TestsTest.kt +++ b/pkl-gradle/src/test/kotlin/org/pkl/gradle/TestsTest.kt @@ -30,7 +30,7 @@ class TestsTest : AbstractTest() { writePklFile() val res = runTask("evalTest") - assertThat(res.output).contains("✅ should pass") + assertThat(res.output).contains("✔ should pass") } @Test @@ -49,7 +49,7 @@ class TestsTest : AbstractTest() { ) 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(""""foo" == "bar"""") } @@ -81,15 +81,16 @@ class TestsTest : AbstractTest() { > Task :evalTest FAILED module test facts - ✅ should pass - ❌ error + ✔ should pass + ✘ error –– Pkl Error –– exception 9 | throw("exception") ^^^^^^^^^^^^^^^^^^ 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() ) @@ -118,15 +119,15 @@ class TestsTest : AbstractTest() { pkl: TRACE: 8 = 8 (file:///file, line x) module test facts - ✅ sum numbers - ✅ divide numbers - ❌ fail + ✔ sum numbers + ✔ divide numbers + ✘ fail 4 == 9 (file:///file, line x) "foo" == "bar" (file:///file, line x) examples - ✅ user 0 - ❌ user 1 - #1 (file:///file, line x): + ✔ user 0 + ✘ user 1 + #1: (file:///file, line x) Expected: (file:///file, line x) new { name = "Parrot" @@ -137,7 +138,8 @@ class TestsTest : AbstractTest() { name = "Welma" 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() ) @@ -187,8 +189,8 @@ class TestsTest : AbstractTest() { > Task :evalTest FAILED module test facts - ✅ should pass - ❌ error + ✔ should pass + ✘ error –– Pkl Error –– exception @@ -196,9 +198,9 @@ class TestsTest : AbstractTest() { ^^^^^^^^^^^^^^^^^^ at test#facts["error"][#1] (file:///file, line x) examples - ✅ user 0 - ❌ user 1 - #1 (file:///file, line x): + ✔ user 0 + ✘ user 1 + #1: (file:///file, line x) Expected: (file:///file, line x) new { name = "Parrot" @@ -209,7 +211,8 @@ class TestsTest : AbstractTest() { name = "Welma" 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() @@ -241,7 +244,7 @@ class TestsTest : AbstractTest() { - #1 (file:///file, line x): + #1: (file:///file, line x) Expected: (file:///file, line x) new { name = "Parrot" @@ -301,7 +304,7 @@ class TestsTest : AbstractTest() { - #1 (file:///file, line x): + #1: (file:///file, line x) Expected: (file:///file, line x) new { name = "Parrot"