Skip to content

Commit

Permalink
Merge pull request #38954 from mkouba/qute-test-recording
Browse files Browse the repository at this point in the history
Qute: introduce RenderedResults assert API for tests
  • Loading branch information
mkouba authored Feb 26, 2024
2 parents e38cf8c + 07f6a3c commit 9f5c196
Show file tree
Hide file tree
Showing 14 changed files with 664 additions and 6 deletions.
6 changes: 6 additions & 0 deletions docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -874,6 +879,18 @@ void validateCheckedFragments(List<CheckedFragmentValidationBuildItem> 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()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
@@ -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> renderedResults;

@Test
public void testRenderedResultsNotRegistered() {
assertTrue(renderedResults.isUnsatisfied());
}

}
Original file line number Diff line number Diff line change
@@ -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("<h1>{name}{#fragment id=bar rendered=false}bar{/fragment}</h1>"),
"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", "<h1>oof</h1>");
}

@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", "<h1>oof</h1>");
}

@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<String> 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<RenderedResult> 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());
}

}
Original file line number Diff line number Diff line change
@@ -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();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

}
Original file line number Diff line number Diff line change
@@ -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;

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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<WeakReference<InjectableTemplate>> 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> renderedResults) {
this.engine = engine;
Map<String, TemplateVariants> templateVariants = new HashMap<>();
for (Entry<String, List<String>> entry : context.getVariants().entrySet()) {
Expand All @@ -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);
}
Expand Down Expand Up @@ -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));
}
Expand All @@ -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<Template> unambiguousTemplate;
private final RenderedResults renderedResults;

public InjectableTemplate(String path, Map<String, TemplateVariants> templateVariants, Engine engine) {
InjectableTemplate(String path, Map<String, TemplateVariants> templateVariants, Engine engine,
RenderedResults renderedResults) {
this.path = path;
this.variants = templateVariants.get(path);
this.engine = engine;
Expand All @@ -158,11 +167,13 @@ public Template get() {
} else {
unambiguousTemplate = null;
}
this.renderedResults = renderedResults;
}

@Override
public TemplateInstance instance() {
return new InjectableTemplateInstanceImpl();
TemplateInstance instance = new InjectableTemplateInstanceImpl();
return renderedResults != null ? new ResultsCollectingTemplateInstance(instance, renderedResults) : instance;
}

@Override
Expand Down Expand Up @@ -290,7 +301,8 @@ public Set<String> getFragmentIds() {

@Override
public TemplateInstance instance() {
return new InjectableFragmentTemplateInstanceImpl(identifier);
TemplateInstance instance = new InjectableFragmentTemplateInstanceImpl(identifier);
return renderedResults != null ? new ResultsCollectingTemplateInstance(instance, renderedResults) : instance;
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.quarkus.qute.runtime.test;

import io.quarkus.arc.BeanCreator;
import io.quarkus.arc.SyntheticCreationalContext;
import io.quarkus.qute.RenderedResults;

public class RenderedResultsCreator implements BeanCreator<RenderedResults> {

@Override
public RenderedResults create(SyntheticCreationalContext<RenderedResults> context) {
return new RenderedResults();
}

}
Loading

0 comments on commit 9f5c196

Please sign in to comment.