/* * Copyright 2010 The Closure Compiler Authors. * * 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.javascript.jscomp; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.javascript.jscomp.SourceFile.Generator; import com.google.javascript.rhino.Node; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.util.List; /** * Unit tests for {@link RecoverableJsAst}. * */ @RunWith(JUnit4.class) public class RecoverableJsAstTest { protected static final Joiner LINE_JOINER = Joiner.on('\n'); private String srcCode = ""; @Test public void testSimple() { setSourceCode("var a;"); SourceAst realAst = createRealAst(); // Note that here we do not test the caching of the tree between calls, // since that is the responsibility of the JsAst class. Here we just test // that proxying is successful and a copy is happening // Initial compile. RecoverableJsAst ast1 = new RecoverableJsAst(realAst, true); checkCompile(realAst, makeDefensiveCopy(ast1), "var a;\n"); // Change in the file-system. setSourceCode("var b;"); realAst.clearAst(); // The first RecoverableJsAst should continue to have the same value. checkCompile(realAst, makeDefensiveCopy(ast1), "var a;\n"); // A newly created one from the source should have a different value. RecoverableJsAst ast2 = new RecoverableJsAst(realAst, true); checkCompile(realAst, makeDefensiveCopy(ast2), "var b;\n"); // Clearing the first AST should also pick up the new changes. ast1.clearAst(); checkCompile(realAst, makeDefensiveCopy(ast1), "var b;\n"); } @Test public void testWarningReplay() { setSourceCode("var f() = a;"); SourceAst realAst = createRealAst(); // Note that here we do not test the caching of the tree between calls, // since that is the responsibility of the JsAst class. Here we just test // that proxying is successful and a copy is happening // Initial compile. RecoverableJsAst ast1 = new RecoverableJsAst(realAst, true); checkParseErrors(realAst, makeDefensiveCopy(ast1), "Parse error. Semi-colon expected"); // The first RecoverableJsAst should continue to have the same value. checkParseErrors(realAst, makeDefensiveCopy(ast1), "Parse error. Semi-colon expected"); } private RecoverableJsAst makeDefensiveCopy(SourceAst ast) { // NOTE: We reuse RecoverableJsAst as a way of making tree clones, because // compilation mutates the tree. This is unrelated to testing // RecoverableJsAst. return new RecoverableJsAst(ast, true); } private String getSourceCode() { return srcCode; } private void setSourceCode(String code) { srcCode = code; } private SourceAst createRealAst() { SourceFile file = SourceFile.fromGenerator("tests.js", new Generator() { @Override public String getCode() { return getSourceCode(); } }); return new JsAst(file); } private static CompilerOptions createCompilerOptions() { CompilerOptions options = new CompilerOptions(); options.setPrettyPrint(true); return options; } private void checkParseErrors(SourceAst realAst, RecoverableJsAst ast, String... expectedErrors) { checkCompile(realAst, ast, null, ImmutableList.copyOf(expectedErrors)); } private void checkCompile(SourceAst realAst, RecoverableJsAst ast, String expected) { checkCompile(realAst, ast, expected, ImmutableList.<String>of()); } private void checkCompile(SourceAst realAst, RecoverableJsAst ast, String expected, ImmutableList<String> expectedErrors) { Compiler compiler = new Compiler(); // Keep this a "small" test. Don't use threads. compiler.disableThreads(); JSModule module = new JSModule("m0"); module.add(new CompilerInput(ast)); compiler.compileModules(ImmutableList.<SourceFile>of(), ImmutableList.of(module), createCompilerOptions()); Node mainRoot = compiler.getRoot().getLastChild(); Node expectedRoot = null; if (expected != null) { expectedRoot = parseExpectedJs(ImmutableList.of( SourceFile.fromCode("expected.js", expected))); expectedRoot.detach(); } if (expectedRoot == null) { // We use null to signal a parse failure, which results in an empty sources root. assertTrue(mainRoot.isRoot() && !mainRoot.hasChildren()); } else { String explanation = expectedRoot.checkTreeEqualsIncludingJsDoc(mainRoot); if (explanation != null) { String expectedAsSource = compiler.toSource(expectedRoot); String mainAsSource = compiler.toSource(mainRoot); if (expectedAsSource.equals(mainAsSource)) { fail("In: " + expectedAsSource + "\n" + explanation); } else { fail("\nExpected: " + expectedAsSource + "\nResult: " + mainAsSource + "\n" + explanation); } } } JSError[] errors = compiler.getResult().errors; if (!expectedErrors.isEmpty()) { for (int i = 0; i < expectedErrors.size(); i++) { if (i < errors.length) { assertThat(errors[i].toString()).contains(expectedErrors.get(i)); } else { fail("missing error: " + expectedErrors.get(i)); } } } else { assertThat(errors).isEmpty(); } assertThat(ast.getAstRoot(compiler)).isNotSameAs(realAst.getAstRoot(compiler)); } /** * Parses expected JS inputs and returns the root of the parse tree. */ protected Node parseExpectedJs(List<SourceFile> inputs) { Compiler compiler = new Compiler(); compiler.init(ImmutableList.<SourceFile>of(), inputs, createCompilerOptions()); compiler.parse(); Node root = compiler.getRoot(); assertThat(root).isNotNull(); Node externsRoot = root.getFirstChild(); Node mainRoot = externsRoot.getNext(); return mainRoot; } }