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.text.StringUtil
import com.intellij.openapi.vfs.CharsetToolkit
import com.intellij.openapi.vfs.StandardFileSystems
import com.intellij.openapi.vfs.VirtualFileManager
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.TargetBackend
import org.jetbrains.kotlin.utils.DFS
import org.mozilla.javascript.Context
import java.io.ByteArrayOutputStream
import java.io.Closeable
import java.io.File
@@ -76,6 +74,8 @@ abstract class BasicBoxTest(
protected open fun getOutputPrefixFile(testFilePath: String): File? = null
protected open fun getOutputPostfixFile(testFilePath: String): File? = null
protected open val runMinifierByDefault: Boolean = false
fun doTest(filePath: String) {
doTest(filePath, "OK", MainCallParameters.noCall())
}
@@ -159,8 +159,46 @@ abstract class BasicBoxTest(
performAdditionalChecks(generatedJsFiles.map { it.first }, outputPrefixFile, outputPostfixFile)
minifyAndRun(File(File(outputDir, "min"), file.nameWithoutExtension), allJsFiles, generatedJsFiles, expectedResult,
mainModuleName, testFactory.testPackage, TEST_FUNCTION, withModuleSystem)
val expectedReachableNodesMatcher = EXPECTED_REACHABLE_NODES.matcher(fileContent)
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(
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 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 additionalReachableNodes = setOf(
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 reachableNodes = DeadCodeElimination.run(allFilesToMinify, additionalReachableNodes) { }
if (reachableNodes.size > 1500) println("!!!")
println(reachableNodes.size.toString() + ": " + workDir.path)
val dceResult = DeadCodeElimination.run(allFilesToMinify, additionalReachableNodes) { }
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>()
runList += kotlinJsLibOutput
runList += kotlinTestJsLibOutput
runList += TEST_DATA_DIR_PATH + "nashorn-polyfills.js"
runList += allJsFiles.map { filesToMinify[it]?.outputPath ?: it }
val context = Context.enter()
context.languageVersion = Context.VERSION_1_8
context.optimizationLevel = -1
val scope = context.initStandardObjects()
fun applyFile(fileToRun: String) {
val code = FileUtil.loadFile(File(fileToRun), CharsetToolkit.UTF8, true)
context.evaluateString(scope, code, fileToRun, 1, null)
val result = engineForMinifier.runAndRestoreContext {
runList.forEach(this::loadFile)
overrideAsserter()
eval(NashornJsTestChecker.SETUP_KOTLIN_OUTPUT)
runTestFunction(testModuleName, testPackage, testFunction, withModuleSystem)
}
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)
TestCase.assertEquals(expectedResult, result)
}
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_INLINE_PATTERN = Pattern.compile("^// *NO_INLINE *$", 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 AST_EXTENSION = "jsast"
private val METADATA_EXTENSION = "jsmeta"
@@ -554,5 +587,7 @@ abstract class BasicBoxTest(
private val OLD_MODULE_SUFFIX = "-old"
const val KOTLIN_TEST_INTERNAL = "\$kotlin_test_internal\$"
private val engineForMinifier = createScriptEngine()
}
}
@@ -27,14 +27,63 @@ fun createScriptEngine(): ScriptEngine =
// TODO use "-strict"
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('\\', '/')}');")
}
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] }
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 engine = createScriptEngineForTest()
@@ -68,47 +117,17 @@ object NashornJsTestChecker {
testFunctionName: String,
withModuleSystem: Boolean
) = run(files) {
val testModule =
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)
runTestFunction(testModuleName, testPackageName, testFunctionName, withModuleSystem)
}
private fun run(
files: List<String>,
f: ScriptEngine.() -> Any?
): Any? {
val globalObject = engine.eval("this") as ScriptObjectMirror
val before = globalObject.toMapWithAllMembers()
engine.eval(SETUP_KOTLIN_OUTPUT)
try {
return engine.runAndRestoreContext {
files.forEach(engine::loadFile)
return engine.f()
}
finally {
val after = globalObject.toMapWithAllMembers()
val diff = after.entries - before.entries
diff.forEach {
globalObject.put(it.key, before[it.key] ?: ScriptRuntime.UNDEFINED)
}
engine.f()
}
}
@@ -121,7 +140,7 @@ object NashornJsTestChecker {
BasicBoxTest.DIST_DIR_JS_PATH + "kotlin-test.js"
).forEach(engine::loadFile)
engine.eval("this['kotlin-test'].kotlin.test.overrideAsserter_wbnzx$(new this['kotlin-test'].kotlin.test.DefaultAsserter());")
engine.overrideAsserter()
return engine
}
@@ -42,7 +42,9 @@ abstract class AbstractInlineDefaultValuesTests : BorrowedInlineTest("defaultVal
abstract class AbstractBoxJsTest : BasicBoxTest(
BasicBoxTest.TEST_DATA_DIR_PATH + "box/",
BasicBoxTest.TEST_DATA_DIR_PATH + "out/box/"
)
) {
override val runMinifierByDefault: Boolean = true
}
abstract class AbstractJsCodegenBoxTest : BasicBoxTest(
"compiler/testData/codegen/box/",