Improve test infrastructure for JS DCE tool

Introduce directives to assert minification rate (MINIFIER_THRESHOLD)
and to skip DCE test at all (SKIP_MINIFIER).
Allow to run DCE on some of the box tests by default.
This commit is contained in:
Alexey Andreev
2017-04-25 19:05:24 +03:00
parent 8a8fdf1968
commit ab9b639652
3 changed files with 121 additions and 65 deletions
@@ -18,7 +18,6 @@ package org.jetbrains.kotlin.js.test
import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.CharsetToolkit
import com.intellij.openapi.vfs.StandardFileSystems import com.intellij.openapi.vfs.StandardFileSystems
import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.psi.PsiManager import com.intellij.psi.PsiManager
@@ -56,7 +55,6 @@ import org.jetbrains.kotlin.test.KotlinTestUtils.TestFileFactory
import org.jetbrains.kotlin.test.KotlinTestWithEnvironment import org.jetbrains.kotlin.test.KotlinTestWithEnvironment
import org.jetbrains.kotlin.test.TargetBackend import org.jetbrains.kotlin.test.TargetBackend
import org.jetbrains.kotlin.utils.DFS import org.jetbrains.kotlin.utils.DFS
import org.mozilla.javascript.Context
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.Closeable import java.io.Closeable
import java.io.File import java.io.File
@@ -76,6 +74,8 @@ abstract class BasicBoxTest(
protected open fun getOutputPrefixFile(testFilePath: String): File? = null protected open fun getOutputPrefixFile(testFilePath: String): File? = null
protected open fun getOutputPostfixFile(testFilePath: String): File? = null protected open fun getOutputPostfixFile(testFilePath: String): File? = null
protected open val runMinifierByDefault: Boolean = false
fun doTest(filePath: String) { fun doTest(filePath: String) {
doTest(filePath, "OK", MainCallParameters.noCall()) doTest(filePath, "OK", MainCallParameters.noCall())
} }
@@ -159,8 +159,46 @@ abstract class BasicBoxTest(
performAdditionalChecks(generatedJsFiles.map { it.first }, outputPrefixFile, outputPostfixFile) performAdditionalChecks(generatedJsFiles.map { it.first }, outputPrefixFile, outputPostfixFile)
minifyAndRun(File(File(outputDir, "min"), file.nameWithoutExtension), allJsFiles, generatedJsFiles, expectedResult, val expectedReachableNodesMatcher = EXPECTED_REACHABLE_NODES.matcher(fileContent)
mainModuleName, testFactory.testPackage, TEST_FUNCTION, withModuleSystem) val expectedReachableNodesFound = expectedReachableNodesMatcher.find()
val skipMinification = System.getProperty("kotlin.js.skipMinificationTest", "false").toBoolean()
if (!skipMinification &&
(runMinifierByDefault || expectedReachableNodesFound) &&
!SKIP_MINIFICATION.matcher(fileContent).find()
) {
val thresholdChecker: (Int) -> Unit = { reachableNodesCount ->
val replacement = "// $EXPECTED_REACHABLE_NODES_DIRECTIVE: $reachableNodesCount"
if (!expectedReachableNodesFound) {
file.writeText("$replacement\n$fileContent")
fail("The number of expected reachable nodes was not set. Actual reachable nodes: $reachableNodesCount")
}
else {
val expectedReachableNodes = expectedReachableNodesMatcher.group(1).toInt()
val minThreshold = expectedReachableNodes * 9 / 10
val maxThreshold = expectedReachableNodes * 11 / 10
if (reachableNodesCount < minThreshold || reachableNodesCount > maxThreshold) {
val newText = fileContent.substring(0, expectedReachableNodesMatcher.start()) +
replacement +
fileContent.substring(expectedReachableNodesMatcher.end())
file.writeText(newText)
fail("Number of reachable nodes ($reachableNodesCount) does not fit into expected range " +
"[$minThreshold; $maxThreshold]")
}
}
}
minifyAndRun(
workDir = File(File(outputDir, "min"), file.nameWithoutExtension),
allJsFiles = allJsFiles,
generatedJsFiles = generatedJsFiles,
expectedResult = expectedResult,
testModuleName = mainModuleName,
testPackage = testFactory.testPackage,
testFunction = TEST_FUNCTION,
withModuleSystem = withModuleSystem,
minificationThresholdChecker = thresholdChecker)
}
} }
} }
@@ -407,7 +445,8 @@ abstract class BasicBoxTest(
private fun minifyAndRun( private fun minifyAndRun(
workDir: File, allJsFiles: List<String>, generatedJsFiles: List<Pair<String, TestModule>>, workDir: File, allJsFiles: List<String>, generatedJsFiles: List<Pair<String, TestModule>>,
expectedResult: String, testModuleName: String, testPackage: String?, testFunction: String, withModuleSystem: Boolean expectedResult: String, testModuleName: String, testPackage: String?, testFunction: String, withModuleSystem: Boolean,
minificationThresholdChecker: (Int) -> Unit
) { ) {
val kotlinJsLib = DIST_DIR_JS_PATH + "kotlin.js" val kotlinJsLib = DIST_DIR_JS_PATH + "kotlin.js"
val kotlinTestJsLib = DIST_DIR_JS_PATH + "kotlin-test.js" val kotlinTestJsLib = DIST_DIR_JS_PATH + "kotlin-test.js"
@@ -425,37 +464,28 @@ abstract class BasicBoxTest(
val testFunctionFqn = testModuleName + (if (testPackage.isNullOrEmpty()) "" else ".$testPackage") + ".$testFunction" val testFunctionFqn = testModuleName + (if (testPackage.isNullOrEmpty()) "" else ".$testPackage") + ".$testFunction"
val additionalReachableNodes = setOf( val additionalReachableNodes = setOf(
testFunctionFqn, "kotlin.kotlin.io.BufferedOutput", "kotlin.kotlin.io.output.flush", testFunctionFqn, "kotlin.kotlin.io.BufferedOutput", "kotlin.kotlin.io.output.flush",
"kotlin.kotlin.io.output.buffer" "kotlin.kotlin.io.output.buffer", "kotlin-test.kotlin.test.overrideAsserter_wbnzx$"
) )
val allFilesToMinify = filesToMinify.values + kotlinJsInputFile + kotlinTestJsInputFile val allFilesToMinify = filesToMinify.values + kotlinJsInputFile + kotlinTestJsInputFile
val reachableNodes = DeadCodeElimination.run(allFilesToMinify, additionalReachableNodes) { } val dceResult = DeadCodeElimination.run(allFilesToMinify, additionalReachableNodes) { }
if (reachableNodes.size > 1500) println("!!!")
println(reachableNodes.size.toString() + ": " + workDir.path) val moduleDeclarations = dceResult.globalScope.member("module").member("exports")
val reachableNodes = dceResult.reachableNodes.filter { moduleDeclarations !in generateSequence(it) { it.qualifier?.parent } }
minificationThresholdChecker(reachableNodes.size)
val runList = mutableListOf<String>() val runList = mutableListOf<String>()
runList += kotlinJsLibOutput runList += kotlinJsLibOutput
runList += kotlinTestJsLibOutput runList += kotlinTestJsLibOutput
runList += TEST_DATA_DIR_PATH + "nashorn-polyfills.js"
runList += allJsFiles.map { filesToMinify[it]?.outputPath ?: it }
val context = Context.enter() val result = engineForMinifier.runAndRestoreContext {
context.languageVersion = Context.VERSION_1_8 runList.forEach(this::loadFile)
context.optimizationLevel = -1 overrideAsserter()
val scope = context.initStandardObjects() eval(NashornJsTestChecker.SETUP_KOTLIN_OUTPUT)
runTestFunction(testModuleName, testPackage, testFunction, withModuleSystem)
fun applyFile(fileToRun: String) {
val code = FileUtil.loadFile(File(fileToRun), CharsetToolkit.UTF8, true)
context.evaluateString(scope, code, fileToRun, 1, null)
} }
TestCase.assertEquals(expectedResult, result)
applyFile(DIST_DIR_JS_PATH + RhinoUtils.RHINO_POLYFILLS_RELATIVE_PATH)
applyFile(kotlinJsLibOutput)
applyFile(kotlinTestJsLibOutput)
context.evaluateString(scope, "function ok() {}", "setup assertions", 0, null)
context.evaluateString(scope, RhinoUtils.SETUP_KOTLIN_OUTPUT, "setup kotlin output", 0, null)
allJsFiles.map { filesToMinify[it]?.outputName ?: it }.forEach(::applyFile)
val checker = RhinoFunctionResultChecker(testModuleName, testPackage, testFunction, expectedResult, withModuleSystem)
checker.runChecks(context, scope)
} }
private inner class TestFileFactoryImpl : TestFileFactory<TestModule, TestFile>, Closeable { private inner class TestFileFactoryImpl : TestFileFactory<TestModule, TestFile>, Closeable {
@@ -543,6 +573,9 @@ abstract class BasicBoxTest(
private val NO_MODULE_SYSTEM_PATTERN = Pattern.compile("^// *NO_JS_MODULE_SYSTEM", Pattern.MULTILINE) private val NO_MODULE_SYSTEM_PATTERN = Pattern.compile("^// *NO_JS_MODULE_SYSTEM", Pattern.MULTILINE)
private val NO_INLINE_PATTERN = Pattern.compile("^// *NO_INLINE *$", Pattern.MULTILINE) private val NO_INLINE_PATTERN = Pattern.compile("^// *NO_INLINE *$", Pattern.MULTILINE)
private val SKIP_NODE_JS = Pattern.compile("^// *SKIP_NODE_JS *$", Pattern.MULTILINE) private val SKIP_NODE_JS = Pattern.compile("^// *SKIP_NODE_JS *$", Pattern.MULTILINE)
private val SKIP_MINIFICATION = Pattern.compile("^// *SKIP_MINIFICATION *$", Pattern.MULTILINE)
private val EXPECTED_REACHABLE_NODES_DIRECTIVE = "EXPECTED_REACHABLE_NODES"
private val EXPECTED_REACHABLE_NODES = Pattern.compile("^// *$EXPECTED_REACHABLE_NODES_DIRECTIVE: *([0-9]+) *$", Pattern.MULTILINE)
private val RECOMPILE_PATTERN = Pattern.compile("^// *RECOMPILE *$", Pattern.MULTILINE) private val RECOMPILE_PATTERN = Pattern.compile("^// *RECOMPILE *$", Pattern.MULTILINE)
private val AST_EXTENSION = "jsast" private val AST_EXTENSION = "jsast"
private val METADATA_EXTENSION = "jsmeta" private val METADATA_EXTENSION = "jsmeta"
@@ -554,5 +587,7 @@ abstract class BasicBoxTest(
private val OLD_MODULE_SUFFIX = "-old" private val OLD_MODULE_SUFFIX = "-old"
const val KOTLIN_TEST_INTERNAL = "\$kotlin_test_internal\$" const val KOTLIN_TEST_INTERNAL = "\$kotlin_test_internal\$"
private val engineForMinifier = createScriptEngine()
} }
} }
@@ -27,14 +27,63 @@ fun createScriptEngine(): ScriptEngine =
// TODO use "-strict" // TODO use "-strict"
NashornScriptEngineFactory().getScriptEngine("--language=es5", "--no-java", "--no-syntax-extensions") NashornScriptEngineFactory().getScriptEngine("--language=es5", "--no-java", "--no-syntax-extensions")
private fun ScriptEngine.loadFile(path: String) { fun ScriptEngine.overrideAsserter() {
eval("this['kotlin-test'].kotlin.test.overrideAsserter_wbnzx$(new this['kotlin-test'].kotlin.test.DefaultAsserter());")
}
fun ScriptEngine.runTestFunction(
testModuleName: String, testPackageName: String?, testFunctionName: String,
withModuleSystem: Boolean
): Any? {
val testModule =
when {
withModuleSystem ->
eval(BasicBoxTest.Companion.KOTLIN_TEST_INTERNAL + ".require('" + testModuleName + "')")
else ->
get(testModuleName)
}
testModule as ScriptObjectMirror
val testPackage =
when {
testPackageName === null ->
testModule
testPackageName.contains(".") ->
testPackageName.split(".").fold(testModule) { p, part -> p[part] as ScriptObjectMirror }
else ->
testModule[testPackageName]!!
}
return (this as Invocable).invokeMethod(testPackage, testFunctionName)
}
fun ScriptEngine.loadFile(path: String) {
eval("load('${path.replace('\\', '/')}');") eval("load('${path.replace('\\', '/')}');")
} }
fun ScriptEngine.runAndRestoreContext(
f: ScriptEngine.() -> Any?
): Any? {
val globalObject = eval("this") as ScriptObjectMirror
val before = globalObject.toMapWithAllMembers()
return try {
this.f()
}
finally {
val after = globalObject.toMapWithAllMembers()
val diff = after.entries - before.entries
diff.forEach {
globalObject.put(it.key, before[it.key] ?: ScriptRuntime.UNDEFINED)
}
}
}
private fun ScriptObjectMirror.toMapWithAllMembers(): Map<String, Any?> = getOwnKeys(true).associate { it to this[it] } private fun ScriptObjectMirror.toMapWithAllMembers(): Map<String, Any?> = getOwnKeys(true).associate { it to this[it] }
object NashornJsTestChecker { object NashornJsTestChecker {
private val SETUP_KOTLIN_OUTPUT = "kotlin.kotlin.io.output = new kotlin.kotlin.io.BufferedOutput();" val SETUP_KOTLIN_OUTPUT = "kotlin.kotlin.io.output = new kotlin.kotlin.io.BufferedOutput();"
private val GET_KOTLIN_OUTPUT = "kotlin.kotlin.io.output.buffer;" private val GET_KOTLIN_OUTPUT = "kotlin.kotlin.io.output.buffer;"
private val engine = createScriptEngineForTest() private val engine = createScriptEngineForTest()
@@ -68,47 +117,17 @@ object NashornJsTestChecker {
testFunctionName: String, testFunctionName: String,
withModuleSystem: Boolean withModuleSystem: Boolean
) = run(files) { ) = run(files) {
val testModule = runTestFunction(testModuleName, testPackageName, testFunctionName, withModuleSystem)
when {
withModuleSystem ->
engine.eval(BasicBoxTest.Companion.KOTLIN_TEST_INTERNAL + ".require('" + testModuleName + "')") as ScriptObjectMirror
else ->
engine.get(testModuleName) as ScriptObjectMirror
}
val testPackage =
when {
testPackageName === null ->
testModule
testPackageName.contains(".") ->
testPackageName.split(".").fold(testModule) { p, part -> p[part] as ScriptObjectMirror }
else ->
testModule[testPackageName]!!
}
(engine as Invocable).invokeMethod(testPackage, testFunctionName)
} }
private fun run( private fun run(
files: List<String>, files: List<String>,
f: ScriptEngine.() -> Any? f: ScriptEngine.() -> Any?
): Any? { ): Any? {
val globalObject = engine.eval("this") as ScriptObjectMirror
val before = globalObject.toMapWithAllMembers()
engine.eval(SETUP_KOTLIN_OUTPUT) engine.eval(SETUP_KOTLIN_OUTPUT)
return engine.runAndRestoreContext {
try {
files.forEach(engine::loadFile) files.forEach(engine::loadFile)
return engine.f() engine.f()
}
finally {
val after = globalObject.toMapWithAllMembers()
val diff = after.entries - before.entries
diff.forEach {
globalObject.put(it.key, before[it.key] ?: ScriptRuntime.UNDEFINED)
}
} }
} }
@@ -121,7 +140,7 @@ object NashornJsTestChecker {
BasicBoxTest.DIST_DIR_JS_PATH + "kotlin-test.js" BasicBoxTest.DIST_DIR_JS_PATH + "kotlin-test.js"
).forEach(engine::loadFile) ).forEach(engine::loadFile)
engine.eval("this['kotlin-test'].kotlin.test.overrideAsserter_wbnzx$(new this['kotlin-test'].kotlin.test.DefaultAsserter());") engine.overrideAsserter()
return engine return engine
} }
@@ -42,7 +42,9 @@ abstract class AbstractInlineDefaultValuesTests : BorrowedInlineTest("defaultVal
abstract class AbstractBoxJsTest : BasicBoxTest( abstract class AbstractBoxJsTest : BasicBoxTest(
BasicBoxTest.TEST_DATA_DIR_PATH + "box/", BasicBoxTest.TEST_DATA_DIR_PATH + "box/",
BasicBoxTest.TEST_DATA_DIR_PATH + "out/box/" BasicBoxTest.TEST_DATA_DIR_PATH + "out/box/"
) ) {
override val runMinifierByDefault: Boolean = true
}
abstract class AbstractJsCodegenBoxTest : BasicBoxTest( abstract class AbstractJsCodegenBoxTest : BasicBoxTest(
"compiler/testData/codegen/box/", "compiler/testData/codegen/box/",