mirror of
https://github.com/apple/pkl.git
synced 2026-05-25 08:09:17 +02:00
Introduce "minimal" test reporter (#1563)
This commit is contained in:
@@ -546,6 +546,15 @@ When enabled, test failures will show intermediate values in the assertion expre
|
|||||||
Use `--no-power-assertions` to disable this feature if you prefer simpler output.
|
Use `--no-power-assertions` to disable this feature if you prefer simpler output.
|
||||||
====
|
====
|
||||||
|
|
||||||
|
[[test-reporter]]
|
||||||
|
.--test-reporter
|
||||||
|
[%collapsible]
|
||||||
|
====
|
||||||
|
Default: `spec` +
|
||||||
|
Example: `--test-reporter minimal` +
|
||||||
|
Which test reporter to use for CLI output. Possible values are `spec` and `minimal`.
|
||||||
|
====
|
||||||
|
|
||||||
This command also takes <<common-options, common options>>.
|
This command also takes <<common-options, common options>>.
|
||||||
|
|
||||||
[[command-run]]
|
[[command-run]]
|
||||||
@@ -667,6 +676,14 @@ Force generation of expected examples. +
|
|||||||
The old expected files will be deleted if present.
|
The old expected files will be deleted if present.
|
||||||
====
|
====
|
||||||
|
|
||||||
|
.--test-reporter
|
||||||
|
[%collapsible]
|
||||||
|
====
|
||||||
|
Default: `spec` +
|
||||||
|
Example: `--test-reporter minimal` +
|
||||||
|
Which test reporter to use for CLI output. Possible values are `spec` and `minimal`.
|
||||||
|
====
|
||||||
|
|
||||||
This command also takes <<common-options,common options>>.
|
This command also takes <<common-options,common options>>.
|
||||||
|
|
||||||
[[command-project-resolve]]
|
[[command-project-resolve]]
|
||||||
|
|||||||
@@ -322,6 +322,15 @@ Default: `false` +
|
|||||||
Whether to ignore expected example files and generate them again.
|
Whether to ignore expected example files and generate them again.
|
||||||
====
|
====
|
||||||
|
|
||||||
|
[[test-reporter]]
|
||||||
|
.test-reporter: Property<String>
|
||||||
|
[%collapsible]
|
||||||
|
====
|
||||||
|
Default: `"spec"` +
|
||||||
|
Example: `reporter = "minimal"` +
|
||||||
|
Which test reporter to use for CLI output. Possible values are `"spec"` and `"minimal"`.
|
||||||
|
====
|
||||||
|
|
||||||
[[power-assertions-test]]
|
[[power-assertions-test]]
|
||||||
.powerAssertions: Property<Boolean>
|
.powerAssertions: Property<Boolean>
|
||||||
[%collapsible]
|
[%collapsible]
|
||||||
@@ -677,6 +686,14 @@ Default: `false` +
|
|||||||
Whether to ignore expected example files and generate them again.
|
Whether to ignore expected example files and generate them again.
|
||||||
====
|
====
|
||||||
|
|
||||||
|
.test-reporter: Property<String>
|
||||||
|
[%collapsible]
|
||||||
|
====
|
||||||
|
Default: `"spec"` +
|
||||||
|
Example: `reporter = "minimal"` +
|
||||||
|
Which test reporter to use for CLI output. Possible values are `"spec"` and `"minimal"`.
|
||||||
|
====
|
||||||
|
|
||||||
Common properties:
|
Common properties:
|
||||||
|
|
||||||
include::../partials/gradle-common-properties.adoc[]
|
include::../partials/gradle-common-properties.adoc[]
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.pkl.cli
|
package org.pkl.cli
|
||||||
|
|
||||||
|
import java.io.StringWriter
|
||||||
import java.io.Writer
|
import java.io.Writer
|
||||||
import org.pkl.commons.cli.*
|
import org.pkl.commons.cli.*
|
||||||
import org.pkl.core.Closeables
|
import org.pkl.core.Closeables
|
||||||
@@ -22,8 +23,9 @@ import org.pkl.core.EvaluatorBuilder
|
|||||||
import org.pkl.core.ModuleSource.uri
|
import org.pkl.core.ModuleSource.uri
|
||||||
import org.pkl.core.PklException
|
import org.pkl.core.PklException
|
||||||
import org.pkl.core.TestResults
|
import org.pkl.core.TestResults
|
||||||
import org.pkl.core.stdlib.test.report.JUnitReport
|
import org.pkl.core.stdlib.test.report.JUnitReporter
|
||||||
import org.pkl.core.stdlib.test.report.SimpleReport
|
import org.pkl.core.stdlib.test.report.MinimalReporter
|
||||||
|
import org.pkl.core.stdlib.test.report.SpecReporter
|
||||||
import org.pkl.core.util.ErrorMessages
|
import org.pkl.core.util.ErrorMessages
|
||||||
|
|
||||||
class CliTestRunner
|
class CliTestRunner
|
||||||
@@ -64,13 +66,15 @@ 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 reporter =
|
||||||
|
when (testOptions.reporter) {
|
||||||
|
TestReporter.SPEC -> SpecReporter(useColor)
|
||||||
|
TestReporter.MINIMAL -> MinimalReporter(useColor)
|
||||||
|
}
|
||||||
val allTestResults = mutableListOf<TestResults>()
|
val allTestResults = mutableListOf<TestResults>()
|
||||||
|
|
||||||
val junitDir = testOptions.junitDir
|
val junitDir = testOptions.junitDir
|
||||||
if (junitDir != null) {
|
junitDir?.toFile()?.mkdirs()
|
||||||
junitDir.toFile().mkdirs()
|
|
||||||
}
|
|
||||||
|
|
||||||
for ((idx, moduleUri) in sources.withIndex()) {
|
for ((idx, moduleUri) in sources.withIndex()) {
|
||||||
try {
|
try {
|
||||||
@@ -80,8 +84,11 @@ constructor(
|
|||||||
failed = results.failed()
|
failed = results.failed()
|
||||||
isExampleWrittenFailure = results.isExampleWrittenFailure.and(isExampleWrittenFailure)
|
isExampleWrittenFailure = results.isExampleWrittenFailure.and(isExampleWrittenFailure)
|
||||||
}
|
}
|
||||||
reporter.report(results, consoleWriter)
|
val tmpWriter = StringWriter()
|
||||||
if (sources.size > 1 && idx != sources.size - 1) {
|
reporter.report(results, tmpWriter)
|
||||||
|
val report = tmpWriter.toString()
|
||||||
|
consoleWriter.write(report)
|
||||||
|
if (report.isNotEmpty() && sources.size > 1 && idx != sources.size - 1) {
|
||||||
consoleWriter.append('\n')
|
consoleWriter.append('\n')
|
||||||
}
|
}
|
||||||
consoleWriter.flush()
|
consoleWriter.flush()
|
||||||
@@ -101,7 +108,7 @@ constructor(
|
|||||||
moduleNames += moduleName
|
moduleNames += moduleName
|
||||||
|
|
||||||
if (!testOptions.junitAggregateReports) {
|
if (!testOptions.junitAggregateReports) {
|
||||||
JUnitReport().reportToPath(results, junitDir.resolve(moduleName))
|
JUnitReporter().reportToPath(results, junitDir.resolve(moduleName))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
@@ -119,7 +126,7 @@ constructor(
|
|||||||
}
|
}
|
||||||
if (testOptions.junitAggregateReports && junitDir != null) {
|
if (testOptions.junitAggregateReports && junitDir != null) {
|
||||||
val fileName = "${testOptions.junitAggregateSuiteName}.xml"
|
val fileName = "${testOptions.junitAggregateSuiteName}.xml"
|
||||||
JUnitReport(testOptions.junitAggregateSuiteName)
|
JUnitReporter(testOptions.junitAggregateSuiteName)
|
||||||
.summarizeToPath(allTestResults, junitDir.resolve(fileName))
|
.summarizeToPath(allTestResults, junitDir.resolve(fileName))
|
||||||
}
|
}
|
||||||
consoleWriter.append('\n')
|
consoleWriter.append('\n')
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -22,4 +22,5 @@ class CliTestOptions(
|
|||||||
val overwrite: Boolean = false,
|
val overwrite: Boolean = false,
|
||||||
val junitAggregateReports: Boolean = false,
|
val junitAggregateReports: Boolean = false,
|
||||||
val junitAggregateSuiteName: String = "pkl-tests",
|
val junitAggregateSuiteName: String = "pkl-tests",
|
||||||
|
val reporter: TestReporter = TestReporter.SPEC,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 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.commons.cli
|
||||||
|
|
||||||
|
enum class TestReporter {
|
||||||
|
SPEC,
|
||||||
|
MINIMAL,
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -19,9 +19,11 @@ import com.github.ajalt.clikt.parameters.groups.OptionGroup
|
|||||||
import com.github.ajalt.clikt.parameters.options.default
|
import com.github.ajalt.clikt.parameters.options.default
|
||||||
import com.github.ajalt.clikt.parameters.options.flag
|
import com.github.ajalt.clikt.parameters.options.flag
|
||||||
import com.github.ajalt.clikt.parameters.options.option
|
import com.github.ajalt.clikt.parameters.options.option
|
||||||
|
import com.github.ajalt.clikt.parameters.types.enum
|
||||||
import com.github.ajalt.clikt.parameters.types.path
|
import com.github.ajalt.clikt.parameters.types.path
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import org.pkl.commons.cli.CliTestOptions
|
import org.pkl.commons.cli.CliTestOptions
|
||||||
|
import org.pkl.commons.cli.TestReporter
|
||||||
|
|
||||||
class TestOptions : OptionGroup() {
|
class TestOptions : OptionGroup() {
|
||||||
private val junitReportDir: Path? by
|
private val junitReportDir: Path? by
|
||||||
@@ -51,7 +53,19 @@ class TestOptions : OptionGroup() {
|
|||||||
private val overwrite: Boolean by
|
private val overwrite: Boolean by
|
||||||
option(names = arrayOf("--overwrite"), help = "Force generation of expected examples.").flag()
|
option(names = arrayOf("--overwrite"), help = "Force generation of expected examples.").flag()
|
||||||
|
|
||||||
|
private val reporter: TestReporter by
|
||||||
|
option(names = arrayOf("--test-reporter"), help = "Which test reporter to use for CLI output.")
|
||||||
|
.enum<TestReporter> { it.name.lowercase() }
|
||||||
|
.single()
|
||||||
|
.default(TestReporter.SPEC)
|
||||||
|
|
||||||
val cliTestOptions: CliTestOptions by lazy {
|
val cliTestOptions: CliTestOptions by lazy {
|
||||||
CliTestOptions(junitReportDir, overwrite, junitAggregateReports, junitAggregateSuiteName)
|
CliTestOptions(
|
||||||
|
junitReportDir,
|
||||||
|
overwrite,
|
||||||
|
junitAggregateReports,
|
||||||
|
junitAggregateSuiteName,
|
||||||
|
reporter,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-37
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -24,41 +24,21 @@ import java.util.Locale;
|
|||||||
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.util.AnsiStringBuilder;
|
import org.pkl.core.util.AnsiStringBuilder;
|
||||||
import org.pkl.core.util.AnsiStringBuilder.AnsiCode;
|
import org.pkl.core.util.AnsiStringBuilder.AnsiCode;
|
||||||
import org.pkl.core.util.AnsiTheme;
|
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 abstract class BaseReporter implements TestReporter {
|
||||||
|
protected static final String passingMark = "✔ ";
|
||||||
|
protected static final String failingMark = "✘ ";
|
||||||
|
|
||||||
private static final String passingMark = "✔ ";
|
protected final boolean useColor;
|
||||||
private static final String failingMark = "✘ ";
|
|
||||||
|
|
||||||
private final boolean useColor;
|
public BaseReporter(boolean useColor) {
|
||||||
|
|
||||||
public SimpleReport(boolean useColor) {
|
|
||||||
this.useColor = useColor;
|
this.useColor = useColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void report(TestResults results, Writer writer) throws IOException {
|
|
||||||
var builder = new AnsiStringBuilder(useColor);
|
|
||||||
|
|
||||||
builder.append("module ").append(results.moduleName()).append('\n');
|
|
||||||
|
|
||||||
if (results.error() != null) {
|
|
||||||
var rendered = results.error().exception().getMessage();
|
|
||||||
appendPadded(builder, rendered, " ");
|
|
||||||
builder.append('\n');
|
|
||||||
} else {
|
|
||||||
reportResults(results.facts(), builder);
|
|
||||||
reportResults(results.examples(), builder);
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.append(builder.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void summarize(List<TestResults> allTestResults, Writer writer) throws IOException {
|
public void summarize(List<TestResults> allTestResults, Writer writer) throws IOException {
|
||||||
var totalTests = 0;
|
var totalTests = 0;
|
||||||
@@ -91,16 +71,7 @@ public final class SimpleReport implements TestReport {
|
|||||||
writer.append(builder.toString());
|
writer.append(builder.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reportResults(TestSectionResults section, AnsiStringBuilder builder) {
|
protected void reportResult(TestResult result, AnsiStringBuilder builder) {
|
||||||
if (!section.results().isEmpty()) {
|
|
||||||
builder.append(" ").append(section.name()).append('\n');
|
|
||||||
StringUtils.joinToStringBuilder(
|
|
||||||
builder, section.results(), "\n", res -> reportResult(res, builder));
|
|
||||||
builder.append('\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void reportResult(TestResult result, AnsiStringBuilder builder) {
|
|
||||||
builder.append(" ");
|
builder.append(" ");
|
||||||
|
|
||||||
if (result.isExampleWritten()) {
|
if (result.isExampleWritten()) {
|
||||||
@@ -129,7 +100,7 @@ public final class SimpleReport implements TestReport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void appendPadded(AnsiStringBuilder builder, String lines, String padding) {
|
protected static void appendPadded(AnsiStringBuilder builder, String lines, String padding) {
|
||||||
StringUtils.joinToStringBuilder(
|
StringUtils.joinToStringBuilder(
|
||||||
builder,
|
builder,
|
||||||
lines.lines().collect(Collectors.toList()),
|
lines.lines().collect(Collectors.toList()),
|
||||||
+3
-3
@@ -37,15 +37,15 @@ import org.pkl.core.stdlib.PklConverter;
|
|||||||
import org.pkl.core.stdlib.xml.RendererNodes.Renderer;
|
import org.pkl.core.stdlib.xml.RendererNodes.Renderer;
|
||||||
import org.pkl.core.util.EconomicMaps;
|
import org.pkl.core.util.EconomicMaps;
|
||||||
|
|
||||||
public final class JUnitReport implements TestReport {
|
public final class JUnitReporter implements TestReporter {
|
||||||
|
|
||||||
private final String aggregateSuiteName;
|
private final String aggregateSuiteName;
|
||||||
|
|
||||||
public JUnitReport(String aggregateSuiteName) {
|
public JUnitReporter(String aggregateSuiteName) {
|
||||||
this.aggregateSuiteName = aggregateSuiteName;
|
this.aggregateSuiteName = aggregateSuiteName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public JUnitReport() {
|
public JUnitReporter() {
|
||||||
this("");
|
this("");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 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.stdlib.test.report;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.Writer;
|
||||||
|
import java.util.List;
|
||||||
|
import org.pkl.core.TestResults;
|
||||||
|
import org.pkl.core.TestResults.TestResult;
|
||||||
|
import org.pkl.core.TestResults.TestSectionResults;
|
||||||
|
import org.pkl.core.util.AnsiStringBuilder;
|
||||||
|
import org.pkl.core.util.StringUtils;
|
||||||
|
|
||||||
|
/** Minimal reporter. Only reports failures and errors. */
|
||||||
|
public final class MinimalReporter extends BaseReporter {
|
||||||
|
|
||||||
|
public MinimalReporter(boolean useColor) {
|
||||||
|
super(useColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void report(TestResults results, Writer writer) throws IOException {
|
||||||
|
var builder = new AnsiStringBuilder(useColor);
|
||||||
|
|
||||||
|
if (results.error() != null) {
|
||||||
|
builder.append("module ").append(results.moduleName()).append('\n');
|
||||||
|
|
||||||
|
var rendered = results.error().exception().getMessage();
|
||||||
|
appendPadded(builder, rendered, " ");
|
||||||
|
builder.append('\n');
|
||||||
|
} else {
|
||||||
|
var factFailures = results.facts().results().stream().filter(TestResult::isFailure).toList();
|
||||||
|
var exampleFailures =
|
||||||
|
results.examples().results().stream().filter(TestResult::isFailure).toList();
|
||||||
|
if (!factFailures.isEmpty() || !exampleFailures.isEmpty()) {
|
||||||
|
builder.append("module ").append(results.moduleName()).append('\n');
|
||||||
|
|
||||||
|
reportResults(results.facts(), factFailures, builder);
|
||||||
|
reportResults(results.examples(), exampleFailures, builder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.append(builder.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportResults(
|
||||||
|
TestSectionResults section, List<TestResults.TestResult> results, AnsiStringBuilder builder) {
|
||||||
|
if (!results.isEmpty()) {
|
||||||
|
builder.append(" ").append(section.name()).append('\n');
|
||||||
|
StringUtils.joinToStringBuilder(builder, results, "\n", res -> reportResult(res, builder));
|
||||||
|
builder.append('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2024-2026 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.stdlib.test.report;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.Writer;
|
||||||
|
import org.pkl.core.TestResults;
|
||||||
|
import org.pkl.core.TestResults.TestSectionResults;
|
||||||
|
import org.pkl.core.util.AnsiStringBuilder;
|
||||||
|
import org.pkl.core.util.StringUtils;
|
||||||
|
|
||||||
|
public final class SpecReporter extends BaseReporter {
|
||||||
|
|
||||||
|
public SpecReporter(boolean useColor) {
|
||||||
|
super(useColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void report(TestResults results, Writer writer) throws IOException {
|
||||||
|
var builder = new AnsiStringBuilder(useColor);
|
||||||
|
|
||||||
|
builder.append("module ").append(results.moduleName()).append('\n');
|
||||||
|
|
||||||
|
if (results.error() != null) {
|
||||||
|
var rendered = results.error().exception().getMessage();
|
||||||
|
appendPadded(builder, rendered, " ");
|
||||||
|
builder.append('\n');
|
||||||
|
} else {
|
||||||
|
reportResults(results.facts(), builder);
|
||||||
|
reportResults(results.examples(), builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.append(builder.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportResults(TestSectionResults section, AnsiStringBuilder builder) {
|
||||||
|
if (!section.results().isEmpty()) {
|
||||||
|
builder.append(" ").append(section.name()).append('\n');
|
||||||
|
StringUtils.joinToStringBuilder(
|
||||||
|
builder, section.results(), "\n", res -> reportResult(res, builder));
|
||||||
|
builder.append('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -25,7 +25,7 @@ import org.pkl.core.PklBugException;
|
|||||||
import org.pkl.core.TestResults;
|
import org.pkl.core.TestResults;
|
||||||
import org.pkl.core.util.StringBuilderWriter;
|
import org.pkl.core.util.StringBuilderWriter;
|
||||||
|
|
||||||
public interface TestReport {
|
public interface TestReporter {
|
||||||
|
|
||||||
void report(TestResults results, Writer writer) throws IOException;
|
void report(TestResults results, Writer writer) throws IOException;
|
||||||
|
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 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.stdlib
|
||||||
|
|
||||||
|
import java.io.StringWriter
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.pkl.core.TestResults
|
||||||
|
import org.pkl.core.TestResults.TestResult
|
||||||
|
import org.pkl.core.TestResults.TestSectionResults
|
||||||
|
import org.pkl.core.stdlib.test.report.MinimalReporter
|
||||||
|
|
||||||
|
class MinimalReportTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `report with only passing tests does not show module or test names`() {
|
||||||
|
val resultsBuilder = TestResults.Builder("module1", "module1")
|
||||||
|
resultsBuilder.setFactsSection(
|
||||||
|
TestSectionResults(
|
||||||
|
TestResults.TestSectionName.FACTS,
|
||||||
|
listOf(TestResult("passing fact", 1, emptyList(), emptyList(), false)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
resultsBuilder.setExamplesSection(
|
||||||
|
TestSectionResults(
|
||||||
|
TestResults.TestSectionName.EXAMPLES,
|
||||||
|
listOf(TestResult("passing example", 1, emptyList(), emptyList(), false)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val testResults = resultsBuilder.build()
|
||||||
|
|
||||||
|
val writer = StringWriter()
|
||||||
|
val minimalReport = MinimalReporter(false)
|
||||||
|
minimalReport.report(testResults, writer)
|
||||||
|
|
||||||
|
assertThat(writer.toString()).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `report with failures shows module name and only failed tests`() {
|
||||||
|
val resultsBuilder = TestResults.Builder("module1", "module1")
|
||||||
|
resultsBuilder.setFactsSection(
|
||||||
|
TestSectionResults(
|
||||||
|
TestResults.TestSectionName.FACTS,
|
||||||
|
listOf(
|
||||||
|
TestResult("passing fact", 1, emptyList(), emptyList(), false),
|
||||||
|
TestResult(
|
||||||
|
"failing fact",
|
||||||
|
1,
|
||||||
|
listOf(TestResults.Failure("Fact Failure", "failed")),
|
||||||
|
emptyList(),
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
resultsBuilder.setExamplesSection(
|
||||||
|
TestSectionResults(
|
||||||
|
TestResults.TestSectionName.EXAMPLES,
|
||||||
|
listOf(TestResult("passing example", 1, emptyList(), emptyList(), false)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val testResults = resultsBuilder.build()
|
||||||
|
|
||||||
|
val writer = StringWriter()
|
||||||
|
val minimalReport = MinimalReporter(false)
|
||||||
|
minimalReport.report(testResults, writer)
|
||||||
|
|
||||||
|
val output = writer.toString()
|
||||||
|
assertThat(output).contains("module module1")
|
||||||
|
assertThat(output).contains("failing fact")
|
||||||
|
assertThat(output).doesNotContain("passing fact")
|
||||||
|
assertThat(output).doesNotContain("passing example")
|
||||||
|
assertThat(output).doesNotContain("examples")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `summarize includes stats even when all tests pass`() {
|
||||||
|
val resultsBuilder = TestResults.Builder("module1", "module1")
|
||||||
|
resultsBuilder.setFactsSection(
|
||||||
|
TestSectionResults(
|
||||||
|
TestResults.TestSectionName.FACTS,
|
||||||
|
listOf(TestResult("passing fact", 1, emptyList(), emptyList(), false)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
resultsBuilder.setExamplesSection(
|
||||||
|
TestSectionResults(TestResults.TestSectionName.EXAMPLES, emptyList())
|
||||||
|
)
|
||||||
|
val testResults = listOf(resultsBuilder.build())
|
||||||
|
|
||||||
|
val writer = StringWriter()
|
||||||
|
val minimalReport = MinimalReporter(false)
|
||||||
|
minimalReport.summarize(testResults, writer)
|
||||||
|
|
||||||
|
val output = writer.toString()
|
||||||
|
assertThat(output).contains("100.0% tests pass")
|
||||||
|
assertThat(output).contains("1 passed")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `summarize method should generate correct output for failures`() {
|
||||||
|
val resultsBuilder = TestResults.Builder("module1", "module1")
|
||||||
|
resultsBuilder.setFactsSection(
|
||||||
|
TestSectionResults(
|
||||||
|
TestResults.TestSectionName.FACTS,
|
||||||
|
listOf(
|
||||||
|
TestResult(
|
||||||
|
"example1",
|
||||||
|
321919,
|
||||||
|
listOf(TestResults.Failure("Fact Failure", "failed")),
|
||||||
|
emptyList(),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
resultsBuilder.setExamplesSection(
|
||||||
|
TestSectionResults(
|
||||||
|
TestResults.TestSectionName.EXAMPLES,
|
||||||
|
listOf(
|
||||||
|
TestResult(
|
||||||
|
"example1",
|
||||||
|
432525,
|
||||||
|
listOf(TestResults.Failure("Output Mismatch", "does not match")),
|
||||||
|
emptyList(),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val testResults = listOf(resultsBuilder.build())
|
||||||
|
|
||||||
|
val writer = StringWriter()
|
||||||
|
val minimalReport = MinimalReporter(false)
|
||||||
|
minimalReport.summarize(testResults, writer)
|
||||||
|
|
||||||
|
val expectedOutput =
|
||||||
|
"""
|
||||||
|
0.0% tests pass [2/2 failed], 99.9% asserts pass [2/754444 failed]
|
||||||
|
"""
|
||||||
|
.trimIndent()
|
||||||
|
|
||||||
|
assertThat(writer.toString().trimIndent()).isEqualTo(expectedOutput)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,19 +16,18 @@
|
|||||||
package org.pkl.core.stdlib
|
package org.pkl.core.stdlib
|
||||||
|
|
||||||
import java.io.StringWriter
|
import java.io.StringWriter
|
||||||
import java.util.*
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
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.stdlib.test.report.SimpleReport
|
import org.pkl.core.stdlib.test.report.SpecReporter
|
||||||
|
|
||||||
class SimpleReportTest {
|
class SimpleReportTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `summarize method should generate correct output`() {
|
fun `summarize method should generate correct output`() {
|
||||||
var resultsBuilder = TestResults.Builder("module1", "module1")
|
val resultsBuilder = TestResults.Builder("module1", "module1")
|
||||||
resultsBuilder.setFactsSection(
|
resultsBuilder.setFactsSection(
|
||||||
TestSectionResults(
|
TestSectionResults(
|
||||||
TestResults.TestSectionName.FACTS,
|
TestResults.TestSectionName.FACTS,
|
||||||
@@ -60,7 +59,7 @@ class SimpleReportTest {
|
|||||||
val testResults = listOf(resultsBuilder.build())
|
val testResults = listOf(resultsBuilder.build())
|
||||||
|
|
||||||
val writer = StringWriter()
|
val writer = StringWriter()
|
||||||
val simpleReport = SimpleReport(false)
|
val simpleReport = SpecReporter(false)
|
||||||
simpleReport.summarize(testResults, writer)
|
simpleReport.summarize(testResults, writer)
|
||||||
|
|
||||||
val expectedOutput =
|
val expectedOutput =
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ public class PklPlugin implements Plugin<Project> {
|
|||||||
spec.getOutputPath()
|
spec.getOutputPath()
|
||||||
.convention(project.getLayout().getBuildDirectory().dir("generated/pkl/packages"));
|
.convention(project.getLayout().getBuildDirectory().dir("generated/pkl/packages"));
|
||||||
spec.getOverwrite().convention(false);
|
spec.getOverwrite().convention(false);
|
||||||
|
spec.getReporter().convention("spec");
|
||||||
var packageTask = createTask(project, ProjectPackageTask.class, spec);
|
var packageTask = createTask(project, ProjectPackageTask.class, spec);
|
||||||
packageTask.configure(
|
packageTask.configure(
|
||||||
task -> {
|
task -> {
|
||||||
@@ -108,6 +109,7 @@ public class PklPlugin implements Plugin<Project> {
|
|||||||
task.getSkipPublishCheck().set(spec.getSkipPublishCheck());
|
task.getSkipPublishCheck().set(spec.getSkipPublishCheck());
|
||||||
task.getJunitReportsDir().set(spec.getJunitReportsDir());
|
task.getJunitReportsDir().set(spec.getJunitReportsDir());
|
||||||
task.getOverwrite().set(spec.getOverwrite());
|
task.getOverwrite().set(spec.getOverwrite());
|
||||||
|
task.getTestReporter().set(spec.getReporter());
|
||||||
});
|
});
|
||||||
project
|
project
|
||||||
.getPluginManager()
|
.getPluginManager()
|
||||||
@@ -278,12 +280,14 @@ public class PklPlugin implements Plugin<Project> {
|
|||||||
configureBaseSpec(project, spec);
|
configureBaseSpec(project, spec);
|
||||||
|
|
||||||
spec.getOverwrite().convention(false);
|
spec.getOverwrite().convention(false);
|
||||||
|
spec.getTestReporter().convention("spec");
|
||||||
|
|
||||||
var testTask = createModulesTask(project, TestTask.class, spec);
|
var testTask = createModulesTask(project, TestTask.class, spec);
|
||||||
testTask.configure(
|
testTask.configure(
|
||||||
task -> {
|
task -> {
|
||||||
task.getJunitReportsDir().set(spec.getJunitReportsDir());
|
task.getJunitReportsDir().set(spec.getJunitReportsDir());
|
||||||
task.getOverwrite().set(spec.getOverwrite());
|
task.getOverwrite().set(spec.getOverwrite());
|
||||||
|
task.getTestReporter().set(spec.getTestReporter());
|
||||||
});
|
});
|
||||||
|
|
||||||
project
|
project
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -29,4 +29,6 @@ public interface ProjectPackageSpec extends BasePklSpec {
|
|||||||
Property<Boolean> getOverwrite();
|
Property<Boolean> getOverwrite();
|
||||||
|
|
||||||
Property<Boolean> getSkipPublishCheck();
|
Property<Boolean> getSkipPublishCheck();
|
||||||
|
|
||||||
|
Property<String> getReporter();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -22,4 +22,6 @@ public interface TestSpec extends ModulesSpec {
|
|||||||
DirectoryProperty getJunitReportsDir();
|
DirectoryProperty getJunitReportsDir();
|
||||||
|
|
||||||
Property<Boolean> getOverwrite();
|
Property<Boolean> getOverwrite();
|
||||||
|
|
||||||
|
Property<String> getTestReporter();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import org.gradle.api.tasks.PathSensitivity;
|
|||||||
import org.gradle.api.tasks.UntrackedTask;
|
import org.gradle.api.tasks.UntrackedTask;
|
||||||
import org.pkl.cli.CliProjectPackager;
|
import org.pkl.cli.CliProjectPackager;
|
||||||
import org.pkl.commons.cli.CliTestOptions;
|
import org.pkl.commons.cli.CliTestOptions;
|
||||||
|
import org.pkl.commons.cli.TestReporter;
|
||||||
|
|
||||||
@UntrackedTask(because = "Output names are known only after execution")
|
@UntrackedTask(because = "Output names are known only after execution")
|
||||||
public abstract class ProjectPackageTask extends BasePklTask {
|
public abstract class ProjectPackageTask extends BasePklTask {
|
||||||
@@ -62,6 +63,10 @@ public abstract class ProjectPackageTask extends BasePklTask {
|
|||||||
@Optional
|
@Optional
|
||||||
public abstract Property<Boolean> getSkipPublishCheck();
|
public abstract Property<Boolean> getSkipPublishCheck();
|
||||||
|
|
||||||
|
@Input
|
||||||
|
@Optional
|
||||||
|
public abstract Property<String> getTestReporter();
|
||||||
|
|
||||||
public ProjectPackageTask() {
|
public ProjectPackageTask() {
|
||||||
this.getJunitAggregateSuiteName().convention("pkl-tests");
|
this.getJunitAggregateSuiteName().convention("pkl-tests");
|
||||||
}
|
}
|
||||||
@@ -75,6 +80,18 @@ public abstract class ProjectPackageTask extends BasePklTask {
|
|||||||
if (projectDirectories.isEmpty()) {
|
if (projectDirectories.isEmpty()) {
|
||||||
throw new InvalidUserDataException("No project directories specified.");
|
throw new InvalidUserDataException("No project directories specified.");
|
||||||
}
|
}
|
||||||
|
TestReporter testReporter;
|
||||||
|
try {
|
||||||
|
testReporter = TestReporter.valueOf(getTestReporter().getOrElse("SPEC").toUpperCase());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new InvalidUserDataException(
|
||||||
|
"Invalid reporter: '%s'. Valid reporter options: %s"
|
||||||
|
.formatted(
|
||||||
|
getTestReporter().get(),
|
||||||
|
TestReporter.getEntries().stream()
|
||||||
|
.map(it -> it.name().toLowerCase())
|
||||||
|
.collect(Collectors.joining(", "))));
|
||||||
|
}
|
||||||
|
|
||||||
new CliProjectPackager(
|
new CliProjectPackager(
|
||||||
getCliBaseOptions(),
|
getCliBaseOptions(),
|
||||||
@@ -83,7 +100,8 @@ public abstract class ProjectPackageTask extends BasePklTask {
|
|||||||
mapAndGetOrNull(getJunitReportsDir(), it -> it.getAsFile().toPath()),
|
mapAndGetOrNull(getJunitReportsDir(), it -> it.getAsFile().toPath()),
|
||||||
getOverwrite().get(),
|
getOverwrite().get(),
|
||||||
getJunitAggregateReports().getOrElse(false),
|
getJunitAggregateReports().getOrElse(false),
|
||||||
getJunitAggregateSuiteName().get()),
|
getJunitAggregateSuiteName().get(),
|
||||||
|
testReporter),
|
||||||
getOutputPath().get().getAsFile().getAbsolutePath(),
|
getOutputPath().get().getAsFile().getAbsolutePath(),
|
||||||
getSkipPublishCheck().getOrElse(false),
|
getSkipPublishCheck().getOrElse(false),
|
||||||
new PrintWriter(System.out),
|
new PrintWriter(System.out),
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ package org.pkl.gradle.task;
|
|||||||
import static org.pkl.gradle.utils.PluginUtils.mapAndGetOrNull;
|
import static org.pkl.gradle.utils.PluginUtils.mapAndGetOrNull;
|
||||||
|
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import org.gradle.api.InvalidUserDataException;
|
||||||
import org.gradle.api.file.DirectoryProperty;
|
import org.gradle.api.file.DirectoryProperty;
|
||||||
import org.gradle.api.provider.Property;
|
import org.gradle.api.provider.Property;
|
||||||
import org.gradle.api.tasks.CacheableTask;
|
import org.gradle.api.tasks.CacheableTask;
|
||||||
@@ -26,6 +28,7 @@ import org.gradle.api.tasks.Optional;
|
|||||||
import org.gradle.api.tasks.OutputDirectory;
|
import org.gradle.api.tasks.OutputDirectory;
|
||||||
import org.pkl.cli.CliTestRunner;
|
import org.pkl.cli.CliTestRunner;
|
||||||
import org.pkl.commons.cli.CliTestOptions;
|
import org.pkl.commons.cli.CliTestOptions;
|
||||||
|
import org.pkl.commons.cli.TestReporter;
|
||||||
|
|
||||||
@CacheableTask
|
@CacheableTask
|
||||||
public abstract class TestTask extends ModulesTask {
|
public abstract class TestTask extends ModulesTask {
|
||||||
@@ -43,6 +46,10 @@ public abstract class TestTask extends ModulesTask {
|
|||||||
@Input
|
@Input
|
||||||
public abstract Property<Boolean> getOverwrite();
|
public abstract Property<Boolean> getOverwrite();
|
||||||
|
|
||||||
|
@Input
|
||||||
|
@Optional
|
||||||
|
public abstract Property<String> getTestReporter();
|
||||||
|
|
||||||
public TestTask() {
|
public TestTask() {
|
||||||
this.getJunitAggregateSuiteName().convention("pkl-tests");
|
this.getJunitAggregateSuiteName().convention("pkl-tests");
|
||||||
this.getPowerAssertions().convention(true);
|
this.getPowerAssertions().convention(true);
|
||||||
@@ -50,13 +57,26 @@ public abstract class TestTask extends ModulesTask {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doRunTask() {
|
protected void doRunTask() {
|
||||||
|
TestReporter testReporter;
|
||||||
|
try {
|
||||||
|
testReporter = TestReporter.valueOf(getTestReporter().getOrElse("SPEC").toUpperCase());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new InvalidUserDataException(
|
||||||
|
"Invalid reporter: '%s'. Valid reporter options: %s"
|
||||||
|
.formatted(
|
||||||
|
getTestReporter().get(),
|
||||||
|
TestReporter.getEntries().stream()
|
||||||
|
.map(it -> it.name().toLowerCase())
|
||||||
|
.collect(Collectors.joining(", "))));
|
||||||
|
}
|
||||||
new CliTestRunner(
|
new CliTestRunner(
|
||||||
getCliBaseOptions(),
|
getCliBaseOptions(),
|
||||||
new CliTestOptions(
|
new CliTestOptions(
|
||||||
mapAndGetOrNull(getJunitReportsDir(), it -> it.getAsFile().toPath()),
|
mapAndGetOrNull(getJunitReportsDir(), it -> it.getAsFile().toPath()),
|
||||||
getOverwrite().get(),
|
getOverwrite().get(),
|
||||||
getJunitAggregateReports().getOrElse(false),
|
getJunitAggregateReports().getOrElse(false),
|
||||||
getJunitAggregateSuiteName().get()),
|
getJunitAggregateSuiteName().get(),
|
||||||
|
testReporter),
|
||||||
new PrintWriter(System.out),
|
new PrintWriter(System.out),
|
||||||
new PrintWriter(System.err))
|
new PrintWriter(System.err))
|
||||||
.run();
|
.run();
|
||||||
|
|||||||
Reference in New Issue
Block a user