From 0484df00c3b57f4c10cbab4e00bba4df0b7ade24 Mon Sep 17 00:00:00 2001 From: Alexey Tsvetkov Date: Mon, 14 Jul 2014 14:26:54 +0400 Subject: [PATCH] JS test: added test utils --- .../k2js/inline/util/collectUtils.kt | 30 +++ .../util/collectors/FunctionCollector.kt | 45 ++++ .../k2js/test/utils/AstSearchUtil.java | 39 ++++ .../k2js/test/utils/CallCounter.java | 87 +++++++ .../k2js/test/utils/InlineTestUtils.java | 220 ++++++++++++++++++ .../k2js/test/utils/JsTestUtils.java | 2 + 6 files changed, 423 insertions(+) create mode 100644 js/js.inliner/src/org/jetbrains/k2js/inline/util/collectUtils.kt create mode 100644 js/js.inliner/src/org/jetbrains/k2js/inline/util/collectors/FunctionCollector.kt create mode 100644 js/js.tests/test/org/jetbrains/k2js/test/utils/AstSearchUtil.java create mode 100644 js/js.tests/test/org/jetbrains/k2js/test/utils/CallCounter.java create mode 100644 js/js.tests/test/org/jetbrains/k2js/test/utils/InlineTestUtils.java diff --git a/js/js.inliner/src/org/jetbrains/k2js/inline/util/collectUtils.kt b/js/js.inliner/src/org/jetbrains/k2js/inline/util/collectUtils.kt new file mode 100644 index 00000000000..ccc49a2802f --- /dev/null +++ b/js/js.inliner/src/org/jetbrains/k2js/inline/util/collectUtils.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2010-2014 JetBrains s.r.o. + * + * 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 org.jetbrains.k2js.inline.util + +import com.google.dart.compiler.backend.js.ast.* + +import java.util.ArrayList +import java.util.IdentityHashMap +import org.jetbrains.k2js.inline.util.collectors.FunctionCollector + +public fun collectNamedFunctions(scope: JsNode): IdentityHashMap { + return with(FunctionCollector()) { + accept(scope) + functions + } +} \ No newline at end of file diff --git a/js/js.inliner/src/org/jetbrains/k2js/inline/util/collectors/FunctionCollector.kt b/js/js.inliner/src/org/jetbrains/k2js/inline/util/collectors/FunctionCollector.kt new file mode 100644 index 00000000000..101c77bec8b --- /dev/null +++ b/js/js.inliner/src/org/jetbrains/k2js/inline/util/collectors/FunctionCollector.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2010-2014 JetBrains s.r.o. + * + * 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 org.jetbrains.k2js.inline.util.collectors + +import com.google.dart.compiler.backend.js.ast.RecursiveJsVisitor +import com.google.dart.compiler.backend.js.ast.JsPropertyInitializer +import com.google.dart.compiler.backend.js.ast.JsNameRef +import com.google.dart.compiler.backend.js.ast.JsFunction +import com.google.dart.compiler.backend.js.ast.JsName + +import java.util.IdentityHashMap + +class FunctionCollector : RecursiveJsVisitor() { + public val functions: IdentityHashMap = IdentityHashMap() + + override fun visitPropertyInitializer(x: JsPropertyInitializer?) { + super.visitPropertyInitializer(x) + + val label = x?.getLabelExpr() + val value = x?.getValueExpr() + + if (label is JsNameRef && value is JsFunction) { + val name = (label as JsNameRef).getName() + val function = value as JsFunction + + if (name != null) { + functions[name] = function + } + } + } +} \ No newline at end of file diff --git a/js/js.tests/test/org/jetbrains/k2js/test/utils/AstSearchUtil.java b/js/js.tests/test/org/jetbrains/k2js/test/utils/AstSearchUtil.java new file mode 100644 index 00000000000..79a1d21dc48 --- /dev/null +++ b/js/js.tests/test/org/jetbrains/k2js/test/utils/AstSearchUtil.java @@ -0,0 +1,39 @@ +/* + * Copyright 2010-2014 JetBrains s.r.o. + * + * 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 org.jetbrains.k2js.test.utils; + +import com.google.dart.compiler.backend.js.ast.*; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + +import static org.jetbrains.k2js.inline.util.UtilPackage.collectNamedFunctions; + +public class AstSearchUtil { + @NotNull + public static JsFunction getFunction(@NotNull JsNode searchRoot, String name) { + Map functions = collectNamedFunctions(searchRoot); + + for (Map.Entry entry : functions.entrySet()) { + if (entry.getKey().getIdent().equals(name)) { + return entry.getValue(); + } + } + + throw new AssertionError("Function `" + name + "` was not found"); + } +} diff --git a/js/js.tests/test/org/jetbrains/k2js/test/utils/CallCounter.java b/js/js.tests/test/org/jetbrains/k2js/test/utils/CallCounter.java new file mode 100644 index 00000000000..3a13b4644ff --- /dev/null +++ b/js/js.tests/test/org/jetbrains/k2js/test/utils/CallCounter.java @@ -0,0 +1,87 @@ +/* + * Copyright 2010-2014 JetBrains s.r.o. + * + * 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 org.jetbrains.k2js.test.utils; + +import com.google.dart.compiler.backend.js.ast.*; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class CallCounter extends RecursiveJsVisitor { + + private final List callsNameRefs = new ArrayList(); + + @NotNull + public static CallCounter countCalls(@NotNull JsNode node) { + CallCounter visitor = new CallCounter(); + node.accept(visitor); + + return visitor; + } + + CallCounter() {} + + public int getTotalCallsCount() { + return callsNameRefs.size(); + } + + public int getQualifiedCallsCount(String... qualifiers) { + int count = 0; + List expectedQualifierChain = new ArrayList(); + Collections.addAll(expectedQualifierChain, qualifiers); + + for (JsNameRef callNameRef : callsNameRefs) { + if (matchesQualifiers(callNameRef, expectedQualifierChain)) { + count++; + } + } + + return count; + } + + @Override + public void visitInvocation(JsInvocation invocation) { + super.visitInvocation(invocation); + JsExpression qualifier = invocation.getQualifier(); + + if (qualifier instanceof JsNameRef) { + callsNameRefs.add((JsNameRef) qualifier); + } + } + + private static boolean matchesQualifiers(JsNameRef nameRef, List expectedQualifierChain) { + JsExpression currentQualifier = nameRef; + + for (String expectedQualifier : expectedQualifierChain) { + if (!(currentQualifier instanceof JsNameRef)) { + return false; + } + + JsNameRef currentNameRef = (JsNameRef) currentQualifier; + JsName name = currentNameRef.getName(); + if (name == null || !name.getIdent().equals(expectedQualifier)) { + return false; + } + + currentQualifier = currentNameRef.getQualifier(); + } + + return true; + } +} diff --git a/js/js.tests/test/org/jetbrains/k2js/test/utils/InlineTestUtils.java b/js/js.tests/test/org/jetbrains/k2js/test/utils/InlineTestUtils.java new file mode 100644 index 00000000000..3dd562874de --- /dev/null +++ b/js/js.tests/test/org/jetbrains/k2js/test/utils/InlineTestUtils.java @@ -0,0 +1,220 @@ +/* + * Copyright 2010-2014 JetBrains s.r.o. + * + * 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 org.jetbrains.k2js.test.utils; + +import com.google.dart.compiler.backend.js.ast.JsFunction; +import com.google.dart.compiler.backend.js.ast.JsNode; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +import static org.jetbrains.jet.InTextDirectivesUtils.findLinesWithPrefixesRemoved; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class InlineTestUtils { + + private InlineTestUtils() {} + + private static final DirectiveHandler FUNCTION_CONTAINS_NO_CALLS = new DirectiveHandler("CHECK_CONTAINS_NO_CALLS") { + @Override + void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception { + checkFunctionContainsNoCalls(ast, arguments.getFirst()); + } + }; + + private static final DirectiveHandler FUNCTION_NOT_CALLED = new DirectiveHandler("CHECK_NOT_CALLED") { + @Override + void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception { + checkFunctionNotCalled(ast, arguments.getFirst()); + } + }; + + private static final DirectiveHandler FUNCTION_CALLED_IN_SCOPE = new DirectiveHandler("CHECK_CALLED_IN_SCOPE") { + @Override + void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception { + checkCalledInScope(ast, arguments.getNamedArgument("function"), arguments.getNamedArgument("scope")); + } + }; + + private static final DirectiveHandler FUNCTION_NOT_CALLED_IN_SCOPE = new DirectiveHandler("CHECK_NOT_CALLED_IN_SCOPE") { + @Override + void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception { + checkNotCalledInScope(ast, arguments.getNamedArgument("function"), arguments.getNamedArgument("scope")); + } + }; + + private static final DirectiveHandler FUNCTIONS_HAVE_SAME_LINES = new DirectiveHandler("CHECK_FUNCTIONS_HAVE_SAME_LINES") { + @Override + void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception { + String code1 = getFunctionCode(ast, arguments.getPositionalArgument(0)); + String code2 = getFunctionCode(ast, arguments.getPositionalArgument(1)); + + String regexMatch = arguments.findNamedArgument("match"); + String regexReplace = arguments.findNamedArgument("replace"); + + code1 = applyRegex(code1, regexMatch, regexReplace); + code2 = applyRegex(code2, regexMatch, regexReplace); + + assertEquals(code1, code2); + } + + @NotNull + String getFunctionCode(@NotNull JsNode ast, @NotNull String functionName) { + JsFunction function = AstSearchUtil.getFunction(ast, functionName); + return function.toString(); + } + + @NotNull + String applyRegex(@NotNull String code, @Nullable String match, @Nullable String replace) { + if (match == null || replace == null) return code; + + return code.replaceAll(match, replace); + } + }; + + public static void processDirectives(@NotNull JsNode ast, @NotNull String sourceCode) throws Exception { + FUNCTION_CONTAINS_NO_CALLS.process(ast, sourceCode); + FUNCTION_NOT_CALLED.process(ast, sourceCode); + FUNCTION_CALLED_IN_SCOPE.process(ast, sourceCode); + FUNCTION_NOT_CALLED_IN_SCOPE.process(ast, sourceCode); + FUNCTIONS_HAVE_SAME_LINES.process(ast, sourceCode); + } + + public static void checkFunctionContainsNoCalls(JsNode node, String functionName) throws Exception { + JsFunction function = AstSearchUtil.getFunction(node, functionName); + CallCounter counter = CallCounter.countCalls(function); + int callsCount = counter.getTotalCallsCount(); + + String errorMessage = functionName + " contains calls"; + assertEquals(errorMessage, 0, callsCount); + } + + public static void checkFunctionNotCalled(JsNode node, String functionName) throws Exception { + CallCounter counter = CallCounter.countCalls(node); + int functionCalledCount = counter.getQualifiedCallsCount(functionName); + + String errorMessage = "inline function `" + functionName + "` is called"; + assertEquals(errorMessage, 0, functionCalledCount); + } + + public static void checkCalledInScope( + @NotNull JsNode node, + @NotNull String functionName, + @NotNull String scopeFunctionName + ) throws Exception { + String errorMessage = functionName + " is not called inside " + scopeFunctionName; + assertFalse(errorMessage, isCalledInScope(node, functionName, scopeFunctionName)); + } + + public static void checkNotCalledInScope( + @NotNull JsNode node, + @NotNull String functionName, + @NotNull String scopeFunctionName + ) throws Exception { + String errorMessage = functionName + " is called inside " + scopeFunctionName; + assertTrue(errorMessage, isCalledInScope(node, functionName, scopeFunctionName)); + } + + private static boolean isCalledInScope( + @NotNull JsNode node, + @NotNull String functionName, + @NotNull String scopeFunctionName + ) throws Exception { + JsNode scope = AstSearchUtil.getFunction(node, scopeFunctionName); + + CallCounter counter = CallCounter.countCalls(scope); + return counter.getQualifiedCallsCount(functionName) == 0; + } + + private abstract static class DirectiveHandler { + @NotNull private final String directive; + + DirectiveHandler(@NotNull String directive) { + this.directive = "// " + directive + ": "; + } + + /** + * Processes directive entries. + * + * Each entry is expected to have the following format: + * `// DIRECTIVE: arguments + * + * @see ArgumentsHelper for arguments format + */ + void process(@NotNull JsNode ast, @NotNull String sourceCode) throws Exception { + List directiveEntries = findLinesWithPrefixesRemoved(sourceCode, directive); + for (String directiveEntry : directiveEntries) { + processEntry(ast, new ArgumentsHelper(directiveEntry)); + } + } + + abstract void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception; + } + + /** + * Arguments format: ((namedArg|positionalArg)\s+)*` + * + * Where: namedArg -- "key=value" + * positionalArg -- "value" + * + * Neither key, nor value should contain spaces. + */ + private static class ArgumentsHelper { + private final List positionalArguments = new ArrayList(); + private final Map namedArguments = new HashMap(); + private final String entry; + + ArgumentsHelper(@NotNull String directiveEntry) { + entry = directiveEntry; + + for (String argument: directiveEntry.split("\\s+")) { + String[] keyVal = argument.split("="); + + switch (keyVal.length) { + case 1: positionalArguments.add(keyVal[0]); break; + case 2: namedArguments.put(keyVal[0], keyVal[1]); break; + default: throw new AssertionError("Wrong argument format: " + argument); + } + } + } + + @NotNull + String getFirst() { + return getPositionalArgument(0); + } + + @NotNull + String getPositionalArgument(int index) { + assert positionalArguments.size() > index: "Argument at index `" + index + "` not found in entry: " + entry; + return positionalArguments.get(index); + } + + @NotNull + String getNamedArgument(@NotNull String name) { + assert namedArguments.containsKey(name): "Argument `" + name + "` not found in entry: " + entry; + return namedArguments.get(name); + } + + @Nullable + String findNamedArgument(@NotNull String name) { + return namedArguments.get(name); + } + } +} diff --git a/js/js.tests/test/org/jetbrains/k2js/test/utils/JsTestUtils.java b/js/js.tests/test/org/jetbrains/k2js/test/utils/JsTestUtils.java index b2369c70ebe..3a222bd505a 100644 --- a/js/js.tests/test/org/jetbrains/k2js/test/utils/JsTestUtils.java +++ b/js/js.tests/test/org/jetbrains/k2js/test/utils/JsTestUtils.java @@ -16,6 +16,8 @@ package org.jetbrains.k2js.test.utils; +import com.google.dart.compiler.backend.js.ast.JsFunction; +import com.google.dart.compiler.backend.js.ast.JsNode; import com.intellij.openapi.util.io.FileUtil; import com.intellij.util.Function; import com.intellij.util.containers.ContainerUtil;