Adds test infrastructure for JS optimizer. Adds simple tests
This commit is contained in:
@@ -17,6 +17,7 @@ package com.google.gwt.dev.js;
|
|||||||
|
|
||||||
import com.google.dart.compiler.backend.js.ast.*;
|
import com.google.dart.compiler.backend.js.ast.*;
|
||||||
import com.google.dart.compiler.backend.js.ast.JsLiteral.JsBooleanLiteral;
|
import com.google.dart.compiler.backend.js.ast.JsLiteral.JsBooleanLiteral;
|
||||||
|
import com.google.dart.compiler.backend.js.ast.metadata.HasMetadata;
|
||||||
import com.google.gwt.dev.js.parserExceptions.JsParserException;
|
import com.google.gwt.dev.js.parserExceptions.JsParserException;
|
||||||
import com.google.gwt.dev.js.rhino.*;
|
import com.google.gwt.dev.js.rhino.*;
|
||||||
import com.intellij.util.SmartList;
|
import com.intellij.util.SmartList;
|
||||||
@@ -460,6 +461,12 @@ public class JsAstMapper {
|
|||||||
|
|
||||||
private JsExpression mapExpression(Node exprNode) throws JsParserException {
|
private JsExpression mapExpression(Node exprNode) throws JsParserException {
|
||||||
JsNode unknown = map(exprNode);
|
JsNode unknown = map(exprNode);
|
||||||
|
|
||||||
|
if (unknown instanceof HasMetadata) {
|
||||||
|
HasMetadata metadataContainer = (HasMetadata) unknown;
|
||||||
|
metadataContainer.setData("line", exprNode.getLineno());
|
||||||
|
}
|
||||||
|
|
||||||
if (unknown instanceof JsExpression) {
|
if (unknown instanceof JsExpression) {
|
||||||
return (JsExpression) unknown;
|
return (JsExpression) unknown;
|
||||||
}
|
}
|
||||||
@@ -878,6 +885,12 @@ public class JsAstMapper {
|
|||||||
|
|
||||||
private JsStatement mapStatement(Node nodeStmt) throws JsParserException {
|
private JsStatement mapStatement(Node nodeStmt) throws JsParserException {
|
||||||
JsNode unknown = map(nodeStmt);
|
JsNode unknown = map(nodeStmt);
|
||||||
|
|
||||||
|
if (unknown instanceof HasMetadata) {
|
||||||
|
HasMetadata metadataContainer = (HasMetadata) unknown;
|
||||||
|
metadataContainer.setData("line", nodeStmt.getLineno());
|
||||||
|
}
|
||||||
|
|
||||||
if (unknown != null) {
|
if (unknown != null) {
|
||||||
if (unknown instanceof JsStatement) {
|
if (unknown instanceof JsStatement) {
|
||||||
return (JsStatement) unknown;
|
return (JsStatement) unknown;
|
||||||
|
|||||||
@@ -328,8 +328,8 @@ public class IRFactory {
|
|||||||
int lineno)
|
int lineno)
|
||||||
{
|
{
|
||||||
if (ifFalse == null)
|
if (ifFalse == null)
|
||||||
return new Node(TokenStream.IF, (Node)cond, (Node)ifTrue);
|
return new Node(TokenStream.IF, (Node)cond, (Node)ifTrue, lineno);
|
||||||
return new Node(TokenStream.IF, (Node)cond, (Node)ifTrue, (Node)ifFalse);
|
return new Node(TokenStream.IF, (Node)cond, (Node)ifTrue, (Node)ifFalse, lineno);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Object createTernary(Object cond, Object ifTrue, Object ifFalse) {
|
public Object createTernary(Object cond, Object ifTrue, Object ifFalse) {
|
||||||
|
|||||||
@@ -416,6 +416,7 @@ public class Node implements Cloneable {
|
|||||||
case TokenStream.BREAK:
|
case TokenStream.BREAK:
|
||||||
case TokenStream.CONTINUE:
|
case TokenStream.CONTINUE:
|
||||||
case TokenStream.WITH:
|
case TokenStream.WITH:
|
||||||
|
case TokenStream.IF:
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2016 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.kotlin.js.test.optimizer
|
||||||
|
|
||||||
|
|
||||||
|
import com.google.dart.compiler.backend.js.ast.*
|
||||||
|
import com.google.dart.compiler.backend.js.ast.metadata.synthetic
|
||||||
|
import com.google.dart.compiler.util.TextOutputImpl
|
||||||
|
import com.google.gwt.dev.js.rhino.CodePosition
|
||||||
|
import com.google.gwt.dev.js.rhino.ErrorReporter
|
||||||
|
import com.intellij.openapi.util.io.FileUtil
|
||||||
|
import org.jetbrains.kotlin.js.inline.clean.FunctionPostProcessor
|
||||||
|
import org.jetbrains.kotlin.js.parser.parse
|
||||||
|
import org.jetbrains.kotlin.js.sourceMap.JsSourceGenerationVisitor
|
||||||
|
import org.jetbrains.kotlin.js.test.BasicTest
|
||||||
|
import org.jetbrains.kotlin.js.translate.utils.JsAstUtils
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.rules.TestName
|
||||||
|
import org.mozilla.javascript.Context
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
abstract class BasicOptimizerTest(private var basePath: String) {
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
var testName = TestName()
|
||||||
|
|
||||||
|
protected fun box() {
|
||||||
|
val methodName = testName.methodName
|
||||||
|
val baseName = "${BasicTest.TEST_DATA_DIR_PATH}/js-optimizer/$basePath"
|
||||||
|
val unoptimizedName = "$baseName/$methodName.original.js"
|
||||||
|
val optimizedName = "$baseName/$methodName.optimized.js"
|
||||||
|
|
||||||
|
val unoptimizedCode = FileUtil.loadFile(File(unoptimizedName))
|
||||||
|
val optimizedCode = FileUtil.loadFile(File(optimizedName))
|
||||||
|
|
||||||
|
runFiles(unoptimizedName, optimizedName, unoptimizedCode, optimizedCode)
|
||||||
|
checkOptimizer(unoptimizedCode, optimizedCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runFiles(unoptimizedName: String, optimizedName: String, unoptimizedCode: String, optimizedCode: String) {
|
||||||
|
try {
|
||||||
|
val ctx = Context.enter()
|
||||||
|
runScript(ctx, unoptimizedName, unoptimizedCode)
|
||||||
|
runScript(ctx, optimizedName, optimizedCode)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Context.exit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkOptimizer(unoptimizedCode: String, optimizedCode: String) {
|
||||||
|
val parserScope = JsFunctionScope(JsRootScope(JsProgram("<js checker>")), "<js fun>")
|
||||||
|
val unoptimizedAst = parse(unoptimizedCode, errorReporter, parserScope)
|
||||||
|
|
||||||
|
updateMetadata(unoptimizedCode, unoptimizedAst)
|
||||||
|
|
||||||
|
for (statement in unoptimizedAst) {
|
||||||
|
object : RecursiveJsVisitor() {
|
||||||
|
override fun visitFunction(x: JsFunction) {
|
||||||
|
FunctionPostProcessor(x.body).apply()
|
||||||
|
super.visitFunction(x)
|
||||||
|
}
|
||||||
|
}.accept(statement)
|
||||||
|
}
|
||||||
|
|
||||||
|
val optimizedAst = parse(optimizedCode, errorReporter, parserScope)
|
||||||
|
Assert.assertEquals(astToString(optimizedAst), astToString(unoptimizedAst))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateMetadata(code: String, ast: List<JsStatement>) {
|
||||||
|
val comments = findSyntheticComments(code)
|
||||||
|
|
||||||
|
for (stmt in ast) {
|
||||||
|
object : RecursiveJsVisitor() {
|
||||||
|
override fun visitVars(x: JsVars) {
|
||||||
|
x.synthetic = x.vars.any { isSyntheticId(it.name.ident) }
|
||||||
|
super.visitVars(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun visitExpressionStatement(x: JsExpressionStatement) {
|
||||||
|
val assignment = JsAstUtils.decomposeAssignmentToVariable(x.expression)
|
||||||
|
if (assignment != null) {
|
||||||
|
val name = assignment.first
|
||||||
|
x.synthetic = isSyntheticId(name.ident)
|
||||||
|
}
|
||||||
|
super.visitExpressionStatement(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun visitIf(x: JsIf) {
|
||||||
|
val line = x.getData<Int?>("line")
|
||||||
|
if (line != null && line in comments.indices && comments[line]) {
|
||||||
|
x.synthetic = true
|
||||||
|
}
|
||||||
|
super.visitIf(x)
|
||||||
|
}
|
||||||
|
}.accept(stmt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findSyntheticComments(code: String): List<Boolean> {
|
||||||
|
val parts = code.lines()
|
||||||
|
return parts.map { it.contains("/*synthetic*/") }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isSyntheticId(id: String) = id.startsWith("$")
|
||||||
|
|
||||||
|
|
||||||
|
private fun astToString(ast: List<JsStatement>): String {
|
||||||
|
val output = TextOutputImpl()
|
||||||
|
val visitor = JsSourceGenerationVisitor(output, null)
|
||||||
|
for (stmt in ast) {
|
||||||
|
stmt.accept(visitor)
|
||||||
|
}
|
||||||
|
return output.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runScript(ctx: Context, fileName: String, code: String) {
|
||||||
|
val scope = ctx.initStandardObjects()
|
||||||
|
ctx.evaluateString(scope, code, fileName, 1, null)
|
||||||
|
val result = ctx.evaluateString(scope, "box()", "unit test", 1, null)
|
||||||
|
|
||||||
|
Assert.assertEquals("box() function must return 'OK'", "OK", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val errorReporter = object : ErrorReporter {
|
||||||
|
override fun warning(message: String, startPosition: CodePosition, endPosition: CodePosition) { }
|
||||||
|
|
||||||
|
override fun error(message: String, startPosition: CodePosition, endPosition: CodePosition) {
|
||||||
|
Assert.fail("Error parsing JS file: $message at $startPosition")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2016 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.kotlin.js.test.optimizer
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class IfStatementReductionTest : BasicOptimizerTest("if-reduction") {
|
||||||
|
@Test fun assignment() = box()
|
||||||
|
|
||||||
|
@Test fun returnStatement() = box()
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2016 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.kotlin.js.test.optimizer
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class TemporaryAssignmentEliminationTest : BasicOptimizerTest("temporary-assignment") {
|
||||||
|
@Test fun assignment() = box()
|
||||||
|
|
||||||
|
@Test fun returnStatement() = box()
|
||||||
|
|
||||||
|
@Test fun declaration() = box()
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2016 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.kotlin.js.test.optimizer
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class TemporaryVariableEliminationTest : BasicOptimizerTest("temporary-variable") {
|
||||||
|
@Test fun declaration() = box()
|
||||||
|
|
||||||
|
@Test fun assignment() = box()
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
function test(n) {
|
||||||
|
var result;
|
||||||
|
result = n >= 0 ? n : -n;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function box() {
|
||||||
|
var result = test(20);
|
||||||
|
if (result != 20) return "fail1: " + result;
|
||||||
|
|
||||||
|
result = test(-20);
|
||||||
|
if (result != 20) return "fail2: " + result;
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
function test(n) {
|
||||||
|
var result;
|
||||||
|
/*synthetic*/ if (n >= 0) {
|
||||||
|
result = n;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result = -n;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function box() {
|
||||||
|
var result = test(20);
|
||||||
|
if (result != 20) return "fail1: " + result;
|
||||||
|
|
||||||
|
result = test(-20);
|
||||||
|
if (result != 20) return "fail2: " + result;
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
function test(n) {
|
||||||
|
return n >= 0 ? n : -n;
|
||||||
|
}
|
||||||
|
|
||||||
|
function box() {
|
||||||
|
var result = test(20);
|
||||||
|
if (result != 20) return "fail1: " + result;
|
||||||
|
|
||||||
|
result = test(-20);
|
||||||
|
if (result != 20) return "fail2: " + result;
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
function test(n) {
|
||||||
|
/*synthetic*/ if (n >= 0) {
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return -n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function box() {
|
||||||
|
var result = test(20);
|
||||||
|
if (result != 20) return "fail1: " + result;
|
||||||
|
|
||||||
|
result = test(-20);
|
||||||
|
if (result != 20) return "fail2: " + result;
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
function test(n) {
|
||||||
|
var result;
|
||||||
|
if (n >= 0) {
|
||||||
|
result = n;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result = -n;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function box() {
|
||||||
|
var result = test(20);
|
||||||
|
if (result != 20) return "fail1: " + result;
|
||||||
|
|
||||||
|
result = test(-20);
|
||||||
|
if (result != 20) return "fail2: " + result;
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
function test(n) {
|
||||||
|
var result;
|
||||||
|
var $tmp;
|
||||||
|
if (n >= 0) {
|
||||||
|
$tmp = n;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$tmp = -n;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = $tmp;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function box() {
|
||||||
|
var result = test(20);
|
||||||
|
if (result != 20) return "fail1: " + result;
|
||||||
|
|
||||||
|
result = test(-20);
|
||||||
|
if (result != 20) return "fail2: " + result;
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
function test(n) {
|
||||||
|
var result;
|
||||||
|
if (n >= 0) {
|
||||||
|
result = n;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result = -n;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function box() {
|
||||||
|
var result = test(20)
|
||||||
|
if (result != 20) return "fail1: " + result;
|
||||||
|
|
||||||
|
result = test(-20)
|
||||||
|
if (result != 20) return "fail2: " + result;
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
function test(n) {
|
||||||
|
var $tmp;
|
||||||
|
if (n >= 0) {
|
||||||
|
$tmp = n;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$tmp = -n;
|
||||||
|
}
|
||||||
|
var result = $tmp;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function box() {
|
||||||
|
var result = test(20);
|
||||||
|
if (result != 20) return "fail1: " + result;
|
||||||
|
|
||||||
|
result = test(-20);
|
||||||
|
if (result != 20) return "fail2: " + result;
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
function test(n) {
|
||||||
|
if (n >= 0) {
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return -n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function box() {
|
||||||
|
var result = test(20);
|
||||||
|
if (result != 20) return "fail1: " + result;
|
||||||
|
|
||||||
|
result = test(-20);
|
||||||
|
if (result != 20) return "fail2: " + result;
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
function test(n) {
|
||||||
|
var $tmp;
|
||||||
|
if (n >= 0) {
|
||||||
|
$tmp = n;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$tmp = -n;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
function box() {
|
||||||
|
var result = test(20);
|
||||||
|
if (result != 20) return "fail1: " + result;
|
||||||
|
|
||||||
|
result = test(-20);
|
||||||
|
if (result != 20) return "fail2: " + result;
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
function test(a, b, c) {
|
||||||
|
return a + b + c;
|
||||||
|
}
|
||||||
|
|
||||||
|
function box() {
|
||||||
|
var result = test(2, 3, 4);
|
||||||
|
if (result != 9) return "fail: " + result;
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
function test(a, b, c) {
|
||||||
|
var $tmp;
|
||||||
|
$tmp = a + b;
|
||||||
|
return $tmp + c;
|
||||||
|
}
|
||||||
|
|
||||||
|
function box() {
|
||||||
|
var result = test(2, 3, 4);
|
||||||
|
if (result != 9) return "fail: " + result;
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
function test(a, b, c) {
|
||||||
|
return a + b + c;
|
||||||
|
}
|
||||||
|
|
||||||
|
function box() {
|
||||||
|
var result = test(2, 3, 4);
|
||||||
|
if (result != 9) return "fail: " + result;
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
function test(a, b, c) {
|
||||||
|
var $tmp = a + b;
|
||||||
|
return $tmp + c;
|
||||||
|
}
|
||||||
|
|
||||||
|
function box() {
|
||||||
|
var result = test(2, 3, 4);
|
||||||
|
if (result != 9) return "fail: " + result;
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user