/* * Copyright 2016 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.deps; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.javascript.jscomp.ErrorManager; import com.google.javascript.jscomp.PrintStreamErrorManager; import com.google.javascript.jscomp.SourceFile; import java.util.ArrayList; import java.util.List; import junit.framework.TestCase; /** Tests for {@link DepsGenerator}. */ public final class DepsGeneratorTest extends TestCase { private static final Joiner LINE_JOINER = Joiner.on("\n"); private ErrorManager errorManager; @Override protected void setUp() throws Exception { errorManager = new PrintStreamErrorManager(System.err); } public void testEs6Modules() throws Exception { List<SourceFile> srcs = new ArrayList<>(); srcs.add(SourceFile.fromCode("/base/javascript/foo/foo.js", "import '../closure/goog/array';")); srcs.add(SourceFile.fromCode("/base/javascript/closure/goog/array.js", "export var array;")); DepsGenerator depsGenerator = new DepsGenerator( ImmutableList.<SourceFile>of(), srcs, DepsGenerator.InclusionStrategy.ALWAYS, "/base/javascript/closure", errorManager, new ModuleLoader( null, ImmutableList.of("/base/"), ImmutableList.<DependencyInfo>of(), ModuleLoader.PathResolver.ABSOLUTE, ModuleLoader.ResolutionMode.LEGACY)); String output = depsGenerator.computeDependencyCalls(); assertNoWarnings(); // Write the output. assertWithMessage("There should be output").that(output).isNotEmpty(); // Write the expected output. String expected = LINE_JOINER.join( "goog.addDependency('../foo/foo.js', ['module$javascript$foo$foo'], " + "['module$javascript$closure$goog$array'], {'lang': 'es6', 'module': 'es6'});", "goog.addDependency('goog/array.js', ['module$javascript$closure$goog$array'], " + "[], {'lang': 'es6', 'module': 'es6'});", ""); assertEquals(expected, output); } /** * Ensures that deps files are handled correctly both when listed as deps and when listed as * sources. */ public void testWithDepsAndSources() throws Exception { final SourceFile depsFile1 = SourceFile.fromCode( "/base/my-project/deps1.js", LINE_JOINER.join( "goog.addDependency('../prescanned1/file1.js', ['dep.string'], []);", "goog.addDependency('../prescanned1/file2.js', [], []);", // Test that this appears only once in the output. "goog.addDependency('../this/is/defined/thrice.js', [], []);", "goog.addDependency('../this/is/defined/thrice.js', [], []);", "")); final SourceFile depsFile2 = SourceFile.fromCode( "/base/my-project/deps2.js", LINE_JOINER.join( "goog.addDependency(" + "'../prescanned2/file1.js'," + " ['dep.bool', 'dep.number']," + " ['dep.string']);", "goog.addDependency('../prescanned2/file2.js', [], []);", "goog.addDependency('../this/is/defined/thrice.js', [], []);", "")); final SourceFile srcFile1 = SourceFile.fromCode( "/base/my-project/src1.js", LINE_JOINER.join( "goog.provide('makejsdeps.file1');", "goog.provide('makejsdeps.file1.Test');", "", // Ensure comments are stripped. These cause syntax errors if not stripped. "/*", "goog.require('failure1)", "*/", "// goog.require('failure2)", "", "goog.require('makejsdeps.file2');", // 'goog' should be silently dropped. "goog.require(\"goog\");", "goog.require(\"dep.string\");", "goog.require(\"dep.number\");", "")); final SourceFile srcFile2 = SourceFile.fromCode("/base/my-project/src2.js", "goog.provide('makejsdeps.file2');"); String expected = LINE_JOINER.join( "goog.addDependency(" + "'../../my-project/src1.js'," + " ['makejsdeps.file1', 'makejsdeps.file1.Test']," + " ['makejsdeps.file2', 'dep.string', 'dep.number']);", "goog.addDependency('../../my-project/src2.js', ['makejsdeps.file2'], []);", "", "// Included from: /base/my-project/deps1.js", "goog.addDependency('../prescanned1/file1.js', ['dep.string'], []);", "goog.addDependency('../prescanned1/file2.js', [], []);", "", "// Included from: /base/my-project/deps2.js", "goog.addDependency('../this/is/defined/thrice.js', [], []);", "goog.addDependency(" + "'../prescanned2/file1.js'," + " ['dep.bool', 'dep.number']," + " ['dep.string']);", "goog.addDependency('../prescanned2/file2.js', [], []);", ""); DepsGenerator depsGenerator = new DepsGenerator( ImmutableList.of(depsFile1, depsFile2), ImmutableList.of(srcFile1, srcFile2), DepsGenerator.InclusionStrategy.ALWAYS, "/base/javascript/closure", errorManager, new ModuleLoader( null, ImmutableList.of("/base/"), ImmutableList.<DependencyInfo>of(), ModuleLoader.PathResolver.ABSOLUTE, ModuleLoader.ResolutionMode.LEGACY)); String output = depsGenerator.computeDependencyCalls(); assertNoWarnings(); assertThat(output).isEqualTo(expected); // Repeat the test with the deps files listed as sources. // The only difference should be the addition of addDependency() calls for the deps files. depsGenerator = new DepsGenerator( ImmutableList.<SourceFile>of(), ImmutableList.of(depsFile1, depsFile2, srcFile1, srcFile2), DepsGenerator.InclusionStrategy.ALWAYS, "/base/javascript/closure", errorManager, new ModuleLoader( null, ImmutableList.of("/base/"), ImmutableList.<DependencyInfo>of(), ModuleLoader.PathResolver.ABSOLUTE, ModuleLoader.ResolutionMode.LEGACY)); String expectedWithDepsAsSources = LINE_JOINER.join( "goog.addDependency('../../my-project/deps1.js', [], []);", "goog.addDependency('../../my-project/deps2.js', [], []);", expected); output = depsGenerator.computeDependencyCalls(); assertNoWarnings(); assertThat(output).isEqualTo(expectedWithDepsAsSources); } /** * Ensures that everything still works when both a deps.js and a deps-runfiles.js file are * included. Also uses real files. */ public void testDepsAsSrcs() throws Exception { final SourceFile depsFile1 = SourceFile.fromCode( "/base/deps1.js", LINE_JOINER.join( "// Test deps file 1.", "", "goog.addDependency('../prescanned1/file1.js', ['dep.string'], []);", "goog.addDependency('../prescanned1/file2.js', [], []);", "// Test that this appears only once in the output. It's also defined in deps2.js", "goog.addDependency('../this/is/defined/thrice.js', [], []);", "goog.addDependency('../this/is/defined/thrice.js', [], []);", "")); final SourceFile depsFile2 = SourceFile.fromCode( "/base/deps2.js", LINE_JOINER.join( "// Test deps file 2.", "", "goog.addDependency(" + "'../prescanned2/file1.js', ['dep.bool', 'dep.number'], ['dep.string']);", "goog.addDependency('../prescanned2/file2.js', [], []);", "goog.addDependency('../prescanned2/generated.js', ['dep.generated'], []);", "goog.addDependency('../this/is/defined/thrice.js', [], []);", "")); DepsGenerator depsGenerator = new DepsGenerator( ImmutableList.of(depsFile1), ImmutableList.of(depsFile2), DepsGenerator.InclusionStrategy.ALWAYS, PathUtil.makeAbsolute("/base/javascript/closure"), errorManager, new ModuleLoader( null, ImmutableList.of("/base/" + "/"), ImmutableList.<DependencyInfo>of(), ModuleLoader.PathResolver.ABSOLUTE, ModuleLoader.ResolutionMode.LEGACY)); String output = depsGenerator.computeDependencyCalls(); assertWithMessage("There should be output").that(output).isNotEmpty(); assertNoWarnings(); } public void testMergeStrategyAlways() throws Exception { String result = testMergeStrategyHelper(DepsGenerator.InclusionStrategy.ALWAYS); assertContains("['a']", result); assertContains("['b']", result); assertContains("['c']", result); assertContains("d.js", result); } public void testMergeStrategyWhenInSrcs() throws Exception { String result = testMergeStrategyHelper(DepsGenerator.InclusionStrategy.WHEN_IN_SRCS); assertNotContains("['a']", result); assertContains("['b']", result); assertContains("['c']", result); assertNotContains("d.js", result); } public void testMergeStrategyDoNotDuplicate() throws Exception { String result = testMergeStrategyHelper(DepsGenerator.InclusionStrategy.DO_NOT_DUPLICATE); assertNotContains("['a']", result); assertNotContains("['b']", result); assertContains("['c']", result); assertNotContains("d.js", result); } private String testMergeStrategyHelper(DepsGenerator.InclusionStrategy mergeStrategy) throws Exception { SourceFile dep1 = SourceFile.fromCode( "dep1.js", LINE_JOINER.join( "goog.addDependency('../../a.js', ['a'], []);", "goog.addDependency('../../src1.js', ['b'], []);", "goog.addDependency('../../d.js', ['d'], []);\n")); SourceFile src1 = SourceFile.fromCode("/base/" + "/src1.js", "goog.provide('b');\n"); SourceFile src2 = SourceFile.fromCode( "/base/" + "/src2.js", LINE_JOINER.join("goog.provide('c');", "goog.require('d');")); DepsGenerator depsGenerator = new DepsGenerator( ImmutableList.of(dep1), ImmutableList.of(src1, src2), mergeStrategy, PathUtil.makeAbsolute("/base/javascript/closure"), errorManager, new ModuleLoader( null, ImmutableList.of("/base/"), ImmutableList.<DependencyInfo>of(), ModuleLoader.PathResolver.ABSOLUTE, ModuleLoader.ResolutionMode.LEGACY)); String output = depsGenerator.computeDependencyCalls(); assertWithMessage("There should be output files").that(output).isNotEmpty(); assertNoWarnings(); return output; } private void doErrorMessagesRun( List<SourceFile> deps, List<SourceFile> srcs, boolean fatal, String errorMessage) throws Exception { DepsGenerator depsGenerator = new DepsGenerator( deps, srcs, DepsGenerator.InclusionStrategy.ALWAYS, "/javascript/closure", errorManager, new ModuleLoader( null, ImmutableList.of(""), ImmutableList.<DependencyInfo>of(), ModuleLoader.PathResolver.ABSOLUTE, ModuleLoader.ResolutionMode.LEGACY)); String output = depsGenerator.computeDependencyCalls(); if (fatal) { assertWithMessage("No output should have been created.").that(output).isNull(); assertError(errorMessage); } else { assertWithMessage("Output should have been created.").that(output).isNotNull(); assertWarning(errorMessage); } } public void testDuplicateProvides() throws Exception { SourceFile dep1 = SourceFile.fromCode("dep1.js", "goog.addDependency('a.js', ['a'], []);\n"); SourceFile src1 = SourceFile.fromCode("src1.js", "goog.provide('a');\n"); doErrorMessagesRun(ImmutableList.of(dep1), ImmutableList.of(src1), true /* fatal */, "Namespace \"a\" is already provided in other file dep1.js"); } /** * Ensures that an error is thrown when the closure_path flag is set incorrectly. */ public void testDuplicateProvidesErrorThrownIfBadClosurePathSpecified() throws Exception { // Create a stub Closure Library. SourceFile fauxClosureDeps = SourceFile.fromCode("dep1.js", "goog.addDependency('foo/a.js', ['a'], []);\n"); SourceFile fauxClosureSrc = SourceFile.fromCode("path/to/closure/foo/a.js", "goog.provide('a');\n"); // Create a source file that depends on the stub Closure Library. SourceFile userSrc = SourceFile.fromCode("my/package/script.js", "goog.require('a');\n" + "goog.provide('my.package.script');\n"); // doErrorMessagesRun uses closure_path //javascript/closure and therefore // fails to recognize and de-dupe the stub Closure Library at // //path/to/closure. doErrorMessagesRun(ImmutableList.of(fauxClosureDeps), ImmutableList.of(fauxClosureSrc, userSrc), true /* fatal */, "Namespace \"a\" is already provided in other file dep1.js"); } /** Ensures that DepsGenerator deduplicates dependencies from custom Closure Library branches. */ public void testDuplicateProvidesIgnoredIfInClosureDirectory() throws Exception { // Create a stub Closure Library. SourceFile fauxClosureDeps = SourceFile.fromCode("dep1.js", "goog.addDependency('foo/a.js', ['a'], []);\n"); SourceFile fauxClosureSrc = SourceFile.fromCode("path/to/closure/foo/a.js", "goog.provide('a');\n"); // Create a source file that depends on the stub Closure Library. SourceFile userSrc = SourceFile.fromCode("my/package/script.js", "goog.require('a');\n" + "goog.provide('my.package.script');\n"); DepsGenerator worker = new DepsGenerator( ImmutableList.of(fauxClosureDeps), ImmutableList.of(fauxClosureSrc, userSrc), DepsGenerator.InclusionStrategy.ALWAYS, PathUtil.makeAbsolute("./path/to/closure"), errorManager, new ModuleLoader( null, ImmutableList.of("."), ImmutableList.<DependencyInfo>of(), ModuleLoader.PathResolver.ABSOLUTE, ModuleLoader.ResolutionMode.LEGACY)); String output = worker.computeDependencyCalls(); assertWithMessage("Output should have been created.").that(output).isNotEmpty(); assertNoWarnings(); } public void testDuplicateProvidesSameFile() throws Exception { SourceFile dep1 = SourceFile.fromCode("dep1.js", "goog.addDependency('a.js', ['a'], []);\n"); SourceFile src1 = SourceFile.fromCode( "src1.js", LINE_JOINER.join("goog.provide('b');", "goog.provide('b');\n")); doErrorMessagesRun(ImmutableList.of(dep1), ImmutableList.of(src1), false /* fatal */, "Multiple calls to goog.provide(\"b\")"); } public void testDuplicateRequire() throws Exception { SourceFile dep1 = SourceFile.fromCode("dep1.js", "goog.addDependency('a.js', ['a'], []);\n"); SourceFile src1 = SourceFile.fromCode( "src1.js", LINE_JOINER.join("goog.require('a');", "goog.require('a');", "")); doErrorMessagesRun(ImmutableList.of(dep1), ImmutableList.of(src1), false /* fatal */, "Namespace \"a\" is required multiple times"); } public void testSameFileProvideRequire() throws Exception { SourceFile dep1 = SourceFile.fromCode("dep1.js", "goog.addDependency('a.js', ['a'], []);\n"); SourceFile src1 = SourceFile.fromCode( "src1.js", LINE_JOINER.join("goog.provide('b');", "goog.require('b');", "")); doErrorMessagesRun(ImmutableList.of(dep1), ImmutableList.of(src1), false /* fatal */, "Namespace \"b\" is both required and provided in the same file."); } public void testUnknownNamespace() throws Exception { SourceFile dep1 = SourceFile.fromCode("dep1.js", "goog.addDependency('a.js', ['a'], []);\n"); SourceFile src1 = SourceFile.fromCode("src1.js", "goog.require('b');\n"); doErrorMessagesRun(ImmutableList.of(dep1), ImmutableList.of(src1), true /* fatal */, "Namespace \"b\" is required but never provided."); } public void testNoDepsInDepsFile() throws Exception { SourceFile dep1 = SourceFile.fromCode("dep1.js", ""); doErrorMessagesRun(ImmutableList.of(dep1), ImmutableList.<SourceFile>of(), false /* fatal */, "No dependencies found in file"); } private void assertErrorWarningCount(int errorCount, int warningCount) { if (errorManager.getErrorCount() != errorCount) { fail(String.format("Expected %d errors but got\n%s", errorCount, Joiner.on("\n").join(errorManager.getErrors()))); } if (errorManager.getWarningCount() != warningCount) { fail(String.format("Expected %d warnings but got\n%s", warningCount, Joiner.on("\n").join(errorManager.getWarnings()))); } } private void assertNoWarnings() { assertErrorWarningCount(0, 0); } private void assertWarning(String message) { assertErrorWarningCount(0, 1); assertThat(errorManager.getWarnings()[0].description).isEqualTo(message); } private void assertError(String message) { assertErrorWarningCount(1, 0); assertThat(errorManager.getErrors()[0].description).isEqualTo(message); } private static void assertContains(String part, String whole) { assertWithMessage("Expected string to contain: " + part).that(whole.contains(part)).isTrue(); } private static void assertNotContains(String part, String whole) { assertWithMessage("Expected string not to contain: " + part) .that(whole.contains(part)) .isFalse(); } }