From 07f6a3ccf335fa8b4a0e44e95d0118d2e6f0211e Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Wed, 21 Feb 2024 15:25:59 +0100 Subject: [PATCH] Qute: introduce RenderedResults assert API for tests --- docs/src/main/asciidoc/qute-reference.adoc | 6 + .../qute/deployment/QuteProcessor.java | 17 ++ .../qute/deployment/test/FooTemplates.java | 12 ++ .../test/RenderedResultsDisabledTest.java | 33 ++++ .../deployment/test/RenderedResultsTest.java | 98 ++++++++++++ .../qute/deployment/test/SimpleBean.java | 19 +++ .../io/quarkus/qute/runtime/QuteConfig.java | 8 +- .../qute/runtime/QuteTestModeConfig.java | 17 ++ .../qute/runtime/TemplateProducer.java | 22 ++- .../runtime/test/RenderedResultsCreator.java | 14 ++ .../qute/ForwardingTemplateInstance.java | 82 ++++++++++ .../java/io/quarkus/qute/RenderedResults.java | 145 ++++++++++++++++++ .../ResultsCollectingTemplateInstance.java | 71 +++++++++ ...ResultsCollectingTemplateInstanceTest.java | 126 +++++++++++++++ 14 files changed, 664 insertions(+), 6 deletions(-) create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/test/FooTemplates.java create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/test/RenderedResultsDisabledTest.java create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/test/RenderedResultsTest.java create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/test/SimpleBean.java create mode 100644 extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteTestModeConfig.java create mode 100644 extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/test/RenderedResultsCreator.java create mode 100644 independent-projects/qute/core/src/main/java/io/quarkus/qute/ForwardingTemplateInstance.java create mode 100644 independent-projects/qute/core/src/main/java/io/quarkus/qute/RenderedResults.java create mode 100644 independent-projects/qute/core/src/main/java/io/quarkus/qute/ResultsCollectingTemplateInstance.java create mode 100644 independent-projects/qute/core/src/test/java/io/quarkus/qute/ResultsCollectingTemplateInstanceTest.java diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index ba56d9283048c..799ac39c1b0bb 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -2654,6 +2654,12 @@ The configration value is a regular expression that matches the template path re For example, `quarkus.qute.dev-mode.no-restart-templates=templates/foo.html` matches the template `src/main/resources/templates/foo.html`. The matching templates are reloaded and only runtime validations are performed. +=== Testing + +In the test mode, the rendering results of injected and type-safe templates are recorded in the managed `io.quarkus.qute.RenderedResults` which is registered as a CDI bean. +You can inject `RenderedResults` in a test or any other CDI bean and assert the results. +However, it's possible to set the `quarkus.qute.test-mode.record-rendered-results` configuration property to `false` to disable this feature. + [[type-safe-message-bundles]] === Type-safe Message Bundles diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java index c2fc7b7393167..e2ab7c880eaed 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java @@ -41,6 +41,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import jakarta.inject.Singleton; + import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.AnnotationTarget.Kind; @@ -77,6 +79,7 @@ import io.quarkus.deployment.ApplicationArchive; import io.quarkus.deployment.Feature; import io.quarkus.deployment.GeneratedClassGizmoAdaptor; +import io.quarkus.deployment.IsTest; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.Record; @@ -109,6 +112,7 @@ import io.quarkus.qute.ParameterDeclaration; import io.quarkus.qute.ParserHelper; import io.quarkus.qute.ParserHook; +import io.quarkus.qute.RenderedResults; import io.quarkus.qute.ResultNode; import io.quarkus.qute.SectionHelper; import io.quarkus.qute.SectionHelperFactory; @@ -148,6 +152,7 @@ import io.quarkus.qute.runtime.extensions.OrOperatorTemplateExtensions; import io.quarkus.qute.runtime.extensions.StringTemplateExtensions; import io.quarkus.qute.runtime.extensions.TimeTemplateExtensions; +import io.quarkus.qute.runtime.test.RenderedResultsCreator; import io.quarkus.runtime.util.StringUtil; public class QuteProcessor { @@ -874,6 +879,18 @@ void validateCheckedFragments(List validatio } } + @BuildStep(onlyIf = IsTest.class) + SyntheticBeanBuildItem registerRenderedResults(QuteConfig config) { + if (config.testMode.recordRenderedResults) { + return SyntheticBeanBuildItem.configure(RenderedResults.class) + .unremovable() + .scope(Singleton.class) + .creator(RenderedResultsCreator.class) + .done(); + } + return null; + } + @SuppressWarnings("incomplete-switch") private static String getCheckedTemplateParameterTypeName(Type type) { switch (type.kind()) { diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/test/FooTemplates.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/test/FooTemplates.java new file mode 100644 index 0000000000000..3aa4576ca3168 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/test/FooTemplates.java @@ -0,0 +1,12 @@ +package io.quarkus.qute.deployment.test; + +import io.quarkus.qute.CheckedTemplate; +import io.quarkus.qute.TemplateInstance; + +@CheckedTemplate +public class FooTemplates { + + static native TemplateInstance foo(String name); + + static native TemplateInstance foo$bar(); +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/test/RenderedResultsDisabledTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/test/RenderedResultsDisabledTest.java new file mode 100644 index 0000000000000..867778d4ee832 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/test/RenderedResultsDisabledTest.java @@ -0,0 +1,33 @@ +package io.quarkus.qute.deployment.test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.RenderedResults; +import io.quarkus.test.QuarkusUnitTest; + +public class RenderedResultsDisabledTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(SimpleBean.class) + .addAsResource(new StringAsset("quarkus.qute.test-mode.record-rendered-results=false"), + "application.properties") + .addAsResource(new StringAsset("{name}"), "templates/foo.txt")); + + @Inject + Instance renderedResults; + + @Test + public void testRenderedResultsNotRegistered() { + assertTrue(renderedResults.isUnsatisfied()); + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/test/RenderedResultsTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/test/RenderedResultsTest.java new file mode 100644 index 0000000000000..3c15c854ebc24 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/test/RenderedResultsTest.java @@ -0,0 +1,98 @@ +package io.quarkus.qute.deployment.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.RenderedResults; +import io.quarkus.qute.RenderedResults.RenderedResult; +import io.quarkus.qute.TemplateInstance; +import io.quarkus.qute.Variant; +import io.quarkus.test.QuarkusUnitTest; + +public class RenderedResultsTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(SimpleBean.class, FooTemplates.class) + .addAsResource(new StringAsset("quarkus.qute.suffixes=txt,html"), "application.properties") + .addAsResource(new StringAsset("{name}{#fragment id=bar rendered=false}bar{/fragment}"), + "templates/foo.txt") + .addAsResource(new StringAsset("

{name}{#fragment id=bar rendered=false}bar{/fragment}

"), + "templates/foo.html")); + + @Inject + RenderedResults renderedResults; + + @Inject + SimpleBean bean; + + @Test + public void testInjectedTemplate() throws InterruptedException { + assertResults(() -> bean.fooInstance().data("name", "oof").render(), "foo.txt", "oof"); + } + + @Test + public void testInjectedTemplateSelectedVariant() throws InterruptedException { + assertResults(() -> bean.fooInstance() + .setAttribute(TemplateInstance.SELECTED_VARIANT, Variant.forContentType(Variant.TEXT_HTML)) + .data("name", "oof") + .render(), "foo.html", "

oof

"); + } + + @Test + public void testTypesafeTemplate() throws InterruptedException { + assertResults(() -> FooTemplates.foo("oof").render(), "foo.txt", "oof"); + } + + @Test + public void testTypesafeFragment() throws InterruptedException { + assertResults(() -> FooTemplates.foo$bar().render(), "foo.txt$bar", "bar"); + } + + @Test + public void testTypesafeTemplateSelectedVariant() throws InterruptedException { + assertResults( + () -> FooTemplates.foo("oof") + .setAttribute(TemplateInstance.SELECTED_VARIANT, Variant.forContentType(Variant.TEXT_HTML)).render(), + "foo.html", "

oof

"); + } + + @Test + public void testTypesafeFragmentSelectedVariant() throws InterruptedException { + assertResults( + () -> FooTemplates.foo$bar() + .setAttribute(TemplateInstance.SELECTED_VARIANT, Variant.forContentType(Variant.TEXT_HTML)).render(), + "foo.html$bar", "bar"); + } + + private void assertResults(Supplier renderAction, String templateId, String expectedResult) + throws InterruptedException { + renderedResults.clear(); + assertEquals(expectedResult, renderAction.get()); + // Wait a little so that we can test the RenderedResult#timeout() + // Note that LocalDateTime.now() has precision of the system clock and it seems that windows has millisecond precision + TimeUnit.MILLISECONDS.sleep(50); + List results = renderedResults.getResults(templateId); + assertEquals(1, results.size(), renderedResults.toString()); + assertEquals(expectedResult, results.get(0).result()); + assertEquals(expectedResult, renderAction.get()); + results = renderedResults.getResults(templateId); + assertEquals(2, results.size(), renderedResults.toString()); + assertEquals(expectedResult, results.get(1).result()); + assertTrue(results.get(0).timestamp().isBefore(results.get(1).timestamp())); + renderedResults.clear(); + assertTrue(renderedResults.getResults(templateId).isEmpty()); + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/test/SimpleBean.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/test/SimpleBean.java new file mode 100644 index 0000000000000..04439289c7450 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/test/SimpleBean.java @@ -0,0 +1,19 @@ +package io.quarkus.qute.deployment.test; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateInstance; + +@Singleton +public class SimpleBean { + + @Inject + Template foo; + + public TemplateInstance fooInstance() { + return foo.instance(); + } + +} diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteConfig.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteConfig.java index 5849bb84a5fdd..ad59c9a174406 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteConfig.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteConfig.java @@ -89,9 +89,15 @@ public class QuteConfig { public Charset defaultCharset; /** - * Dev mode configuration. + * Development mode configuration. */ @ConfigItem public QuteDevModeConfig devMode; + /** + * Test mode configuration. + */ + @ConfigItem + public QuteTestModeConfig testMode; + } diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteTestModeConfig.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteTestModeConfig.java new file mode 100644 index 0000000000000..13d4dfa30d294 --- /dev/null +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteTestModeConfig.java @@ -0,0 +1,17 @@ +package io.quarkus.qute.runtime; + +import io.quarkus.qute.RenderedResults; +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class QuteTestModeConfig { + + /** + * By default, the rendering results of injected and type-safe templates are recorded in the managed + * {@link RenderedResults} which is registered as a CDI bean. + */ + @ConfigItem(defaultValue = "true") + public boolean recordRenderedResults; + +} \ No newline at end of file diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java index 6f15bfa4bca36..1a7c6256a1664 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java @@ -18,6 +18,7 @@ import java.util.function.Predicate; import java.util.function.Supplier; +import jakarta.enterprise.inject.Instance; import jakarta.enterprise.inject.Produces; import jakarta.enterprise.inject.spi.AnnotatedParameter; import jakarta.enterprise.inject.spi.InjectionPoint; @@ -30,6 +31,8 @@ import io.quarkus.qute.Expression; import io.quarkus.qute.Location; import io.quarkus.qute.ParameterDeclaration; +import io.quarkus.qute.RenderedResults; +import io.quarkus.qute.ResultsCollectingTemplateInstance; import io.quarkus.qute.Template; import io.quarkus.qute.TemplateInstance; import io.quarkus.qute.TemplateInstanceBase; @@ -51,7 +54,10 @@ public class TemplateProducer { // In the dev mode, we need to keep track of injected templates so that we can clear the cached values private final List> injectedTemplates; - TemplateProducer(Engine engine, QuteContext context, ContentTypes contentTypes, LaunchMode launchMode) { + private final RenderedResults renderedResults; + + TemplateProducer(Engine engine, QuteContext context, ContentTypes contentTypes, LaunchMode launchMode, + Instance renderedResults) { this.engine = engine; Map templateVariants = new HashMap<>(); for (Entry> entry : context.getVariants().entrySet()) { @@ -60,6 +66,7 @@ public class TemplateProducer { templateVariants.put(entry.getKey(), var); } this.templateVariants = Collections.unmodifiableMap(templateVariants); + this.renderedResults = launchMode == LaunchMode.TEST ? renderedResults.get() : null; this.injectedTemplates = launchMode == LaunchMode.DEVELOPMENT ? Collections.synchronizedList(new ArrayList<>()) : null; LOGGER.debugf("Initializing Qute variant templates: %s", templateVariants); } @@ -122,7 +129,7 @@ public void clearInjectedTemplates() { } private Template newInjectableTemplate(String path) { - InjectableTemplate template = new InjectableTemplate(path, templateVariants, engine); + InjectableTemplate template = new InjectableTemplate(path, templateVariants, engine, renderedResults); if (injectedTemplates != null) { injectedTemplates.add(new WeakReference<>(template)); } @@ -142,8 +149,10 @@ static class InjectableTemplate implements Template { private final Engine engine; // Some methods may only work if a single template variant is found private final LazyValue