/* * Copyright 2015 Google Inc. * * 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 * * http://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 com.google.template.soy.jbcsrc; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.template.soy.data.SoyValueConverter.EMPTY_DICT; import com.google.common.base.Joiner; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.common.truth.FailureStrategy; import com.google.common.truth.Subject; import com.google.common.truth.SubjectFactory; import com.google.common.truth.Truth; import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Provider; import com.google.inject.Provides; import com.google.template.soy.SoyFileSetParserBuilder; import com.google.template.soy.SoyModule; import com.google.template.soy.data.SoyRecord; import com.google.template.soy.data.SoyValueConverter; import com.google.template.soy.error.ExplodingErrorReporter; import com.google.template.soy.exprtree.FunctionNode; import com.google.template.soy.jbcsrc.api.AdvisingStringBuilder; import com.google.template.soy.jbcsrc.api.RenderResult; import com.google.template.soy.jbcsrc.shared.CompiledTemplate; import com.google.template.soy.jbcsrc.shared.CompiledTemplates; import com.google.template.soy.jbcsrc.shared.RenderContext; import com.google.template.soy.msgs.SoyMsgBundle; import com.google.template.soy.shared.SoyCssRenamingMap; import com.google.template.soy.shared.SoyGeneralOptions; import com.google.template.soy.shared.SoyIdRenamingMap; import com.google.template.soy.shared.restricted.SoyFunction; import com.google.template.soy.shared.restricted.SoyJavaFunction; import com.google.template.soy.shared.restricted.SoyJavaPrintDirective; import com.google.template.soy.soytree.SoyFileSetNode; import com.google.template.soy.soytree.SoyTreeUtils; import com.google.template.soy.soytree.TemplateRegistry; import com.google.template.soy.types.SoyTypeRegistry; import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** Utilities for testing compiled soy templates. */ public final class TemplateTester { private static final Injector INJECTOR = Guice.createInjector( new SoyModule(), new AbstractModule() { @Provides RenderContext.Builder provideContext( ImmutableMap<String, ? extends SoyFunction> functions, SoyValueConverter converter, ImmutableMap<String, ? extends SoyJavaPrintDirective> printDirectives) { @SuppressWarnings("unchecked") ImmutableMap<String, SoyJavaFunction> soyJavaFunctions = ImmutableMap.copyOf( (Map<String, SoyJavaFunction>) Maps.filterValues( functions, Predicates.instanceOf(SoyJavaFunction.class))); return new RenderContext.Builder() .withSoyFunctions(soyJavaFunctions) .withSoyPrintDirectives(printDirectives) .withConverter(converter); } @Override protected void configure() {} }); static final Provider<RenderContext.Builder> DEFAULT_CONTEXT_BUILDER = INJECTOR.getProvider(RenderContext.Builder.class); static RenderContext getDefaultContext(CompiledTemplates templates) { return getDefaultContext(templates, Predicates.<String>alwaysFalse()); } static RenderContext getDefaultContext( CompiledTemplates templates, Predicate<String> activeDelPackages) { return DEFAULT_CONTEXT_BUILDER .get() .withActiveDelPackageSelector(activeDelPackages) .withCompiledTemplates(templates) .build(); } private static final SubjectFactory<CompiledTemplateSubject, String> FACTORY = new SubjectFactory<CompiledTemplateSubject, String>() { @Override public CompiledTemplateSubject getSubject(FailureStrategy fs, String that) { return new CompiledTemplateSubject(fs, that); } }; /** * Returns a truth subject that can be used to assert on an template given the template body. * * <p>The given body lines are wrapped in a template called {@code ns.foo} that has no params. */ public static CompiledTemplateSubject assertThatTemplateBody(String... body) { String template = toTemplate(body); return assertThatFile(template); } static CompiledTemplateSubject assertThatFile(String... template) { return Truth.assertAbout(FACTORY).that(Joiner.on('\n').join(template)); } /** * Returns a {@link com.google.template.soy.jbcsrc.shared.CompiledTemplates} for the given * template body. Containing a single template {@code ns.foo} with the given body */ public static CompiledTemplates compileTemplateBody(String... body) { return compileFile(toTemplate(body)); } static SoyRecord asRecord(Map<String, ?> params) { return (SoyRecord) SoyValueConverter.UNCUSTOMIZED_INSTANCE.convert(params); } static final class CompiledTemplateSubject extends Subject<CompiledTemplateSubject, String> { private final List<SoyFunction> soyFunctions = new ArrayList<>(); private final RenderContext.Builder defaultContextBuilder = DEFAULT_CONTEXT_BUILDER.get(); private Iterable<ClassData> classData; private CompiledTemplate.Factory factory; private SoyTypeRegistry typeRegistry = new SoyTypeRegistry(); private SoyValueConverter converter = SoyValueConverter.UNCUSTOMIZED_INSTANCE; private SoyGeneralOptions generalOptions = new SoyGeneralOptions(); private RenderContext defaultContext; private CompiledTemplateSubject(FailureStrategy failureStrategy, String subject) { super(failureStrategy, subject); } CompiledTemplateSubject withTypeRegistry(SoyTypeRegistry typeRegistry) { classData = null; factory = null; this.typeRegistry = typeRegistry; return this; } CompiledTemplateSubject withValueConverter(SoyValueConverter converter) { classData = null; factory = null; this.converter = converter; defaultContextBuilder.withConverter(converter); return this; } CompiledTemplateSubject withSoyFunction(SoyFunction soyFunction) { classData = null; factory = null; this.soyFunctions.add(checkNotNull(soyFunction)); return this; } CompiledTemplateSubject withGeneralOptions(SoyGeneralOptions options) { this.generalOptions = options; return this; } CompiledTemplateSubject withCssRenamingMap(SoyCssRenamingMap renamingMap) { this.defaultContextBuilder.withCssRenamingMap(renamingMap); return this; } CompiledTemplateSubject withXidRenamingMap(SoyIdRenamingMap renamingMap) { this.defaultContextBuilder.withXidRenamingMap(renamingMap); return this; } CompiledTemplateSubject logsOutput(String expected) { compile(); return rendersAndLogs("", expected, EMPTY_DICT, EMPTY_DICT, defaultContext); } CompiledTemplateSubject rendersAs(String expected) { compile(); return rendersAndLogs(expected, "", EMPTY_DICT, EMPTY_DICT, defaultContext); } CompiledTemplateSubject rendersAs(String expected, Map<String, ?> params) { compile(); return rendersAndLogs(expected, "", asRecord(params), EMPTY_DICT, defaultContext); } CompiledTemplateSubject rendersAs(String expected, Map<String, ?> params, Map<String, ?> ij) { compile(); return rendersAndLogs(expected, "", asRecord(params), asRecord(ij), defaultContext); } CompiledTemplateSubject failsToRenderWith(Class<? extends Throwable> expected) { return failsToRenderWith(expected, ImmutableMap.<String, Object>of()); } CompiledTemplateSubject failsToRenderWith( Class<? extends Throwable> expected, Map<String, ?> params) { AdvisingStringBuilder builder = new AdvisingStringBuilder(); compile(); try { factory.create(asRecord(params), EMPTY_DICT).render(builder, defaultContext); failureStrategy.fail( String.format( "Expected %s to fail to render with a %s, but it rendered '%s'", actual(), expected, "")); } catch (Throwable t) { if (!expected.isInstance(t)) { failWithBadResults("failsToRenderWith", expected, "failed with", t); } } return this; // may be dead } private SoyRecord asRecord(Map<String, ?> params) { return (SoyRecord) converter.convert(params); } private CompiledTemplateSubject rendersAndLogs( String expectedOutput, String expectedLogged, SoyRecord params, SoyRecord ij, RenderContext context) { CompiledTemplate template = factory.create(params, ij); AdvisingStringBuilder builder = new AdvisingStringBuilder(); LogCapturer logOutput = new LogCapturer(); RenderResult result; try (SystemOutRestorer restorer = logOutput.enter()) { result = template.render(builder, context); } catch (Throwable e) { failureStrategy.fail(String.format("Unexpected failure for %s", getDisplaySubject()), e); result = null; } if (result.type() != RenderResult.Type.DONE) { fail("renders to completion", result); } String output = builder.toString(); if (!output.equals(expectedOutput)) { failWithBadResults("renders as", expectedOutput, "renders as", output); } if (!expectedLogged.equals(logOutput.toString())) { failWithBadResults("logs", expectedLogged, "logs", logOutput.toString()); } return this; } @Override protected String getDisplaySubject() { if (classData == null) { // hasn't been compiled yet. just use the source text return actual(); } String customName = super.internalCustomName(); return (customName != null ? customName : "") + " (<\n" + actual() + "\n Compiled as: \n" + Joiner.on('\n').join(classData) + "\n>)"; } private void compile() { if (classData == null) { SoyFileSetParserBuilder builder = SoyFileSetParserBuilder.forFileContents(actual()); for (SoyFunction function : soyFunctions) { builder.addSoyFunction(function); } SoyFileSetNode fileSet = builder .typeRegistry(typeRegistry) .options(generalOptions) .errorReporter(ExplodingErrorReporter.get()) .parse() .fileSet(); new UnsupportedFeatureReporter(ExplodingErrorReporter.get()).check(fileSet); // Clone the tree, there tend to be bugs in the AST clone implementations that don't show // up until development time when we do a lot of AST cloning, so clone here to try to flush // them out. fileSet = SoyTreeUtils.cloneNode(fileSet); Map<String, SoyJavaFunction> functions = new LinkedHashMap<>(); for (FunctionNode fnNode : SoyTreeUtils.getAllNodesOfType(fileSet, FunctionNode.class)) { if (fnNode.getSoyFunction() instanceof SoyJavaFunction) { functions.put(fnNode.getFunctionName(), (SoyJavaFunction) fnNode.getSoyFunction()); } } // N.B. we are reproducing some of BytecodeCompiler here to make it easier to look at // intermediate data structures. TemplateRegistry registry = new TemplateRegistry(fileSet, ExplodingErrorReporter.get()); CompiledTemplateRegistry compilerRegistry = new CompiledTemplateRegistry(registry); String templateName = Iterables.getOnlyElement(registry.getBasicTemplatesMap().keySet()); classData = new TemplateCompiler( compilerRegistry, compilerRegistry.getTemplateInfoByTemplateName(templateName)) .compile(); checkClasses(classData); CompiledTemplates compiledTemplates = new CompiledTemplates( compilerRegistry.getDelegateTemplateNames(), new MemoryClassLoader(classData)); factory = compiledTemplates.getTemplateFactory(templateName); defaultContext = defaultContextBuilder .withCompiledTemplates(compiledTemplates) .withSoyFunctions(ImmutableMap.copyOf(functions)) .withMessageBundle(SoyMsgBundle.EMPTY) .build(); } } private static void checkClasses(Iterable<ClassData> classData2) { for (ClassData d : classData2) { d.checkClass(); } } } private interface SystemOutRestorer extends AutoCloseable { @Override public void close(); } private static final class LogCapturer { private final ByteArrayOutputStream logOutput; private final PrintStream stream; LogCapturer() { this.logOutput = new ByteArrayOutputStream(); try { this.stream = new PrintStream(logOutput, true, StandardCharsets.UTF_8.name()); } catch (UnsupportedEncodingException e) { throw new AssertionError("StandardCharsets must be supported", e); } } SystemOutRestorer enter() { final PrintStream prevStream = System.out; System.setOut(stream); return new SystemOutRestorer() { @Override public void close() { System.setOut(prevStream); } }; } @Override public String toString() { return new String(logOutput.toByteArray(), StandardCharsets.UTF_8); } } private static String toTemplate(String... body) { StringBuilder builder = new StringBuilder(); builder.append("{namespace ns autoescape=\"strict\"}\n").append("{template .foo}\n"); Joiner.on("\n").appendTo(builder, body); builder.append("\n{/template}\n"); return builder.toString(); } static CompiledTemplates compileFile(String... fileBody) { String file = Joiner.on('\n').join(fileBody); return BytecodeCompiler.compile( SoyFileSetParserBuilder.forFileContents(file).parse().registry(), false, ExplodingErrorReporter.get()) .get(); } }