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:
@@ -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
|
||||
}
|
||||
|
||||
+3
-1
@@ -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/",
|
||||
|
||||
Reference in New Issue
Block a user