Introduce "minimal" test reporter (#1563)

This commit is contained in:
Islon Scherer
2026-05-19 17:20:26 +02:00
committed by GitHub
parent 566c42f44d
commit 3fbcd463e0
18 changed files with 438 additions and 63 deletions
+17
View File
@@ -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]]
+17
View File
@@ -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,
)
} }
} }
@@ -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()),
@@ -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');
}
}
}
@@ -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();