package com.intellij.lang.javascript.flex.flexunit; import com.intellij.compiler.options.CompileStepBeforeRun; import com.intellij.compiler.server.BuildManager; import com.intellij.execution.configurations.RunConfiguration; import com.intellij.execution.configurations.RuntimeConfigurationError; import com.intellij.execution.configurations.RuntimeConfigurationException; import com.intellij.flex.FlexCommonUtils; import com.intellij.flex.model.bc.ComponentSet; import com.intellij.flex.model.bc.TargetPlatform; import com.intellij.javascript.flex.resolve.ActionScriptClassResolver; import com.intellij.lang.javascript.flex.FlexBundle; import com.intellij.lang.javascript.flex.projectStructure.model.FlexBuildConfiguration; import com.intellij.lang.javascript.index.JSPackageIndex; import com.intellij.lang.javascript.index.JSPackageIndexInfo; import com.intellij.lang.javascript.psi.ecmal4.JSClass; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.compiler.CompileContext; import com.intellij.openapi.compiler.CompileTask; import com.intellij.openapi.compiler.CompilerMessageCategory; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.module.Module; import com.intellij.openapi.project.DumbService; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.*; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.PsiElement; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.ui.UIBundle; import com.intellij.util.ResourceUtil; import gnu.trove.THashSet; import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.IOException; import java.net.URL; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Set; public class FlexUnitPrecompileTask implements CompileTask { public static final Key<Collection<String>> FILES_TO_DELETE = Key.create("FlexUnitPrecompileTask.filesToRemove"); private final Project myProject; private static final String TEST_RUNNER_VAR = "__testRunner"; private static final int FLEX_UNIT_PORT_START = 7832; private static final int PORTS_ATTEMPT_NUMBER = 20; private static final int SWC_POLICY_PORT_START = FLEX_UNIT_PORT_START + PORTS_ATTEMPT_NUMBER; public FlexUnitPrecompileTask(Project project) { myProject = project; } // in case of external build this path is returned by FlexCommonUtils.getPathToFlexUnitTempDirectory() public static String getPathToFlexUnitTempDirectory(final Project project) { final BuildManager buildManager = BuildManager.getInstance(); final File projectSystemDir = buildManager.getProjectSystemDirectory(project); if (projectSystemDir == null) { Logger.getInstance(FlexUnitPrecompileTask.class.getName()).error(project); return ""; } return FileUtil.toSystemIndependentName(projectSystemDir.getPath()) + "/tmp"; } public boolean execute(CompileContext context) { final RunConfiguration runConfiguration = CompileStepBeforeRun.getRunConfiguration(context.getCompileScope()); if (!(runConfiguration instanceof FlexUnitRunConfiguration)) { return true; } final Ref<Boolean> isDumb = new Ref<>(false); final RuntimeConfigurationException validationError = ApplicationManager.getApplication().runReadAction((NullableComputable<RuntimeConfigurationException>)() -> { if (DumbService.getInstance(myProject).isDumb()) { isDumb.set(true); return null; } try { runConfiguration.checkConfiguration(); return null; } catch (RuntimeConfigurationException e) { return e; } }); if (isDumb.get()) { context.addMessage(CompilerMessageCategory.ERROR, FlexBundle.message("dumb.mode.flex.unit.warning"), null, -1, -1); return false; } if (validationError != null) { context .addMessage(CompilerMessageCategory.ERROR, FlexBundle.message("configuration.not.valid", validationError.getMessage()), null, -1, -1); return false; } int flexUnitPort = ServerConnectionBase.getFreePort(FLEX_UNIT_PORT_START, PORTS_ATTEMPT_NUMBER); if (flexUnitPort == -1) { context.addMessage(CompilerMessageCategory.ERROR, FlexBundle.message("no.free.port"), null, -1, -1); return false; } final int socketPolicyPort; if (SystemInfo.isWindows && ServerConnectionBase.tryPort(SwfPolicyFileConnection.DEFAULT_PORT)) { socketPolicyPort = SwfPolicyFileConnection.DEFAULT_PORT; } else { socketPolicyPort = ServerConnectionBase.getFreePort(SWC_POLICY_PORT_START, PORTS_ATTEMPT_NUMBER); } if (socketPolicyPort == -1) { context.addMessage(CompilerMessageCategory.ERROR, FlexBundle.message("no.free.port"), null, -1, -1); return false; } final FlexUnitRunnerParameters params = ((FlexUnitRunConfiguration)runConfiguration).getRunnerParameters(); params.setPort(flexUnitPort); params.setSocketPolicyPort(socketPolicyPort); final Ref<Module> moduleRef = new Ref<>(); final Ref<FlexBuildConfiguration> bcRef = new Ref<>(); final Ref<FlexUnitSupport> supportRef = new Ref<>(); ApplicationManager.getApplication().runReadAction(() -> { if (DumbService.getInstance(myProject).isDumb()) return; try { final Pair<Module, FlexBuildConfiguration> moduleAndBC = params.checkAndGetModuleAndBC(myProject); moduleRef.set(moduleAndBC.first); bcRef.set(moduleAndBC.second); supportRef.set(FlexUnitSupport.getSupport(moduleAndBC.second, moduleAndBC.first)); } catch (RuntimeConfigurationError e) { // already checked above, can't happen throw new RuntimeException(e); } }); final Module module = moduleRef.get(); final FlexBuildConfiguration bc = bcRef.get(); final FlexUnitSupport support = supportRef.get(); if (bc == null || support == null) { context.addMessage(CompilerMessageCategory.ERROR, FlexBundle.message("dumb.mode.flex.unit.warning"), null, -1, -1); return false; } final GlobalSearchScope moduleScope = GlobalSearchScope.moduleScope(module); StringBuilder imports = new StringBuilder(); StringBuilder code = new StringBuilder(); final boolean flexUnit4; switch (params.getScope()) { case Class: { final Ref<Boolean> isFlexUnit1Suite = new Ref<>(); final Ref<Boolean> isSuite = new Ref<>(); Set<String> customRunners = ApplicationManager.getApplication().runReadAction((NullableComputable<Set<String>>)() -> { if (DumbService.getInstance(myProject).isDumb()) return null; Set<String> result = new THashSet<>(); final JSClass clazz = (JSClass)ActionScriptClassResolver.findClassByQNameStatic(params.getClassName(), moduleScope); collectCustomRunners(result, clazz, support, null); isFlexUnit1Suite.set(support.isFlexUnit1SuiteSubclass(clazz)); isSuite.set(support.isSuite(clazz)); return result; }); if (customRunners == null) { context.addMessage(CompilerMessageCategory.ERROR, FlexBundle.message("dumb.mode.flex.unit.warning"), null, -1, -1); return false; } // FlexUnit4 can't run FlexUnit1 TestSuite subclasses, fallback to FlexUnit1 runner flexUnit4 = support.flexUnit4Present && !isFlexUnit1Suite.get(); generateImportCode(imports, params.getClassName(), customRunners); generateTestClassCode(code, params.getClassName(), customRunners, isSuite.get()); } break; case Method: { Set<String> customRunners = ApplicationManager.getApplication().runReadAction((NullableComputable<Set<String>>)() -> { if (DumbService.getInstance(myProject).isDumb()) return null; Set<String> result = new THashSet<>(); final JSClass clazz = (JSClass)ActionScriptClassResolver.findClassByQNameStatic(params.getClassName(), moduleScope); collectCustomRunners(result, clazz, support, null); return result; }); if (customRunners == null) { context.addMessage(CompilerMessageCategory.ERROR, FlexBundle.message("dumb.mode.flex.unit.warning"), null, -1, -1); return false; } flexUnit4 = support.flexUnit4Present; generateImportCode(imports, params.getClassName(), customRunners); generateTestMethodCode(code, params.getClassName(), params.getMethodName(), customRunners); } break; case Package: { final Collection<Pair<String, Set<String>>> classes = ApplicationManager.getApplication().runReadAction((NullableComputable<Collection<Pair<String, Set<String>>>>)() -> { if (DumbService.getInstance(myProject).isDumb()) return null; final Collection<Pair<String, Set<String>>> result = new ArrayList<>(); JSPackageIndex .processElementsInScopeRecursive(params.getPackageName(), new JSPackageIndex.PackageQualifiedElementsProcessor() { public boolean process(String qualifiedName, JSPackageIndexInfo.Kind kind, boolean isPublic) { if (kind == JSPackageIndexInfo.Kind.CLASS) { PsiElement clazz = ActionScriptClassResolver.findClassByQNameStatic(qualifiedName, moduleScope); if (clazz instanceof JSClass && support.isTestClass((JSClass)clazz, false)) { Set<String> customRunners = new THashSet<>(); collectCustomRunners(customRunners, (JSClass)clazz, support, null); result.add(Pair.create(((JSClass)clazz).getQualifiedName(), customRunners)); } } return true; } }, moduleScope, myProject); return result; }); if (classes == null) { context.addMessage(CompilerMessageCategory.ERROR, FlexBundle.message("dumb.mode.flex.unit.warning"), null, -1, -1); return false; } if (classes.isEmpty()) { String message = MessageFormat.format("No tests found in package ''{0}''", params.getPackageName()); context.addMessage(CompilerMessageCategory.WARNING, message, null, -1, -1); return false; } flexUnit4 = support.flexUnit4Present; for (Pair<String, Set<String>> classAndRunner : classes) { generateImportCode(imports, classAndRunner.first, classAndRunner.second); generateTestClassCode(code, classAndRunner.first, classAndRunner.second, false); } } break; default: flexUnit4 = false; assert false : "Unknown scope: " + params.getScope(); } if (!flexUnit4 && bc.isPureAs()) { context.addMessage(CompilerMessageCategory.ERROR, FlexBundle.message("cant.execute.flexunit1.for.pure.as.bc"), null, -1, -1); } String launcherText; try { launcherText = getLauncherTemplate(bc); } catch (IOException e) { context.addMessage(CompilerMessageCategory.ERROR, e.getMessage(), null, -1, -1); return false; } final boolean desktop = bc.getTargetPlatform() == TargetPlatform.Desktop; if (desktop) { generateImportCode(imports, "flash.desktop.NativeApplication"); } launcherText = replace(launcherText, "/*imports*/", imports.toString()); launcherText = replace(launcherText, "/*test_runner*/", flexUnit4 ? FlexCommonUtils.FLEXUNIT_4_TEST_RUNNER : FlexCommonUtils.FLEXUNIT_1_TEST_RUNNER); launcherText = replace(launcherText, "/*code*/", code.toString()); launcherText = replace(launcherText, "/*port*/", String.valueOf(flexUnitPort)); launcherText = replace(launcherText, "/*socketPolicyPort*/", String.valueOf(socketPolicyPort)); launcherText = replace(launcherText, "/*module*/", module.getName()); if (!bc.isPureAs()) { final FlexUnitRunnerParameters.OutputLogLevel logLevel = params.getOutputLogLevel(); launcherText = replace(launcherText, "/*isLogEnabled*/", logLevel != null ? "1" : "0"); launcherText = replace(launcherText, "/*logLevel*/", logLevel != null ? logLevel.getFlexConstant() : FlexUnitRunnerParameters.OutputLogLevel.All.getFlexConstant()); } final File tmpDir = new File(getPathToFlexUnitTempDirectory(myProject)); boolean ok = true; if (tmpDir.isFile()) ok &= FileUtil.delete(tmpDir); if (!tmpDir.isDirectory()) ok &= tmpDir.mkdirs(); if (!ok) { final String message = UIBundle.message("create.new.folder.could.not.create.folder.error.message", FileUtil.toSystemDependentName(tmpDir.getPath())); context.addMessage(CompilerMessageCategory.ERROR, message, null, -1, -1); return false; } final String fileName = FlexCommonUtils.FLEX_UNIT_LAUNCHER + FlexCommonUtils.getFlexUnitLauncherExtension(bc.getNature()); final File launcherFile = new File(tmpDir, fileName); FileUtil.delete(launcherFile); try { FileUtil.writeToFile(launcherFile, launcherText); } catch (IOException e) { context.addMessage(CompilerMessageCategory.ERROR, e.getMessage(), null, -1, -1); return false; } context.putUserData(FILES_TO_DELETE, Collections.singletonList(launcherFile.getPath())); return true; } private static String replace(final String text, final String pattern, final String replacement) { if (!text.contains(pattern)) { throw new RuntimeException("Pattern '" + pattern + "' not found in launcher text"); } return text.replace(pattern, replacement); } private static void collectCustomRunners(Set<String> result, JSClass testClass, FlexUnitSupport flexUnitSupport, @Nullable Collection<JSClass> seen) { if (seen != null && seen.contains(testClass)) return; final String customRunner = FlexUnitSupport.getCustomRunner(testClass); if (!StringUtil.isEmptyOrSpaces(customRunner)) result.add(customRunner); if (flexUnitSupport.isSuite(testClass)) { if (seen == null) seen = new THashSet<>(); seen.add(testClass); for (JSClass referencedClass : flexUnitSupport.getSuiteTestClasses(testClass)) { collectCustomRunners(result, referencedClass, flexUnitSupport, seen); } } } private static void generateImportCode(StringBuilder imports, String className, Collection<String> customRunners) { if (!StringUtil.isEmpty(StringUtil.getPackageName(className))) { generateImportCode(imports, className); } for (String customRunner : customRunners) { generateImportCode(imports, customRunner); } } private static void generateImportCode(StringBuilder imports, String qname) { imports.append("import ").append(qname).append(";\n"); } private static void generateTestMethodCode(StringBuilder code, String className, String methodName, Collection<String> customRunners) { code.append(TEST_RUNNER_VAR).append(".addTestMethod(").append(className).append(", \"").append(methodName).append("\");\n"); generateReferences(code, className, customRunners); } private static void generateTestClassCode(StringBuilder code, String className, Collection<String> customRunners, boolean isSuite) { code.append(TEST_RUNNER_VAR).append(".").append(isSuite ? "addTestSuiteClass(" : "addTestClass(").append(className).append(");\n"); generateReferences(code, className, customRunners); } private static void generateReferences(StringBuilder code, String className, Collection<String> classes) { int i = 1; for (String qname : classes) { code.append("var __ref_").append(className.replace(".", "_")).append("_").append(i++).append("_ : ").append(qname).append(";\n"); } } private static String getLauncherTemplate(FlexBuildConfiguration bc) throws IOException { String templateName; if (bc.isPureAs()) { templateName = "LauncherTemplateAs.as"; } else if (bc.getNature().isMobilePlatform() || bc.getDependencies().getComponentSet() == ComponentSet.SparkOnly) { templateName = "LauncherTemplateSpark.mxml"; } else { templateName = "LauncherTemplateMx.mxml"; } final URL resource = FlexUnitPrecompileTask.class.getResource("/unittestingsupport/" + templateName); return ResourceUtil.loadText(resource); } }