/* * 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.pysrc.internal; import static com.google.common.truth.Truth.assertAbout; import static com.google.common.truth.Truth.assertThat; import com.google.common.collect.ImmutableMap; import com.google.common.truth.FailureStrategy; import com.google.common.truth.Subject; import com.google.common.truth.SubjectFactory; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Key; import com.google.template.soy.SoyFileSetParserBuilder; import com.google.template.soy.SoyModule; import com.google.template.soy.basetree.SyntaxVersion; import com.google.template.soy.error.ExplodingErrorReporter; import com.google.template.soy.internal.i18n.BidiGlobalDir; import com.google.template.soy.pysrc.SoyPySrcOptions; import com.google.template.soy.pysrc.internal.GenPyExprsVisitor.GenPyExprsVisitorFactory; import com.google.template.soy.pysrc.internal.PyApiCallScopeBindingAnnotations.PyCurrentManifest; import com.google.template.soy.shared.AutoEscapingType; import com.google.template.soy.shared.SharedTestUtils; import com.google.template.soy.shared.internal.ApiCallScopeUtils; import com.google.template.soy.shared.internal.GuiceSimpleScope; import com.google.template.soy.shared.restricted.ApiCallScopeBindingAnnotations.ApiCall; import com.google.template.soy.soytree.SoyFileSetNode; import com.google.template.soy.soytree.SoyNode; import java.util.List; /** * Truth assertion which compiles the provided soy code and asserts that the generated Python code * matches the expected output. * */ public final class SoyCodeForPySubject extends Subject<SoyCodeForPySubject, String> { private static final String RUNTIME_PATH = "example.runtime"; private String bidiIsRtlFn = ""; private String environmentModulePath = ""; private String translationClass = ""; private ImmutableMap<String, String> namespaceManifest = ImmutableMap.<String, String>of(); private boolean isFile; private final Injector injector; /** * A Subject for testing sections of Soy code. The provided data can either be an entire Soy file, * or just the body of a template. If just a body is provided, it is wrapped with a simple * template before compiling. * * @param failureStrategy The environment provided FailureStrategy. * @param code The input Soy code to be compiled and tested. * @param isFile Whether the provided code represents a full file. */ SoyCodeForPySubject(FailureStrategy failureStrategy, String code, boolean isFile) { super(failureStrategy, code); this.isFile = isFile; this.injector = Guice.createInjector(new SoyModule()); } public SoyCodeForPySubject withEnvironmentModule(String environmentModulePath) { this.environmentModulePath = environmentModulePath; return this; } public SoyCodeForPySubject withBidi(String bidiIsRtlFn) { this.bidiIsRtlFn = bidiIsRtlFn; return this; } public SoyCodeForPySubject withTranslationClass(String translationClass) { this.translationClass = translationClass; return this; } public SoyCodeForPySubject withNamespaceManifest(ImmutableMap<String, String> namespaceManifest) { this.namespaceManifest = namespaceManifest; return this; } /** * Asserts that the subject compiles to the expected Python output. * * <p>During compilation, freestanding bodies are compiled as strict templates with the output * variable already being initialized. Additionally, any automatically generated variables have * generated IDs replaced with '###'. Thus 'name123' would become 'name###'. * * @param expectedPyOutput The expected Python result of compilation. */ public void compilesTo(String expectedPyOutput) { if (isFile) { assertThat(compileFile()).isEqualTo(expectedPyOutput); } else { assertThat(compileBody()).isEqualTo(expectedPyOutput); } } /** * Asserts that the subject compiles to Python output which contains the expected output. * * <p>Compilation follows the same rules as {@link #compilesTo}. * * @param expectedPyOutput The expected Python result of compilation. */ public void compilesToSourceContaining(String expectedPyOutput) { if (isFile) { assertThat(compileFile()).contains(expectedPyOutput); } else { assertThat(compileBody()).contains(expectedPyOutput); } } /** * Asserts that the subject compilation throws the expected exception. * * <p>Compilation follows the same rules as {@link #compilesTo}. * * @param expectedClass The class of the expected exception. */ public void compilesWithException(Class<? extends Exception> expectedClass) { try { if (isFile) { compileFile(); } else { compileBody(); } fail("Compilation suceeded when it should have failed."); } catch (Exception actual) { assertThat(actual).isInstanceOf(expectedClass); } } private GuiceSimpleScope.InScope enterScope() { // Setup default configs. SoyPySrcOptions pySrcOptions = new SoyPySrcOptions( RUNTIME_PATH, environmentModulePath, bidiIsRtlFn, translationClass, namespaceManifest, false); GuiceSimpleScope apiCallScope1 = injector.getInstance(Key.get(GuiceSimpleScope.class, ApiCall.class)); GuiceSimpleScope.InScope scope = apiCallScope1.enter(); ApiCallScopeUtils.seedSharedParams(scope, null, BidiGlobalDir.LTR); scope.seed(SoyPySrcOptions.class, pySrcOptions); scope.seed( new Key<ImmutableMap<String, String>>(PyCurrentManifest.class) {}, ImmutableMap.<String, String>of()); return scope; } private String compileFile() { SoyFileSetNode node = SoyFileSetParserBuilder.forFileContents(actual()).parse().fileSet(); try (GuiceSimpleScope.InScope inScope = enterScope()) { List<String> fileContents = injector.getInstance(GenPyCodeVisitor.class).gen(node, ExplodingErrorReporter.get()); return fileContents.get(0).replaceAll("([a-zA-Z]+)\\d+", "$1###"); } } private String compileBody() { SoyNode node = SharedTestUtils.getNode( SoyFileSetParserBuilder.forTemplateContents(AutoEscapingType.STRICT, actual()) .declaredSyntaxVersion(SyntaxVersion.V2_0) .parse() .fileSet(), 0); // Setup the GenPyCodeVisitor's state before the node is visited. try (GuiceSimpleScope.InScope inScope = enterScope()) { GenPyCodeVisitor genPyCodeVisitor = injector.getInstance(GenPyCodeVisitor.class); genPyCodeVisitor.pyCodeBuilder = new PyCodeBuilder(); genPyCodeVisitor.pyCodeBuilder.pushOutputVar("output"); genPyCodeVisitor.pyCodeBuilder.setOutputVarInited(); genPyCodeVisitor.localVarExprs = new LocalVariableStack(); genPyCodeVisitor.localVarExprs.pushFrame(); genPyCodeVisitor.genPyExprsVisitor = injector .getInstance(GenPyExprsVisitorFactory.class) .create(genPyCodeVisitor.localVarExprs, ExplodingErrorReporter.get()); genPyCodeVisitor.visitForTesting(node, ExplodingErrorReporter.get()); return genPyCodeVisitor.pyCodeBuilder.getCode().replaceAll("([a-zA-Z]+)\\d+", "$1###"); } } //----------------------------------------------------------------------------------------------- // Public static functions for starting a SoyCodeForPySubject test. private static final SubjectFactory<SoyCodeForPySubject, String> SOYCODE = new SubjectFactory<SoyCodeForPySubject, String>() { @Override public SoyCodeForPySubject getSubject(FailureStrategy failureStrategy, String code) { return new SoyCodeForPySubject(failureStrategy, code, false); } }; private static final SubjectFactory<SoyCodeForPySubject, String> SOYFILE = new SubjectFactory<SoyCodeForPySubject, String>() { @Override public SoyCodeForPySubject getSubject(FailureStrategy failureStrategy, String file) { return new SoyCodeForPySubject(failureStrategy, file, true); } }; public static SoyCodeForPySubject assertThatSoyCode(String code) { return assertAbout(SOYCODE).that(code); } public static SoyCodeForPySubject assertThatSoyFile(String file) { return assertAbout(SOYFILE).that(file); } }