JS test: added test utils

This commit is contained in:
Alexey Tsvetkov
2014-07-14 14:26:54 +04:00
committed by Zalim Bashorov
parent a26e77fd93
commit 0484df00c3
6 changed files with 423 additions and 0 deletions
@@ -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<JsName, JsFunction> {
return with(FunctionCollector()) {
accept(scope)
functions
}
}
@@ -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<JsName, JsFunction> = 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
}
}
}
}
@@ -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<JsName, JsFunction> functions = collectNamedFunctions(searchRoot);
for (Map.Entry<JsName, JsFunction> entry : functions.entrySet()) {
if (entry.getKey().getIdent().equals(name)) {
return entry.getValue();
}
}
throw new AssertionError("Function `" + name + "` was not found");
}
}
@@ -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<JsNameRef> callsNameRefs = new ArrayList<JsNameRef>();
@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<String> expectedQualifierChain = new ArrayList<String>();
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<String> 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;
}
}
@@ -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<String> 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<String> positionalArguments = new ArrayList<String>();
private final Map<String, String> namedArguments = new HashMap<String, String>();
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);
}
}
}
@@ -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;