diff --git a/.gitignore b/.gitignore index 7a81c1e3c8de..5e640a6eff50 100644 --- a/.gitignore +++ b/.gitignore @@ -17,5 +17,4 @@ edu/dependencies/.gradle edu/dependencies/build native/**/build/ stale_outputs_checked -/android -/tools/ideTestingFramework/intellij.tools.ide.starter \ No newline at end of file +/android \ No newline at end of file diff --git a/.idea/libraries/aia_proto.xml b/.idea/libraries/aia_proto.xml index 9fc3bbfdf3f3..63ec506d6533 100644 --- a/.idea/libraries/aia_proto.xml +++ b/.idea/libraries/aia_proto.xml @@ -1,8 +1,8 @@ - + - + diff --git a/.idea/libraries/android_test_plugin_host_device_info_proto.xml b/.idea/libraries/android_test_plugin_host_device_info_proto.xml index 6e4a6cc564e1..04d2e8f84d80 100644 --- a/.idea/libraries/android_test_plugin_host_device_info_proto.xml +++ b/.idea/libraries/android_test_plugin_host_device_info_proto.xml @@ -1,8 +1,8 @@ - + - + diff --git a/.idea/libraries/baksmali.xml b/.idea/libraries/baksmali.xml index adae6d1501df..f69131f662e6 100644 --- a/.idea/libraries/baksmali.xml +++ b/.idea/libraries/baksmali.xml @@ -1,8 +1,8 @@ - + - + diff --git a/.idea/libraries/dexlib2.xml b/.idea/libraries/dexlib2.xml index d8320eb4e4d5..fb3a02bd0bb7 100644 --- a/.idea/libraries/dexlib2.xml +++ b/.idea/libraries/dexlib2.xml @@ -1,15 +1,8 @@ - - - - - - - + - - + diff --git a/.idea/libraries/emulator_proto.xml b/.idea/libraries/emulator_proto.xml index 40347b3b0b47..2011517ec059 100644 --- a/.idea/libraries/emulator_proto.xml +++ b/.idea/libraries/emulator_proto.xml @@ -1,8 +1,8 @@ - + - + diff --git a/.idea/libraries/ffmpeg.xml b/.idea/libraries/ffmpeg.xml new file mode 100644 index 000000000000..18697b01d560 --- /dev/null +++ b/.idea/libraries/ffmpeg.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/ffmpeg_platform.xml b/.idea/libraries/ffmpeg_platform.xml new file mode 100644 index 000000000000..382da63c0311 --- /dev/null +++ b/.idea/libraries/ffmpeg_platform.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/layout_inspector_compose_java_proto.xml b/.idea/libraries/layout_inspector_compose_java_proto.xml index f2658c2e9292..c442d4a0dfe4 100644 --- a/.idea/libraries/layout_inspector_compose_java_proto.xml +++ b/.idea/libraries/layout_inspector_compose_java_proto.xml @@ -1,8 +1,8 @@ - + - + diff --git a/.idea/libraries/layout_inspector_snapshot_java_proto.xml b/.idea/libraries/layout_inspector_snapshot_java_proto.xml index 374e85feb2c5..b1b449ad4492 100644 --- a/.idea/libraries/layout_inspector_snapshot_java_proto.xml +++ b/.idea/libraries/layout_inspector_snapshot_java_proto.xml @@ -1,8 +1,8 @@ - + - + diff --git a/.idea/libraries/layout_inspector_view_java_proto.xml b/.idea/libraries/layout_inspector_view_java_proto.xml index 0626fdc11e42..b5f4f0520ced 100644 --- a/.idea/libraries/layout_inspector_view_java_proto.xml +++ b/.idea/libraries/layout_inspector_view_java_proto.xml @@ -1,8 +1,8 @@ - + - + diff --git a/.idea/libraries/layoutinspector_skia_proto.xml b/.idea/libraries/layoutinspector_skia_proto.xml index 1d945ddd1b03..e0ddeba1f91d 100644 --- a/.idea/libraries/layoutinspector_skia_proto.xml +++ b/.idea/libraries/layoutinspector_skia_proto.xml @@ -1,8 +1,8 @@ - + - + diff --git a/.idea/libraries/layoutlib.xml b/.idea/libraries/layoutlib.xml index 594cb4c2aa76..fb25de424cff 100644 --- a/.idea/libraries/layoutlib.xml +++ b/.idea/libraries/layoutlib.xml @@ -1,8 +1,8 @@ - + - + diff --git a/.idea/libraries/libam_instrumentation_data_proto.xml b/.idea/libraries/libam_instrumentation_data_proto.xml index 32d969ff7b74..2c59846a8d39 100644 --- a/.idea/libraries/libam_instrumentation_data_proto.xml +++ b/.idea/libraries/libam_instrumentation_data_proto.xml @@ -1,8 +1,8 @@ - + - + diff --git a/.idea/libraries/libapp_processes_proto.xml b/.idea/libraries/libapp_processes_proto.xml index c6c32367f8b3..7da13f078518 100644 --- a/.idea/libraries/libapp_processes_proto.xml +++ b/.idea/libraries/libapp_processes_proto.xml @@ -1,8 +1,8 @@ - + - + diff --git a/.idea/libraries/network_inspector_java_proto.xml b/.idea/libraries/network_inspector_java_proto.xml index a33da4dd87cb..461a32615482 100644 --- a/.idea/libraries/network_inspector_java_proto.xml +++ b/.idea/libraries/network_inspector_java_proto.xml @@ -1,8 +1,8 @@ - + - + diff --git a/.idea/libraries/perfetto_proto.xml b/.idea/libraries/perfetto_proto.xml index bdb7e66049fe..40aaba0c58a1 100644 --- a/.idea/libraries/perfetto_proto.xml +++ b/.idea/libraries/perfetto_proto.xml @@ -1,8 +1,8 @@ - + - + diff --git a/.idea/libraries/sqlite_inspector_proto.xml b/.idea/libraries/sqlite_inspector_proto.xml index e2df5d1c3f4b..ddf8d789d9ec 100644 --- a/.idea/libraries/sqlite_inspector_proto.xml +++ b/.idea/libraries/sqlite_inspector_proto.xml @@ -1,8 +1,8 @@ - + - + diff --git a/.idea/libraries/studio_analytics_proto.xml b/.idea/libraries/studio_analytics_proto.xml index 7d5e8e299bb6..0676477f74c7 100644 --- a/.idea/libraries/studio_analytics_proto.xml +++ b/.idea/libraries/studio_analytics_proto.xml @@ -1,8 +1,8 @@ - + - + diff --git a/.idea/libraries/studio_grpc.xml b/.idea/libraries/studio_grpc.xml index 32ae201bdec4..a161e4067db7 100644 --- a/.idea/libraries/studio_grpc.xml +++ b/.idea/libraries/studio_grpc.xml @@ -1,8 +1,8 @@ - + - + diff --git a/.idea/libraries/studio_proto.xml b/.idea/libraries/studio_proto.xml index 4819708d646d..58375af4aa9a 100644 --- a/.idea/libraries/studio_proto.xml +++ b/.idea/libraries/studio_proto.xml @@ -1,8 +1,8 @@ - + - + diff --git a/.idea/libraries/transport_proto.xml b/.idea/libraries/transport_proto.xml index 3b61b9d93284..1b0016db88e8 100644 --- a/.idea/libraries/transport_proto.xml +++ b/.idea/libraries/transport_proto.xml @@ -1,8 +1,8 @@ - + - + diff --git a/.idea/modules.xml b/.idea/modules.xml index 6cb542914381..1eeeb2d60c8c 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -317,11 +317,14 @@ + + + + - - + + - @@ -329,18 +332,12 @@ - - - - - - @@ -349,7 +346,6 @@ - @@ -357,57 +353,63 @@ + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - + - - @@ -426,6 +428,10 @@ + + + + @@ -434,12 +440,12 @@ + - - + @@ -456,13 +462,11 @@ - - + - @@ -472,7 +476,6 @@ - @@ -499,6 +502,7 @@ + @@ -506,13 +510,17 @@ + + - - - + + + + + @@ -708,6 +716,8 @@ + + @@ -1022,6 +1032,7 @@ + @@ -1204,6 +1215,12 @@ + + + + + + \ No newline at end of file diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/README.md b/tools/ideTestingFramework/intellij.tools.ide.starter/README.md index 9f4341221139..60a846b5ce5d 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/README.md +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/README.md @@ -1,6 +1,5 @@ ### Starter for IntelliJ IDEA based IDE's - #### Overview Starter helps you write tests/code, that will start IntelliJ-based IDE from installer in external process. @@ -14,7 +13,6 @@ Aside from that, you may find useful functionality as below: * reporting of artifacts to CI (optional) * run a test with a profiler (not yet included) - #### Supported products * IDEA @@ -26,7 +24,6 @@ Aside from that, you may find useful functionality as below: * RubyMine * Android Studio - ##### How to setup Configure maven repositories in your `build.gradle` file @@ -48,6 +45,7 @@ OR `maven { url = "https://www.jetbrains.com/intellij-repository/nightly" }` But don't forget to change accordingly version of the packages as such: + * nightly -> LATEST-TRUNK-SNAPSHOT * snapshots -> LATEST-EAP-SNAPSHOT * releases -> semver package version @@ -80,8 +78,6 @@ compileTestKotlin { ``` - - ##### Run with JUnit4 [Example of simple test, that will download IntelliJ IDEA and start import of gradle project](https://github.com/JetBrains/intellij-ide-starter/tree/master/testSrc/com/intellij/ide/starter/tests/examples/junit4) @@ -92,10 +88,10 @@ You should create appropriate classes in your tests for JUnit4StarterRule, IdeaC [Example of simple test, that will download IntelliJ IDEA and start import of gradle project](https://github.com/JetBrains/intellij-ide-starter/tree/master/testSrc/com/intellij/ide/starter/tests/examples/junit5) - ##### Available commands from plugins Dependency `performance-testing-commands` + - waitForSmartMode() - flushIndexes() - setupProjectSdk(sdkName: String, sdkType: String, sdkPath: String) @@ -132,9 +128,11 @@ Dependency `performance-testing-commands` - ... **TBD** Dependency `performance-testing-maven-commands` + - importMavenProject() Dependency `performance-testing-gradle-commands` + - importGradleProject() #### What behaviour might be extended / modified @@ -146,6 +144,7 @@ For example, you might write your own implementation of CIServer and provide it NOTE: Be sure to use the same version of Kodein, that is used in `build.gradle` for starter project. E.g: + ``` di = DI { extend(di) diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/intellij.tools.ide.starter.iml b/tools/ideTestingFramework/intellij.tools.ide.starter/intellij.tools.ide.starter.iml index 3e6666d2ada2..62f4b3359d39 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/intellij.tools.ide.starter.iml +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/intellij.tools.ide.starter.iml @@ -2,14 +2,16 @@ - + - - diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/jvmti/Makefile b/tools/ideTestingFramework/intellij.tools.ide.starter/jvmti/Makefile new file mode 100644 index 000000000000..af6438896daa --- /dev/null +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/jvmti/Makefile @@ -0,0 +1,11 @@ +# JVM TI agent that logs loading and preparation stages of class lifecycle +# Set JAVA_HOME to JBR 17 location + +# Usage VM Options: -agentpath:/var/tmp/libvmtrace.so=output.log +# Original source: https://github.com/odnoklassniki/jvmti-tools#vmtrace + +build-linux: + g++ -O2 -fPIC -shared -I $(JAVA_HOME)/include -I $(JAVA_HOME)/include/linux -olibvmtrace.so vmtrace.cpp + +build-macos: + g++ -O2 -fPIC -shared -I $(JAVA_HOME)/include -I $(JAVA_HOME)/include/darwin -olibvmtrace.dylib vmtrace.cpp diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/jvmti/vmtrace.cpp b/tools/ideTestingFramework/intellij.tools.ide.starter/jvmti/vmtrace.cpp new file mode 100644 index 000000000000..244c4cd835ef --- /dev/null +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/jvmti/vmtrace.cpp @@ -0,0 +1,119 @@ +/* + * Copyright 2019 Odnoklassniki Ltd, Mail.Ru Group + * + * 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. + */ + +// Original source: https://github.com/odnoklassniki/jvmti-tools#vmtrace + +#include +#include +#include +#include + +static FILE* out; +static jrawMonitorID vmtrace_lock; +static jlong start_time; + +static void trace(jvmtiEnv* jvmti, const char* fmt, ...) { + jlong current_time; + jvmti->GetTime(¤t_time); + + char buf[1024]; + va_list args; + va_start(args, fmt); + vsnprintf(buf, sizeof(buf), fmt, args); + va_end(args); + + jvmti->RawMonitorEnter(vmtrace_lock); + + fprintf(out, "[%.5f] %s\n", (current_time - start_time) / 1000000000.0, buf); + + jvmti->RawMonitorExit(vmtrace_lock); +} + +static char* fix_class_name(char* class_name) { + // Strip 'L' and ';' from class signature + class_name[strlen(class_name) - 1] = 0; + return class_name + 1; +} + +class ClassName { + private: + jvmtiEnv* _jvmti; + char* _name; + + public: + ClassName(jvmtiEnv* jvmti, jclass klass) : _jvmti(jvmti), _name(NULL) { + _jvmti->GetClassSignature(klass, &_name, NULL); + } + + ~ClassName() { + _jvmti->Deallocate((unsigned char*) _name); + } + + char* name() { + return _name == NULL ? NULL : fix_class_name(_name); + } +}; + +void JNICALL ClassFileLoadHook(jvmtiEnv* jvmti, JNIEnv* env, + jclass class_being_redefined, jobject loader, + const char* name, jobject protection_domain, + jint data_len, const unsigned char* data, + jint* new_data_len, unsigned char** new_data) { + trace(jvmti, "Loading class: %s (%d bytes)", name, data_len); +} + +void JNICALL ClassPrepare(jvmtiEnv* jvmti, JNIEnv* env, + jthread thread, jclass klass) { + ClassName cn(jvmti, klass); + trace(jvmti, "Class prepared: %s", cn.name()); +} + +JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM* vm, char* options, void* reserved) { + if (options == NULL || !options[0]) { + out = stderr; + } else if ((out = fopen(options, "w")) == NULL) { + fprintf(stderr, "Cannot open output file: %s\n", options); + return 1; + } + + jvmtiEnv* jvmti; + vm->GetEnv((void**) &jvmti, JVMTI_VERSION_1_0); + + jvmti->CreateRawMonitor("vmtrace_lock", &vmtrace_lock); + jvmti->GetTime(&start_time); + + trace(jvmti, "VMTrace started"); + + jvmtiCapabilities capabilities = {0}; + capabilities.can_generate_all_class_hook_events = 1; + jvmti->AddCapabilities(&capabilities); + + jvmtiEventCallbacks callbacks = {0}; + callbacks.ClassFileLoadHook = ClassFileLoadHook; + callbacks.ClassPrepare = ClassPrepare; + jvmti->SetEventCallbacks(&callbacks, sizeof(callbacks)); + + jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, NULL); + jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_CLASS_PREPARE, NULL); + + return 0; +} + +JNIEXPORT void JNICALL Agent_OnUnload(JavaVM* vm) { + if (out != NULL && out != stderr) { + fclose(out); + } +} diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/resources/ide.general.xml b/tools/ideTestingFramework/intellij.tools.ide.starter/resources/ide.general.xml index ef5d50ba15a3..1ca3a5dd93ce 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/resources/ide.general.xml +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/resources/ide.general.xml @@ -1,6 +1,6 @@ - diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/resources/libvmtrace-aarch64.dylib b/tools/ideTestingFramework/intellij.tools.ide.starter/resources/libvmtrace-aarch64.dylib new file mode 100644 index 000000000000..bf0582a489fe Binary files /dev/null and b/tools/ideTestingFramework/intellij.tools.ide.starter/resources/libvmtrace-aarch64.dylib differ diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/resources/libvmtrace.dylib b/tools/ideTestingFramework/intellij.tools.ide.starter/resources/libvmtrace.dylib new file mode 100644 index 000000000000..4d072158fdaf Binary files /dev/null and b/tools/ideTestingFramework/intellij.tools.ide.starter/resources/libvmtrace.dylib differ diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/resources/libvmtrace.so b/tools/ideTestingFramework/intellij.tools.ide.starter/resources/libvmtrace.so new file mode 100755 index 000000000000..70489e9c4ae5 Binary files /dev/null and b/tools/ideTestingFramework/intellij.tools.ide.starter/resources/libvmtrace.so differ diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/android.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/android.kt index fa0fa6e03606..1c2194466489 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/android.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/android.kt @@ -1,10 +1,10 @@ package com.intellij.ide.starter import com.intellij.ide.starter.di.di -import com.intellij.ide.starter.exec.ExecOutputRedirect -import com.intellij.ide.starter.exec.exec import com.intellij.ide.starter.ide.IDETestContext import com.intellij.ide.starter.path.GlobalPaths +import com.intellij.ide.starter.process.exec.ExecOutputRedirect +import com.intellij.ide.starter.process.exec.ProcessExecutor import com.intellij.ide.starter.system.SystemInfo import com.intellij.ide.starter.utils.FileSystem import com.intellij.ide.starter.utils.HttpClient @@ -71,32 +71,32 @@ fun downloadLatestAndroidSdk(javaHome: Path): Path { home.toFile().mkdirs() /// https://stackoverflow.com/questions/38096225/automatically-accept-all-sdk-licences /// sending "yes" to the process in the STDIN :( - exec(presentablePurpose = "android-sdk-licenses", - workDir = home, - environmentVariables = envVariablesWithJavaHome, - args = listOf(sdkManager.toString(), "--sdk_root=$home", "--licenses"), - stderrRedirect = ExecOutputRedirect.ToStdOut("[sdkmanager-err]"), - stdInBytes = "yes\n".repeat(10).toByteArray(), // it asks the confirmation at least two times - timeout = 15.minutes - ) + ProcessExecutor(presentableName = "android-sdk-licenses", + workDir = home, + environmentVariables = envVariablesWithJavaHome, + args = listOf(sdkManager.toString(), "--sdk_root=$home", "--licenses"), + stderrRedirect = ExecOutputRedirect.ToStdOut("[sdkmanager-err]"), + stdInBytes = "yes\n".repeat(10).toByteArray(), // it asks the confirmation at least two times + timeout = 15.minutes + ).start() //loading SDK - exec(presentablePurpose = "android-sdk-loading", - workDir = home, - environmentVariables = envVariablesWithJavaHome, - args = listOf(sdkManager.toString(), "--sdk_root=$home", "--list"), - stderrRedirect = ExecOutputRedirect.ToStdOut("[sdkmanager-err]"), - timeout = 15.minutes - ) + ProcessExecutor(presentableName = "android-sdk-loading", + workDir = home, + environmentVariables = envVariablesWithJavaHome, + args = listOf(sdkManager.toString(), "--sdk_root=$home", "--list"), + stderrRedirect = ExecOutputRedirect.ToStdOut("[sdkmanager-err]"), + timeout = 15.minutes + ).start() //loading SDK - exec(presentablePurpose = "android-sdk-installing", - workDir = home, - environmentVariables = envVariablesWithJavaHome, - args = listOf(sdkManager.toString(), "--sdk_root=$home", "--install", "--verbose") + packages, - stderrRedirect = ExecOutputRedirect.ToStdOut("[sdkmanager-err]"), - timeout = 15.minutes - ) + ProcessExecutor(presentableName = "android-sdk-installing", + workDir = home, + environmentVariables = envVariablesWithJavaHome, + args = listOf(sdkManager.toString(), "--sdk_root=$home", "--install", "--verbose") + packages, + stderrRedirect = ExecOutputRedirect.ToStdOut("[sdkmanager-err]"), + timeout = 15.minutes + ).start() return home } catch (t: Throwable) { @@ -148,13 +148,15 @@ fun IDETestContext.downloadAndroidPluginProject(): IDETestContext { val stdout = ExecOutputRedirect.ToString() val stderr = ExecOutputRedirect.ToString() - exec( + + ProcessExecutor( "git-clone-android-plugin", workDir = projectHome, timeout = 10.minutes, args = scriptContent.split(" "), stdoutRedirect = stdout, stderrRedirect = stderr - ) + ).start() + logOutput(stdout.read().trim()) } return this diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/bus/EventsReceiver.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/bus/EventsReceiver.kt index 058448905e0f..5a4eb0fb2451 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/bus/EventsReceiver.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/bus/EventsReceiver.kt @@ -2,7 +2,6 @@ package com.intellij.ide.starter.bus import com.intellij.ide.starter.utils.catchAll import kotlinx.coroutines.* -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.filterNotNull diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/bus/Extensions.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/bus/Extensions.kt index 754491fd754e..aa67d0e7594a 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/bus/Extensions.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/bus/Extensions.kt @@ -23,8 +23,9 @@ inline fun FlowBus.getFlow() = getFlow(T::class.java) * @param callback The callback function * @return This instance of [EventsReceiver] for chaining */ -inline fun EventsReceiver.subscribe(skipRetained: Boolean = false, noinline callback: suspend (event: T) -> Unit): EventsReceiver { - return subscribeTo(T::class.java, skipRetained, callback) +inline fun EventsReceiver.subscribe(skipRetained: Boolean = false, + noinline callback: suspend (event: T) -> Unit): EventsReceiver { + return subscribeTo(T::class.java, skipRetained, callback) } /** @@ -36,5 +37,5 @@ inline fun EventsReceiver.subscribe(skipRetained: Boolean = fa * @see [subscribe] */ inline fun EventsReceiver.subscribe(callback: EventCallback, skipRetained: Boolean = false): EventsReceiver { - return subscribeTo(T::class.java, callback, skipRetained) + return subscribeTo(T::class.java, callback, skipRetained) } \ No newline at end of file diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/bus/LICENSE b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/bus/LICENSE new file mode 100644 index 000000000000..8b8d15dffa84 --- /dev/null +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/bus/LICENSE @@ -0,0 +1,13 @@ +Copyright 2021 Robert Kosakowski + +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. \ No newline at end of file diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/community/ProductInfoRequestParameters.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/community/ProductInfoRequestParameters.kt index 123d3728b566..abae931d7745 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/community/ProductInfoRequestParameters.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/community/ProductInfoRequestParameters.kt @@ -3,22 +3,22 @@ package com.intellij.ide.starter.community import org.apache.http.client.utils.URIBuilder data class ProductInfoRequestParameters( - val code: String, - val type: String = "release", + val type: String, + val snapshot: String = "release", // e.g "2022.2" val majorVersion: String = "", // e.g "221.5591.52", - val build: String = "", + val buildNumber: String = "", // e.g "2022.1.1" - val version: String = "" + val versionNumber: String = "" ) { fun toUriQuery(): URIBuilder { val builder = URIBuilder() // API seems to filter only by code and type. It doesn't respond to majorVersion, build or version params - if (code.isNotBlank()) builder.addParameter("code", code) - if (type.isNotBlank()) builder.addParameter("type", type) + if (type.isNotBlank()) builder.addParameter("code", type) + if (snapshot.isNotBlank()) builder.addParameter("type", snapshot) return builder } diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/community/PublicIdeDownloader.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/community/PublicIdeDownloader.kt index 3aae8de8ae9c..1b9392fe43b2 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/community/PublicIdeDownloader.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/community/PublicIdeDownloader.kt @@ -21,19 +21,19 @@ object PublicIdeDownloader : IdeDownloader { if (filteringParams.majorVersion.isNotBlank()) return sorted.first { it.majorVersion == filteringParams.majorVersion } // find latest release / eap, if no specific params were provided - if (filteringParams.version.isBlank() && filteringParams.build.isBlank()) return sorted.first() + if (filteringParams.versionNumber.isBlank() && filteringParams.buildNumber.isBlank()) return sorted.first() - if (filteringParams.version.isNotBlank()) return sorted.first { it.version == filteringParams.version } - if (filteringParams.build.isNotBlank()) return sorted.first { it.build == filteringParams.build } + if (filteringParams.versionNumber.isNotBlank()) return sorted.first { it.version == filteringParams.versionNumber } + if (filteringParams.buildNumber.isNotBlank()) return sorted.first { it.build == filteringParams.buildNumber } throw NoSuchElementException("Couldn't find specified release by parameters $filteringParams") } override fun downloadIdeInstaller(ideInfo: IdeInfo, installerDirectory: Path): IdeInstaller { - val params = ProductInfoRequestParameters(code = ideInfo.productCode, - type = ideInfo.buildType, - build = ideInfo.buildNumber, - version = ideInfo.version) + val params = ProductInfoRequestParameters(type = ideInfo.productCode, + snapshot = ideInfo.buildType, + buildNumber = ideInfo.buildNumber, + versionNumber = ideInfo.version) val releaseInfoMap = JetBrainsDataServiceClient.getReleases(params) if (releaseInfoMap.size != 1) throw RuntimeException("Only one product can be downloaded at once. Found ${releaseInfoMap.keys}") diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/coroutine/CommonScope.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/coroutine/CommonScope.kt new file mode 100644 index 000000000000..fccb1d2baf93 --- /dev/null +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/coroutine/CommonScope.kt @@ -0,0 +1,9 @@ +package com.intellij.ide.starter.coroutine + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob + +val supervisorScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) +val simpleScope = CoroutineScope(Job() + Dispatchers.IO) \ No newline at end of file diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/di/diContainer.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/di/diContainer.kt index 44c8da68ea2c..85d9d4552228 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/di/diContainer.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/di/diContainer.kt @@ -16,6 +16,7 @@ import com.intellij.ide.starter.report.publisher.ReportPublisher import com.intellij.ide.starter.report.publisher.impl.ConsoleTestResultPublisher import com.intellij.ide.starter.report.publisher.impl.QodanaTestResultPublisher import com.intellij.ide.starter.runner.CodeBuilderHost +import com.intellij.ide.starter.runner.CurrentTestMethod import com.intellij.ide.starter.utils.logOutput import org.kodein.di.DI import org.kodein.di.bindFactory @@ -40,7 +41,7 @@ var di = DI { bindFactory { testContext: IDETestContext -> PluginConfigurator(testContext) } bindSingleton { PublicIdeDownloader } bindFactory { ideInfo -> - if (ideInfo.productCode == "AI") { + if (ideInfo.productCode == IdeProductProvider.AI.productCode) { AndroidInstaller() } else { @@ -50,6 +51,7 @@ var di = DI { bindFactory { testContext: IDETestContext -> BuildToolDefaultProvider(testContext) } bindSingleton> { listOf(ConsoleTestResultPublisher, QodanaTestResultPublisher) } bindSingleton { IdeProductImp } + bindSingleton { CurrentTestMethod } }.apply { logOutput("DI was initialized") } diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/exec/execUtil.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/exec/execUtil.kt deleted file mode 100644 index 804bcabbb39e..000000000000 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/exec/execUtil.kt +++ /dev/null @@ -1,269 +0,0 @@ -package com.intellij.ide.starter.exec - -import com.intellij.ide.starter.system.SystemInfo -import com.intellij.ide.starter.utils.catchAll -import com.intellij.ide.starter.utils.logError -import com.intellij.ide.starter.utils.logOutput -import java.io.IOException -import java.nio.file.Files -import java.nio.file.Path -import java.util.concurrent.TimeUnit -import kotlin.concurrent.thread -import kotlin.io.path.div -import kotlin.io.path.exists -import kotlin.io.path.readText -import kotlin.time.Duration -import kotlin.time.Duration.Companion.minutes -import kotlin.time.Duration.Companion.seconds - -private fun redirectProcessOutput( - process: Process, - outOrErrStream: Boolean, - redirectOutput: ExecOutputRedirect -): Thread { - val inputStream = if (outOrErrStream) process.inputStream else process.errorStream - return thread(start = true, isDaemon = true, name = "Redirect " + (if (outOrErrStream) "stdout" else "stderr")) { - val reader = inputStream.bufferedReader() - redirectOutput.open() - try { - while (true) { - val line = try { - reader.readLine() ?: break - } - catch (e: IOException) { - break - } - redirectOutput.redirectLine(line) - } - } - finally { - redirectOutput.close() - } - } -} - -private fun redirectProcessInput(process: Process, inputBytes: ByteArray): Thread? { - if (inputBytes.isEmpty()) { - catchAll { process.outputStream.close() } - return null - } - - return thread(start = true, isDaemon = true, name = "Redirect input") { - catchAll { - process.outputStream.use { - it.write(inputBytes) - } - } - } -} - -private fun ProcessBuilder.actualizeEnvVariables(environmentVariables: Map = System.getenv(), - onlyEnrichExistedEnvVariables: Boolean = false): ProcessBuilder { - val processEnvironment = environment() - if (processEnvironment == environmentVariables) return this - - environmentVariables.filter { it.value == null }.forEach { logError("Env variable: ${it.key} has null value ${it.value}") } - val notNullValues = environmentVariables.filter { it.value != null } - - // env variables enrichment - processEnvironment.putAll(notNullValues) - - if (!onlyEnrichExistedEnvVariables) { - val missingKeys = processEnvironment.keys - notNullValues.keys - missingKeys.forEach { key -> processEnvironment.remove(key) } - } - - return this -} - -/** - * Creates new process and wait for it's completion - */ -@Throws(ExecTimeoutException::class) -fun exec( - presentablePurpose: String, - workDir: Path?, - timeout: Duration = 10.minutes, - environmentVariables: Map = System.getenv(), - args: List, - errorDiagnosticFiles: List = emptyList(), - stdoutRedirect: ExecOutputRedirect = ExecOutputRedirect.NoRedirect, - stderrRedirect: ExecOutputRedirect = ExecOutputRedirect.NoRedirect, - onProcessCreated: (Process, Long) -> Unit = { _, _ -> }, - onBeforeKilled: (Process, Long) -> Unit = { _, _ -> }, - stdInBytes: ByteArray = byteArrayOf(), - onlyEnrichExistedEnvVariables: Boolean = false -) { - logOutput(buildString { - appendLine("Running external process for `$presentablePurpose`") - appendLine(" Working directory: $workDir") - appendLine(" Arguments: [${args.joinToString()}]") - appendLine(" STDOUT will be redirected to: $stdoutRedirect") - appendLine(" STDERR will be redirected to: $stderrRedirect") - append(" STDIN is empty: " + stdInBytes.isEmpty()) - }) - - require(args.isNotEmpty()) { "Arguments must be not empty to start external process" } - - val processBuilder = ProcessBuilder() - .directory(workDir?.toFile()) - .command(*args.toTypedArray()) - .redirectInput(ProcessBuilder.Redirect.PIPE) - .redirectOutput(ProcessBuilder.Redirect.PIPE) - .redirectError(ProcessBuilder.Redirect.PIPE) - .actualizeEnvVariables(environmentVariables, onlyEnrichExistedEnvVariables) - - logOutput( - """ - Process: `$presentablePurpose` - Arguments: ${args.joinToString(separator = " ")} - Environment variables: [${processBuilder.environment().entries.joinToString { "${it.key}=${it.value}" }}] - """.trimIndent()) - val process = processBuilder.start() - - val processId = process.pid() - catchAll { - logOutput(" ... started external process `$presentablePurpose` with process ID = $processId") - onProcessCreated(process, processId) - } - - val inputThread = redirectProcessInput(process, stdInBytes) - val stdoutThread = redirectProcessOutput(process, true, stdoutRedirect) - val stderrThread = redirectProcessOutput(process, false, stderrRedirect) - val threads = listOfNotNull(inputThread, stdoutThread, stderrThread) - - fun killProcess() { - catchAll { onBeforeKilled(process, processId) } - catchAll { process.descendants().forEach { catchAll { it.destroyForcibly() } } } - catchAll { process.destroy() } - catchAll { process.destroyForcibly() } - catchAll { threads.forEach { it.interrupt() } } - } - - val stopper = Runnable { - logOutput( - " ... terminating process `$presentablePurpose` by request from external process (either SIGTERM or SIGKILL is caught) ...") - killProcess() - } - - val stopperThread = Thread(stopper, "process-shutdown-hook") - try { - Runtime.getRuntime().addShutdownHook(stopperThread) - } - catch (e: IllegalStateException) { - logError("Process: $presentablePurpose. Shutdown hook cannot be added because: ${e.message}") - } - - try { - if (!runCatching { process.waitFor(timeout.inWholeSeconds, TimeUnit.SECONDS) }.getOrDefault(false)) { - stopperThread.apply { - start() - join(20.seconds.inWholeMilliseconds) - } - throw ExecTimeoutException(args.joinToString(" "), timeout) - } - } - finally { - catchAll { Runtime.getRuntime().removeShutdownHook(stopperThread) } - } - - threads.forEach { catchAll { it.join() } } - - val code = process.exitValue() - if (code != 0) { - val linesLimit = 100 - - logOutput(" ... failed external process `$presentablePurpose` with exit code $code") - val message = buildString { - appendLine("External process `$presentablePurpose` failed with code $code") - for (diagnosticFile in errorDiagnosticFiles.filter { it.exists() && Files.size(it) > 0 }) { - appendLine(diagnosticFile.fileName.toString()) - appendLine(diagnosticFile.readText().lines().joinToString(System.lineSeparator()) { " $it" }) - } - - stderrRedirect.read().lines().apply { - take(linesLimit).dropWhile { it.trim().isBlank() }.let { lines -> - if (lines.isNotEmpty()) { - appendLine(" FIRST $linesLimit lines of the standard error stream") - lines.forEach { appendLine(" $it") } - } - } - - if (size > linesLimit) { - appendLine("...") - - takeLast(linesLimit).dropWhile { it.trim().isBlank() }.let { lines -> - if (lines.isNotEmpty()) { - appendLine(" LAST $linesLimit lines of the standard error stream") - lines.forEach { appendLine(" $it") } - } - } - } - } - - stdoutRedirect.read().lines().takeLast(linesLimit).dropWhile { it.trim().isEmpty() }.let { lines -> - if (lines.isNotEmpty()) { - appendLine(" LAST $linesLimit lines of the standard output stream") - lines.forEach { appendLine(" $it") } - } - } - } - error(message) - } - logOutput(" ... successfully finished external process for `$presentablePurpose` with exit code 0") -} - -class ExecTimeoutException(private val processName: String, - private val timeout: Duration) : RuntimeException() { - override val message - get() = "Failed to wait for the process `$processName` to complete in $timeout" -} - -fun executeScript(fileNameToExecute: String, projectDirPath: Path) { - val stdout = ExecOutputRedirect.ToString() - val stderr = ExecOutputRedirect.ToString() - - exec( - presentablePurpose = "Executing of $fileNameToExecute", - workDir = projectDirPath, - timeout = 20.minutes, - args = listOf(fileNameToExecute), - stdoutRedirect = stdout, - stderrRedirect = stderr - ) - - val commit = stdout.read().trim() - val error = stderr.read().trim() - - logOutput("Stdout of command execution $commit") - logOutput("Stderr of command execution $error") -} - -fun execGradlew(pathToProject: Path, args: List) { - val stdout = ExecOutputRedirect.ToString() - val stderr = ExecOutputRedirect.ToString() - - val command = when (SystemInfo.isWindows) { - true -> (pathToProject / "gradlew.bat").toString() - false -> "./gradlew" - } - - if (!SystemInfo.isWindows) { - exec( - presentablePurpose = "chmod gradlew", - workDir = pathToProject, - timeout = 1.minutes, - args = listOf("chmod", "+x", "gradlew"), - stdoutRedirect = stdout, - stderrRedirect = stderr - ) - } - exec( - presentablePurpose = "Gradle Format", - workDir = pathToProject, - timeout = 1.minutes, - args = listOf(command) + args, - stdoutRedirect = stdout, - stderrRedirect = stderr - ) -} diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/ide/BuildNumber.java b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/ide/BuildNumber.java deleted file mode 100644 index 0ab6a414d800..000000000000 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/ide/BuildNumber.java +++ /dev/null @@ -1,235 +0,0 @@ -package com.intellij.ide.starter.ide; - -import java.util.Arrays; - -// code has been borrowed from Intellij Community sources -// Some parts, that requires dependency on Community, were removed -public class BuildNumber implements Comparable { - private static final String STAR = "*"; - private static final String SNAPSHOT = "SNAPSHOT"; - private static final String FALLBACK_VERSION = "999.SNAPSHOT"; - - public static final int SNAPSHOT_VALUE = Integer.MAX_VALUE; - - private final String myProductCode; - private final int[] myComponents; - - public BuildNumber(String productCode, int baselineVersion, int buildNumber) { - this(productCode, new int[]{baselineVersion, buildNumber}); - } - - public BuildNumber(String productCode, int... components) { - myProductCode = productCode; - myComponents = components; - } - - private static boolean isPlaceholder(String value) { - return "__BUILD_NUMBER__".equals(value) || "__BUILD__".equals(value); - } - - public String getProductCode() { - return myProductCode; - } - - public int getBaselineVersion() { - return myComponents[0]; - } - - public int[] getComponents() { - return myComponents.clone(); - } - - public boolean isSnapshot() { - for (int value : myComponents) { - if (value == SNAPSHOT_VALUE) { - return true; - } - } - return false; - } - - public BuildNumber withoutProductCode() { - return myProductCode.isEmpty() ? this : new BuildNumber("", myComponents); - } - - public String asString() { - return asString(true, true); - } - - public String asStringWithoutProductCode() { - return asString(false, true); - } - - public String asStringWithoutProductCodeAndSnapshot() { - return asString(false, false); - } - - private String asString(boolean includeProductCode, boolean withSnapshotMarker) { - StringBuilder builder = new StringBuilder(); - - if (includeProductCode && !myProductCode.isEmpty()) { - builder.append(myProductCode).append('-'); - } - - for (int each : myComponents) { - if (each != SNAPSHOT_VALUE) { - builder.append(each); - } - else if (withSnapshotMarker) { - builder.append(SNAPSHOT); - } - builder.append('.'); - } - if (builder.charAt(builder.length() - 1) == '.') { - builder.setLength(builder.length() - 1); - } - return builder.toString(); - } - - public static BuildNumber fromPluginsCompatibleBuild() { - return fromString(getPluginsCompatibleBuild()); - } - - /** - * Attempts to parse build number from the specified string. - * Returns {@code null} if the string is not a valid build number. - */ - public static BuildNumber fromStringOrNull(String version) { - try { - return fromString(version); - } - catch (RuntimeException ignored) { - return null; - } - } - - public static BuildNumber fromString(String version) { - if (version == null) { - return null; - } - version = version.trim(); - return fromString(version, null, null); - } - - public static BuildNumber fromStringWithProductCode(String version, String productCode) { - return fromString(version, null, productCode); - } - - public static BuildNumber fromString(String version, String pluginName, String productCodeIfAbsentInVersion) { - if (version.isEmpty()) return null; - String code = version; - int productSeparator = code.indexOf('-'); - String productCode; - if (productSeparator > 0) { - productCode = code.substring(0, productSeparator); - code = code.substring(productSeparator + 1); - } - else { - productCode = productCodeIfAbsentInVersion != null ? productCodeIfAbsentInVersion : ""; - } - - int baselineVersionSeparator = code.indexOf('.'); - - if (baselineVersionSeparator > 0) { - String baselineVersionString = code.substring(0, baselineVersionSeparator); - if (baselineVersionString.trim().isEmpty()) { - return null; - } - - String[] stringComponents = code.split("\\."); - int[] intComponentsList = new int[stringComponents.length]; - for (int i = 0, n = stringComponents.length; i < n; i++) { - String stringComponent = stringComponents[i]; - int comp = parseBuildNumber(version, stringComponent, pluginName); - intComponentsList[i] = comp; - if (comp == SNAPSHOT_VALUE && (i + 1) != n) { - intComponentsList = Arrays.copyOf(intComponentsList, i + 1); - break; - } - } - return new BuildNumber(productCode, intComponentsList); - } - else { - int buildNumber = parseBuildNumber(version, code, pluginName); - if (buildNumber <= 2000) { - // it's probably a baseline, not a build number - return new BuildNumber(productCode, buildNumber, 0); - } - - int baselineVersion = getBaseLineForHistoricBuilds(buildNumber); - return new BuildNumber(productCode, baselineVersion, buildNumber); - } - } - - private static int parseBuildNumber(String version, String code, String pluginName) { - if (SNAPSHOT.equals(code) || isPlaceholder(code) || STAR.equals(code)) { - return SNAPSHOT_VALUE; - } - - try { - return Integer.parseInt(code); - } - catch (NumberFormatException e) { - throw new RuntimeException("Invalid version number: " + version + "; plugin name: " + pluginName); - } - } - - @Override - public int compareTo(BuildNumber o) { - int[] c1 = myComponents; - int[] c2 = o.myComponents; - - for (int i = 0; i < Math.min(c1.length, c2.length); i++) { - if (c1[i] == c2[i] && c1[i] == SNAPSHOT_VALUE) return 0; - if (c1[i] == SNAPSHOT_VALUE) return 1; - if (c2[i] == SNAPSHOT_VALUE) return -1; - int result = c1[i] - c2[i]; - if (result != 0) return result; - } - - return c1.length - c2.length; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - BuildNumber that = (BuildNumber)o; - - if (!myProductCode.equals(that.myProductCode)) return false; - return Arrays.equals(myComponents, that.myComponents); - } - - @Override - public int hashCode() { - int result = myProductCode.hashCode(); - result = 31 * result + Arrays.hashCode(myComponents); - return result; - } - - @Override - public String toString() { - return asString(); - } - - // http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/build_number_ranges.html - private static int getBaseLineForHistoricBuilds(int bn) { - if (bn >= 10000) return 88; // Maia, 9x builds - if (bn >= 9500) return 85; // 8.1 builds - if (bn >= 9100) return 81; // 8.0.x builds - if (bn >= 8000) return 80; // 8.0, including pre-release builds - if (bn >= 7500) return 75; // 7.0.2+ - if (bn >= 7200) return 72; // 7.0 final - if (bn >= 6900) return 69; // 7.0 pre-M2 - if (bn >= 6500) return 65; // 7.0 pre-M1 - if (bn >= 6000) return 60; // 6.0.2+ - if (bn >= 5000) return 55; // 6.0 branch, including all 6.0 EAP builds - if (bn >= 4000) return 50; // 5.1 branch - return 40; - } - - private static String getPluginsCompatibleBuild() { - return System.getProperty("idea.plugins.compatible.build"); - } -} diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/ide/IDETestContext.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/ide/IDETestContext.kt index 72c373f3b576..57459b6bc371 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/ide/IDETestContext.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/ide/IDETestContext.kt @@ -5,7 +5,10 @@ import com.intellij.ide.starter.ci.CIServer import com.intellij.ide.starter.di.di import com.intellij.ide.starter.ide.command.CommandChain import com.intellij.ide.starter.ide.command.MarshallableCommand -import com.intellij.ide.starter.models.* +import com.intellij.ide.starter.models.IDEStartResult +import com.intellij.ide.starter.models.TestCase +import com.intellij.ide.starter.models.VMOptions +import com.intellij.ide.starter.models.andThen import com.intellij.ide.starter.path.IDEDataPaths import com.intellij.ide.starter.plugins.PluginConfigurator import com.intellij.ide.starter.profiler.ProfilerType @@ -21,6 +24,7 @@ import org.kodein.di.newInstance import org.w3c.dom.Node import org.w3c.dom.NodeList import java.io.FileOutputStream +import java.nio.file.Files import java.nio.file.Path import javax.xml.parsers.DocumentBuilderFactory import javax.xml.transform.TransformerFactory @@ -202,6 +206,14 @@ data class IDETestContext( paths.logsDir.toFile().deleteRecursively() } + fun wipeReportDir() = apply { + logOutput("Cleaning report dir for $this at $paths") + Files.walk(paths.reportsDir) + .filter { Files.isRegularFile(it) } + .map { it.toFile() } + .forEach { it.delete() } + } + fun wipeProjectsDir() = apply { val path = paths.systemDir / "projects" logOutput("Cleaning project cache dir for $this at $path") @@ -336,19 +348,19 @@ data class IDETestContext( ): IDEStartResult { val updatedContext = this.copy(testName = "${this.testName}/warmup") val result = updatedContext.runIDE( - patchVMOptions = { - val warmupReports = IDEStartupReports(paths.reportsDir) + patchVMOptions = { + this.run { if (storeClassReport) { - this.enableStartupPerformanceLog(warmupReports).enableClassLoadingReport( - paths.reportsDir / "class-report.txt").patchVMOptions() + this.enableClassLoadingReport(paths.reportsDir / "class-report.txt") } else { this } - }, - commands = testCase.commands.plus(commands), - runTimeout = runTimeout - ) + }.patchVMOptions() + }, + commands = testCase.commands.plus(commands), + runTimeout = runTimeout + ) updatedContext.publishArtifact(this.paths.reportsDir) return result } diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/ide/IdeArchiveExtractor.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/ide/IdeArchiveExtractor.kt index 82347136c79b..5d87b5ce4ade 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/ide/IdeArchiveExtractor.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/ide/IdeArchiveExtractor.kt @@ -1,9 +1,9 @@ package com.intellij.ide.starter.ide import com.intellij.ide.starter.di.di -import com.intellij.ide.starter.exec.ExecOutputRedirect -import com.intellij.ide.starter.exec.exec import com.intellij.ide.starter.path.GlobalPaths +import com.intellij.ide.starter.process.exec.ExecOutputRedirect +import com.intellij.ide.starter.process.exec.ProcessExecutor import com.intellij.ide.starter.utils.FileSystem import com.intellij.ide.starter.utils.HttpClient import com.intellij.ide.starter.utils.catchAll @@ -41,13 +41,14 @@ object IdeArchiveExtractor { val mountDir = File(dmgFile.path + "-mount${System.currentTimeMillis()}") try { - exec(presentablePurpose = "hdiutil", - workDir = target, - timeout = 10.minutes, - stderrRedirect = ExecOutputRedirect.ToStdOut("hdiutil"), - stdoutRedirect = ExecOutputRedirect.ToStdOut("hdiutil"), - args = listOf("hdiutil", "attach", "-readonly", "-noautoopen", "-noautofsck", "-nobrowse", "-mountpoint", "$mountDir", - "$dmgFile")) + ProcessExecutor(presentableName = "hdiutil", + workDir = target, + timeout = 10.minutes, + stderrRedirect = ExecOutputRedirect.ToStdOut("hdiutil"), + stdoutRedirect = ExecOutputRedirect.ToStdOut("hdiutil"), + args = listOf("hdiutil", "attach", "-readonly", "-noautoopen", "-noautofsck", "-nobrowse", "-mountpoint", "$mountDir", + "$dmgFile") + ).start() } catch (t: Throwable) { dmgFile.delete() @@ -59,24 +60,26 @@ object IdeArchiveExtractor { ?: error("Failed to find the only one .app folder in $dmgFile") val targetAppDir = target / appDir.name - exec( - presentablePurpose = "copy-dmg", + ProcessExecutor( + presentableName = "copy-dmg", workDir = target, timeout = 10.minutes, stderrRedirect = ExecOutputRedirect.ToStdOut("cp"), - args = listOf("cp", "-R", "$appDir", "$targetAppDir")) + args = listOf("cp", "-R", "$appDir", "$targetAppDir") + ).start() return targetAppDir } finally { catchAll { - exec( - presentablePurpose = "hdiutil", + ProcessExecutor( + presentableName = "hdiutil", workDir = target, timeout = 10.minutes, stdoutRedirect = ExecOutputRedirect.ToStdOut("hdiutil"), stderrRedirect = ExecOutputRedirect.ToStdOut("hdiutil"), - args = listOf("hdiutil", "detach", "-force", "$mountDir")) + args = listOf("hdiutil", "detach", "-force", "$mountDir") + ).start() } } } @@ -97,11 +100,11 @@ object IdeArchiveExtractor { val severZipToolExe = sevenZipTool.resolve("7z.exe") targetDir.mkdirs() - exec( - presentablePurpose = "unpack-zip", + ProcessExecutor( + presentableName = "unpack-zip", workDir = targetDir.toPath(), timeout = 10.minutes, args = listOf(severZipToolExe.toAbsolutePath().toString(), "x", "-y", "-o$targetDir", exeFile.path) - ) + ).start() } } \ No newline at end of file diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/ide/IdeInstallator.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/ide/IdeInstallator.kt index 156d0b4e8338..5609f758c7f8 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/ide/IdeInstallator.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/ide/IdeInstallator.kt @@ -3,5 +3,8 @@ package com.intellij.ide.starter.ide import com.intellij.ide.starter.models.IdeInfo interface IdeInstallator { + /** + * @return + */ fun install(ideInfo: IdeInfo): Pair } \ No newline at end of file diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/ide/IdeProductProvider.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/ide/IdeProductProvider.kt index 2ef5b631d6a3..126d3378dadf 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/ide/IdeProductProvider.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/ide/IdeProductProvider.kt @@ -35,5 +35,8 @@ object IdeProductProvider { /** PyCharm Professional */ val PY: IdeInfo = di.direct.instance().PY + /** CLion */ + val CL: IdeInfo = di.direct.instance().CL + fun getProducts(): List = IdeProductProvider::class.declaredMemberProperties.map { it.get(IdeProductProvider) as IdeInfo } } \ No newline at end of file diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/ide/LinuxIdeDistribution.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/ide/LinuxIdeDistribution.kt index 917436c24571..a15206e14265 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/ide/LinuxIdeDistribution.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/ide/LinuxIdeDistribution.kt @@ -1,8 +1,8 @@ package com.intellij.ide.starter.ide -import com.intellij.ide.starter.exec.ExecOutputRedirect -import com.intellij.ide.starter.exec.exec import com.intellij.ide.starter.models.VMOptions +import com.intellij.ide.starter.process.exec.ExecOutputRedirect +import com.intellij.ide.starter.process.exec.ProcessExecutor import com.intellij.ide.starter.system.SystemInfo import com.intellij.ide.starter.utils.callJavaVersion import com.intellij.ide.starter.utils.logOutput @@ -18,9 +18,10 @@ class LinuxIdeDistribution : IdeDistribution() { val toolName = "xvfb-run" val homePath = Path(System.getProperty("user.home")).toAbsolutePath() - exec("xvfb-run", homePath, timeout = 5.seconds, args = listOf("which", toolName), - stdoutRedirect = ExecOutputRedirect.ToStdOut("xvfb-run-out"), - stderrRedirect = ExecOutputRedirect.ToStdOut("xvfb-run-err")) + ProcessExecutor("xvfb-run", homePath, timeout = 5.seconds, args = listOf("which", toolName), + stdoutRedirect = ExecOutputRedirect.ToStdOut("xvfb-run-out"), + stderrRedirect = ExecOutputRedirect.ToStdOut("xvfb-run-err") + ).start() toolName } diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/models/IdeProduct.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/models/IdeProduct.kt index 30b4956e2474..80d3f6fe785f 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/models/IdeProduct.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/models/IdeProduct.kt @@ -27,4 +27,7 @@ interface IdeProduct { /** PyCharm Professional */ val PY: IdeInfo + + /** CLion */ + val CL: IdeInfo } \ No newline at end of file diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/models/IdeProductImp.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/models/IdeProductImp.kt index 5e06e880d5b0..57ec1ef3d602 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/models/IdeProductImp.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/models/IdeProductImp.kt @@ -63,4 +63,11 @@ object IdeProductImp : IdeProduct { platformPrefix = "Python", executableFileName = "pycharm" ) + + /** CLion */ + override val CL: IdeInfo = IdeInfo( + productCode = "CL", + platformPrefix = "CLion", + executableFileName = "clion" + ) } \ No newline at end of file diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/models/TestCase.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/models/TestCase.kt index b1a8a7bac68b..05d9c6b01288 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/models/TestCase.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/models/TestCase.kt @@ -16,13 +16,10 @@ data class TestCase( fun withCommands(commands: Iterable = this.commands): TestCase = copy(commands = commands.toList()) - /** Project in this case will be reused between tests */ - fun markNotReusable(): TestCase = markReusable(false) - /** On each test run the project will be unpacked again. * This guarantees that there is not side effects from previous test runs **/ - fun markReusable(isReusable: Boolean = true) = copy(projectInfo = (projectInfo as ProjectInfo).copy(isReusable = isReusable)) + fun markNotReusable(): TestCase = copy(projectInfo = (projectInfo as ProjectInfo).copy(isReusable = false)) /** * [buildNumber] - EAP build number to download diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/models/VMOptions.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/models/VMOptions.kt index 2ea2bcced2fb..4adc8f548db3 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/models/VMOptions.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/models/VMOptions.kt @@ -108,6 +108,16 @@ data class VMOptions( .addSystemProperty("idea.record.classpath.info", "true") } + fun enableVmtraceClassLoadingReport(filePath: Path): VMOptions { + if (!VMTrace.isSupported) return this + + val vmTraceFile = VMTrace.vmTraceFile + + return this + .addSystemProperty("idea.log.vmtrace.file", filePath) + .addLine("-agentpath:${vmTraceFile.toAbsolutePath()}=${filePath.toAbsolutePath()}") + } + fun configureLoggers( debugLoggers: List = emptyList(), traceLoggers: List = emptyList() diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/models/VMTrace.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/models/VMTrace.kt new file mode 100644 index 000000000000..ba61fe1679dc --- /dev/null +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/models/VMTrace.kt @@ -0,0 +1,36 @@ +package com.intellij.ide.starter.models + +import com.intellij.ide.starter.system.SystemInfo +import com.intellij.openapi.util.io.FileUtilRt +import java.nio.file.Files +import java.nio.file.Path + +/** + * Holds path to libvmtrace.so on disk. + */ +object VMTrace { + val vmTraceFile: Path + + val isSupported: Boolean + get() = SystemInfo.isLinux || SystemInfo.isMac + + init { + if (isSupported) { + val resourceName = when { + SystemInfo.isLinux -> "/libvmtrace.so" + SystemInfo.isMac && !SystemInfo.isAarch64 -> "/libvmtrace.dylib" + SystemInfo.isMac && SystemInfo.isAarch64 -> "/libvmtrace-aarch64.dylib" + else -> throw UnsupportedOperationException("Unsupported platform for libvmtrace") + } + + vmTraceFile = Files.createTempFile("libvmtrace", "." + FileUtilRt.getExtension(resourceName)) + + val vmTraceBytes = VMOptions::class.java.getResourceAsStream(resourceName)!! + .use { it.readAllBytes() } + Files.write(vmTraceFile, vmTraceBytes) + } + else { + vmTraceFile = Path.of("unsupported-platform-libvmtrace") + } + } +} \ No newline at end of file diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/path/GlobalPaths.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/path/GlobalPaths.kt index dbbe2185c142..4df318ac1f8f 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/path/GlobalPaths.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/path/GlobalPaths.kt @@ -25,14 +25,14 @@ abstract class GlobalPaths(val checkoutDir: Path) { false -> intelliJOutDirectory // Local run } - open val testHomePath:Path = intelliJOutDirectory.resolve("perf-startup").createDirectories() + open val testHomePath: Path = intelliJOutDirectory.resolve("perf-startup").createDirectories() val installersDirectory = (testHomePath / "installers").createDirectories() val testsDirectory = (testHomePath / "tests").createDirectories() private val cacheDirectory: Path = if (di.direct.instance().isBuildRunningOnCI && - !System.getProperty("agent.persistent.cache").isNullOrEmpty() + !System.getProperty("agent.persistent.cache").isNullOrEmpty() ) { (Paths.get(System.getProperty("agent.persistent.cache"), "perf-tests-cache")).createDirectories() } diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/plugins/PluginConfigurator.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/plugins/PluginConfigurator.kt index e465aa8ab5a5..5f8cfbd0ab28 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/plugins/PluginConfigurator.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/plugins/PluginConfigurator.kt @@ -60,7 +60,8 @@ open class PluginConfigurator(val testContext: IDETestContext) { logError("Plugin $pluginId downloading failed, skipping") return@apply } - } else { + } + else { FileSystem.unpack(downloadedPlugin, testContext.paths.pluginsDir) } logOutput("Plugin $pluginId setup finished") diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/exec/ExecOutputRedirect.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/process/exec/ExecOutputRedirect.kt similarity index 95% rename from tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/exec/ExecOutputRedirect.kt rename to tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/process/exec/ExecOutputRedirect.kt index 56710095106a..28a86e01c8de 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/exec/ExecOutputRedirect.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/process/exec/ExecOutputRedirect.kt @@ -1,6 +1,6 @@ -package com.intellij.ide.starter.exec +package com.intellij.ide.starter.process.exec -import com.intellij.ide.starter.exec.ExecOutputRedirect.* +import com.intellij.ide.starter.process.exec.ExecOutputRedirect.* import com.intellij.ide.starter.utils.logOutput import java.io.File import java.io.PrintWriter diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/process/exec/ExecTimeoutException.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/process/exec/ExecTimeoutException.kt new file mode 100644 index 000000000000..3bada487a59d --- /dev/null +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/process/exec/ExecTimeoutException.kt @@ -0,0 +1,9 @@ +package com.intellij.ide.starter.process.exec + +import kotlin.time.Duration + +class ExecTimeoutException(private val processName: String, + private val timeout: Duration) : RuntimeException() { + override val message + get() = "Failed to wait for the process `$processName` to complete in $timeout" +} \ No newline at end of file diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/process/exec/ProcessExecutor.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/process/exec/ProcessExecutor.kt new file mode 100644 index 000000000000..b5197adefdd3 --- /dev/null +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/process/exec/ProcessExecutor.kt @@ -0,0 +1,228 @@ +package com.intellij.ide.starter.process.exec + +import com.intellij.ide.starter.coroutine.supervisorScope +import com.intellij.ide.starter.utils.catchAll +import com.intellij.ide.starter.utils.logError +import com.intellij.ide.starter.utils.logOutput +import kotlinx.coroutines.* +import kotlinx.coroutines.future.await +import java.io.IOException +import java.lang.Runnable +import java.nio.file.Files +import java.nio.file.Path +import java.util.concurrent.TimeUnit +import kotlin.concurrent.thread +import kotlin.io.path.exists +import kotlin.io.path.readText +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds + +class ProcessExecutor(val presentableName: String, + val workDir: Path?, + val timeout: Duration = 10.minutes, + val environmentVariables: Map = System.getenv(), + val args: List, + val errorDiagnosticFiles: List = emptyList(), + val stdoutRedirect: ExecOutputRedirect = ExecOutputRedirect.NoRedirect, + val stderrRedirect: ExecOutputRedirect = ExecOutputRedirect.NoRedirect, + val onProcessCreated: suspend (Process, Long) -> Unit = { _, _ -> }, + val onBeforeKilled: suspend (Process, Long) -> Unit = { _, _ -> }, + val stdInBytes: ByteArray = byteArrayOf(), + val onlyEnrichExistedEnvVariables: Boolean = false) { + + private fun redirectProcessOutput( + process: Process, + outOrErrStream: Boolean, + redirectOutput: ExecOutputRedirect + ): Thread { + val inputStream = if (outOrErrStream) process.inputStream else process.errorStream + return thread(start = true, isDaemon = true, name = "Redirect " + (if (outOrErrStream) "stdout" else "stderr")) { + val reader = inputStream.bufferedReader() + redirectOutput.open() + try { + while (true) { + val line = try { + reader.readLine() ?: break + } + catch (e: IOException) { + break + } + redirectOutput.redirectLine(line) + } + } + finally { + redirectOutput.close() + } + } + } + + private fun redirectProcessInput(process: Process, inputBytes: ByteArray): Thread? { + if (inputBytes.isEmpty()) { + catchAll { process.outputStream.close() } + return null + } + + return thread(start = true, isDaemon = true, name = "Redirect input") { + catchAll { + process.outputStream.use { + it.write(inputBytes) + } + } + } + } + + private fun ProcessBuilder.actualizeEnvVariables(environmentVariables: Map = System.getenv(), + onlyEnrichExistedEnvVariables: Boolean = false): ProcessBuilder { + val processEnvironment = environment() + if (processEnvironment == environmentVariables) return this + + environmentVariables.filter { it.value == null }.forEach { logError("Env variable: ${it.key} has null value ${it.value}") } + val notNullValues = environmentVariables.filter { it.value != null } + + // env variables enrichment + processEnvironment.putAll(notNullValues) + + if (!onlyEnrichExistedEnvVariables) { + val missingKeys = processEnvironment.keys - notNullValues.keys + missingKeys.forEach { key -> processEnvironment.remove(key) } + } + + return this + } + + private fun killProcessGracefully(process: ProcessHandle) { + process.destroy() + runBlocking { withTimeout(20.seconds) { process.onExit().await() } } + process.destroyForcibly() + } + + private fun analyzeProcessExit(process: Process) { + val code = process.exitValue() + if (code != 0) { + val linesLimit = 100 + + logOutput(" ... failed external process `$presentableName` with exit code $code") + val message = buildString { + appendLine("External process `$presentableName` failed with code $code") + for (diagnosticFile in errorDiagnosticFiles.filter { it.exists() && Files.size(it) > 0 }) { + appendLine(diagnosticFile.fileName.toString()) + appendLine(diagnosticFile.readText().lines().joinToString(System.lineSeparator()) { " $it" }) + } + + stderrRedirect.read().lines().apply { + take(linesLimit).dropWhile { it.trim().isBlank() }.let { lines -> + if (lines.isNotEmpty()) { + appendLine(" FIRST $linesLimit lines of the standard error stream") + lines.forEach { appendLine(" $it") } + } + } + + if (size > linesLimit) { + appendLine("...") + + takeLast(linesLimit).dropWhile { it.trim().isBlank() }.let { lines -> + if (lines.isNotEmpty()) { + appendLine(" LAST $linesLimit lines of the standard error stream") + lines.forEach { appendLine(" $it") } + } + } + } + } + + stdoutRedirect.read().lines().takeLast(linesLimit).dropWhile { it.trim().isEmpty() }.let { lines -> + if (lines.isNotEmpty()) { + appendLine(" LAST $linesLimit lines of the standard output stream") + lines.forEach { appendLine(" $it") } + } + } + } + error(message) + } + + logOutput(" ... successfully finished external process for `$presentableName` with exit code 0") + } + + /** + * Creates new process and wait for it's completion + */ + @Throws(ExecTimeoutException::class) + fun start() { + logOutput(buildString { + appendLine("Running external process for `$presentableName`") + appendLine(" Working directory: $workDir") + appendLine(" Arguments: [${args.joinToString()}]") + appendLine(" STDOUT will be redirected to: $stdoutRedirect") + appendLine(" STDERR will be redirected to: $stderrRedirect") + append(" STDIN is empty: " + stdInBytes.isEmpty()) + }) + + require(args.isNotEmpty()) { "Arguments must be not empty to start external process" } + + val processBuilder = ProcessBuilder() + .directory(workDir?.toFile()) + .command(*args.toTypedArray()) + .redirectInput(ProcessBuilder.Redirect.PIPE) + .redirectOutput(ProcessBuilder.Redirect.PIPE) + .redirectError(ProcessBuilder.Redirect.PIPE) + .actualizeEnvVariables(environmentVariables, onlyEnrichExistedEnvVariables) + + logOutput( + """ + Process: `$presentableName` + Arguments: ${args.joinToString(separator = " ")} + Environment variables: [${processBuilder.environment().entries.joinToString { "${it.key}=${it.value}" }}] + """.trimIndent()) + val process = processBuilder.start() + + val processId = process.pid() + val onProcessCreatedJob: Job = supervisorScope.launch { + logOutput(" ... started external process `$presentableName` with process ID = $processId") + onProcessCreated(process, processId) + } + + val inputThread = redirectProcessInput(process, stdInBytes) + val stdoutThread = redirectProcessOutput(process, true, stdoutRedirect) + val stderrThread = redirectProcessOutput(process, false, stderrRedirect) + val ioThreads = listOfNotNull(inputThread, stdoutThread, stderrThread) + + fun killProcess() { + catchAll { runBlocking { onProcessCreatedJob.cancelAndJoin() } } + catchAll { runBlocking { withTimeout(1.minutes) { onBeforeKilled(process, processId) } } } + process.descendants().forEach { catchAll { killProcessGracefully(it) } } + catchAll { killProcessGracefully(process.toHandle()) } + catchAll { ioThreads.forEach { it.interrupt() } } + } + + val stopper = Runnable { + logOutput( + " ... terminating process `$presentableName` by request from external process (either SIGTERM or SIGKILL is caught) ...") + killProcess() + } + + val stopperThread = Thread(stopper, "process-shutdown-hook") + try { + Runtime.getRuntime().addShutdownHook(stopperThread) + } + catch (e: IllegalStateException) { + logError("Process: $presentableName. Shutdown hook cannot be added because: ${e.message}") + } + + try { + if (!runCatching { process.waitFor(timeout.inWholeSeconds, TimeUnit.SECONDS) }.getOrDefault(false)) { + stopperThread.apply { + start() + join(20.seconds.inWholeMilliseconds) + } + throw ExecTimeoutException(args.joinToString(" "), timeout) + } + } + finally { + catchAll { Runtime.getRuntime().removeShutdownHook(stopperThread) } + } + + ioThreads.forEach { catchAll { it.join() } } + + analyzeProcessExit(process) + } +} \ No newline at end of file diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/process/exec/execUtil.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/process/exec/execUtil.kt new file mode 100644 index 000000000000..51865d971788 --- /dev/null +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/process/exec/execUtil.kt @@ -0,0 +1,58 @@ +package com.intellij.ide.starter.process.exec + +import com.intellij.ide.starter.system.SystemInfo +import com.intellij.ide.starter.utils.logOutput +import java.nio.file.Path +import kotlin.io.path.div +import kotlin.time.Duration.Companion.minutes + + +fun executeScript(fileNameToExecute: String, projectDirPath: Path) { + val stdout = ExecOutputRedirect.ToString() + val stderr = ExecOutputRedirect.ToString() + + ProcessExecutor( + presentableName = "Executing of $fileNameToExecute", + workDir = projectDirPath, + timeout = 20.minutes, + args = listOf(fileNameToExecute), + stdoutRedirect = stdout, + stderrRedirect = stderr + ).start() + + val commit = stdout.read().trim() + val error = stderr.read().trim() + + logOutput("Stdout of command execution $commit") + logOutput("Stderr of command execution $error") +} + +fun execGradlew(pathToProject: Path, args: List) { + val stdout = ExecOutputRedirect.ToString() + val stderr = ExecOutputRedirect.ToString() + + val command = when (SystemInfo.isWindows) { + true -> (pathToProject / "gradlew.bat").toString() + false -> "./gradlew" + } + + if (!SystemInfo.isWindows) { + ProcessExecutor( + presentableName = "chmod gradlew", + workDir = pathToProject, + timeout = 1.minutes, + args = listOf("chmod", "+x", "gradlew"), + stdoutRedirect = stdout, + stderrRedirect = stderr + ).start() + } + + ProcessExecutor( + presentableName = "Gradle Format", + workDir = pathToProject, + timeout = 1.minutes, + args = listOf(command) + args, + stdoutRedirect = stdout, + stderrRedirect = stderr + ).start() +} diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/process/processUtils.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/process/processUtils.kt index 501a36b222a1..364761117170 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/process/processUtils.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/process/processUtils.kt @@ -1,9 +1,9 @@ package com.intellij.ide.starter.process import com.intellij.ide.starter.di.di -import com.intellij.ide.starter.exec.ExecOutputRedirect -import com.intellij.ide.starter.exec.exec import com.intellij.ide.starter.path.GlobalPaths +import com.intellij.ide.starter.process.exec.ExecOutputRedirect +import com.intellij.ide.starter.process.exec.ProcessExecutor import com.intellij.ide.starter.system.SystemInfo import com.intellij.ide.starter.utils.catchAll import com.intellij.ide.starter.utils.logOutput @@ -46,11 +46,13 @@ fun killOutdatedProcessesOnUnix(commandsToSearch: Iterable = listOf("/pe fun dumpListOfProcessesOnMacOS(): List { check(SystemInfo.isMac) val stdoutRedirect = ExecOutputRedirect.ToString() - exec("ps", - di.direct.instance().testsDirectory, - timeout = 1.minutes, - args = listOf("ps", "-ax"), - stdoutRedirect = stdoutRedirect) + ProcessExecutor("ps", + di.direct.instance().testsDirectory, + timeout = 1.minutes, + args = listOf("ps", "-ax"), + stdoutRedirect = stdoutRedirect + ).start() + val processLines = stdoutRedirect.read().lines().drop(1).map { it.trim() }.filterNot { it.isBlank() } //PID TTY TIME CMD // 1 ?? 0:43.67 /sbin/launchd @@ -75,11 +77,13 @@ fun dumpListOfProcessesOnMacOS(): List { fun dumpListOfProcessesOnLinux(): List { check(SystemInfo.isLinux) val stdoutRedirect = ExecOutputRedirect.ToString() - exec("ps", - di.direct.instance().testsDirectory, - timeout = 1.minutes, - args = listOf("ps", "-aux"), - stdoutRedirect = stdoutRedirect) + ProcessExecutor("ps", + di.direct.instance().testsDirectory, + timeout = 1.minutes, + args = listOf("ps", "-aux"), + stdoutRedirect = stdoutRedirect + ).start() + val processLines = stdoutRedirect.read().lines().drop(1).map { it.trim() }.filterNot { it.isBlank() } //USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND //root 823 0.0 0.0 1576524 8128 ? Ssl дек01 0:08 /usr/bin/containerd @@ -111,14 +115,14 @@ private fun killProcessOnUnix(pid: Int) { check(SystemInfo.isUnix) logOutput("Killing process $pid") - exec( + ProcessExecutor( "kill-process-$pid", di.direct.instance().testsDirectory, timeout = 1.minutes, args = listOf("kill", "-9", pid.toString()), stdoutRedirect = ExecOutputRedirect.ToStdOut("[kill-$pid-out]"), stderrRedirect = ExecOutputRedirect.ToStdOut("[kill-$pid-err]") - ) + ).start() } /** @@ -135,14 +139,15 @@ fun getJavaProcessId(javaHome: Path, workDir: Path, originalProcessId: Long, ori val stdout = ExecOutputRedirect.ToString() val stderr = ExecOutputRedirect.ToString() - exec( + ProcessExecutor( "jcmd-run", workDir, timeout = 1.minutes, args = listOf(javaHome.resolve("bin/jcmd").toAbsolutePath().toString()), stdoutRedirect = stdout, stderrRedirect = stderr - ) + ).start() + val mergedOutput = stdout.read() + "\n" + stderr.read() val candidates = arrayListOf() val candidatesFromProcessHandle = arrayListOf() @@ -213,14 +218,14 @@ fun collectJavaThreadDump( val command = listOf(jstackPath.toAbsolutePath().toString(), "-l", javaProcessId.toString()) - exec( + ProcessExecutor( "jstack", workDir, timeout = 1.minutes, args = command, stdoutRedirect = ExecOutputRedirect.ToFile(dumpFile.toFile()), stderrRedirect = ExecOutputRedirect.ToStdOut("[jstack-err]") - ) + ).start() if (includeStdout) { logOutput("jstack output:\n${dumpFile.toFile().readLines().joinToString("\n")}") @@ -229,13 +234,14 @@ fun collectJavaThreadDump( fun destroyGradleDaemonProcessIfExists() { val stdout = ExecOutputRedirect.ToString() - exec( + ProcessExecutor( "get jps process", workDir = null, timeout = 30.seconds, args = listOf("jps", "-l"), stdoutRedirect = stdout - ) + ).start() + logOutput("List of java processes: " + stdout.read()) if (stdout.read().contains("GradleDaemon")) { diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/report/ErrorReporter.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/report/ErrorReporter.kt index 25f7aa3384e1..ba5f4ef00847 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/report/ErrorReporter.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/report/ErrorReporter.kt @@ -2,6 +2,7 @@ package com.intellij.ide.starter.report import com.intellij.ide.starter.ci.CIServer import com.intellij.ide.starter.di.di +import com.intellij.ide.starter.runner.CurrentTestMethod import com.intellij.ide.starter.utils.convertToHashCodeWithOnlyLetters import com.intellij.ide.starter.utils.generifyErrorMessage import org.kodein.di.direct @@ -14,11 +15,18 @@ import kotlin.io.path.listDirectoryEntries object ErrorReporter { private const val MAX_TEST_NAME_LENGTH = 250 + private fun getTestMethodName(): String { + val method = di.direct.instance().get() + return if (method == null) "" else "${method.declaringClass.name}.${method.name}" + } + /** * Sort things out from errors directories, written by performance testing plugin * Take a look at [com.jetbrains.performancePlugin.ProjectLoaded.reportErrorsFromMessagePool] */ fun reportErrorsAsFailedTests(scriptErrorsDir: Path, contextName: String): List> { + val testMethodName = getTestMethodName().ifEmpty { contextName } + return if (scriptErrorsDir.isDirectory()) { val errorsDirectories = scriptErrorsDir.listDirectoryEntries() @@ -30,7 +38,7 @@ object ErrorReporter { val messageText = generifyErrorMessage(messageFile.readText().trim()) val stackTraceContent = stacktraceFile.readText().trim() - var testName: String + val testName: String val onlyLettersHash = convertToHashCodeWithOnlyLetters(generifyErrorMessage(stackTraceContent).hashCode()) @@ -44,7 +52,7 @@ object ErrorReporter { } val stackTrace = """ - Test: $contextName + Test: $testMethodName $stackTraceContent """.trimIndent().trimMargin().trim() diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/report/publisher/impl/QodanaTestResultPublisher.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/report/publisher/impl/QodanaTestResultPublisher.kt index 18ef93f695bd..bffe0121b0c7 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/report/publisher/impl/QodanaTestResultPublisher.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/report/publisher/impl/QodanaTestResultPublisher.kt @@ -9,7 +9,7 @@ import com.intellij.ide.starter.report.sarif.TestContextToQodanaSarifMapper object QodanaTestResultPublisher : ReportPublisher { override fun publish(ideStartResult: IDEStartResult) { - QodanaClient.report(TestContextToQodanaSarifMapper.map(ideStartResult)) + QodanaClient.report(TestContextToQodanaSarifMapper.map(ideStartResult)) } } \ No newline at end of file diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/report/sarif/SarifBuilder.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/report/sarif/SarifBuilder.kt index 39f6619c32f0..bfd4e3d12a35 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/report/sarif/SarifBuilder.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/report/sarif/SarifBuilder.kt @@ -23,6 +23,7 @@ fun taxa(init: ReportingDescriptor.() -> Unit): ReportingDescriptor { taxa.init() return taxa } + fun invocation(init: Invocation.() -> Unit): Invocation { val invocation = Invocation() invocation.init() diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/report/sarif/TestContextToQodanaSarifMapper.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/report/sarif/TestContextToQodanaSarifMapper.kt index 28a46e721eef..864f82cb726a 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/report/sarif/TestContextToQodanaSarifMapper.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/report/sarif/TestContextToQodanaSarifMapper.kt @@ -11,7 +11,7 @@ object TestContextToQodanaSarifMapper { fun map(ideStartResult: IDEStartResult): SarifReport { val defaultReportPath = this::class.java.classLoader.getResource("sarif/qodana.sarif.json")?.path - if(defaultReportPath==null) throw RuntimeException("Default report doesn' exits") + if (defaultReportPath == null) throw RuntimeException("Default report doesn' exits") val sarifReport = SarifUtil.readReport(Path.of(defaultReportPath)) return sarifReport(sarifReport) { diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/runner/CurrentTestMethod.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/runner/CurrentTestMethod.kt new file mode 100644 index 000000000000..ef974fb85938 --- /dev/null +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/runner/CurrentTestMethod.kt @@ -0,0 +1,27 @@ +package com.intellij.ide.starter.runner + +import java.lang.reflect.Method +import java.util.concurrent.atomic.AtomicReference + +/** + * Container, that pass around test method reference + */ +object CurrentTestMethod { + private lateinit var testMethod: AtomicReference + + fun set(method: Method) { + if (this::testMethod.isInitialized) { + testMethod.set(method) + } + else { + testMethod = AtomicReference(method) + } + } + + fun get(): Method? { + return if (this::testMethod.isInitialized) { + testMethod.get() + } + else null + } +} \ No newline at end of file diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/runner/IDERunContext.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/runner/IDERunContext.kt index ff2b25d3a8ba..6ad5a8ac1082 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/runner/IDERunContext.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/runner/IDERunContext.kt @@ -3,9 +3,6 @@ package com.intellij.ide.starter.runner import com.intellij.ide.starter.bus.EventState import com.intellij.ide.starter.bus.StarterBus import com.intellij.ide.starter.di.di -import com.intellij.ide.starter.exec.ExecOutputRedirect -import com.intellij.ide.starter.exec.ExecTimeoutException -import com.intellij.ide.starter.exec.exec import com.intellij.ide.starter.ide.CodeInjector import com.intellij.ide.starter.ide.IDETestContext import com.intellij.ide.starter.ide.command.MarshallableCommand @@ -14,12 +11,16 @@ import com.intellij.ide.starter.models.VMOptions import com.intellij.ide.starter.models.andThen import com.intellij.ide.starter.process.collectJavaThreadDump import com.intellij.ide.starter.process.destroyGradleDaemonProcessIfExists +import com.intellij.ide.starter.process.exec.ExecOutputRedirect +import com.intellij.ide.starter.process.exec.ExecTimeoutException +import com.intellij.ide.starter.process.exec.ProcessExecutor import com.intellij.ide.starter.process.getJavaProcessId import com.intellij.ide.starter.profiler.ProfilerInjector import com.intellij.ide.starter.profiler.ProfilerType import com.intellij.ide.starter.report.ErrorReporter import com.intellij.ide.starter.system.SystemInfo import com.intellij.ide.starter.utils.* +import kotlinx.coroutines.delay import org.kodein.di.direct import org.kodein.di.instance import java.io.Closeable @@ -27,8 +28,10 @@ import java.io.File import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths -import kotlin.concurrent.thread -import kotlin.io.path.* +import kotlin.io.path.absolutePathString +import kotlin.io.path.createDirectories +import kotlin.io.path.div +import kotlin.io.path.listDirectoryEntries import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds @@ -45,7 +48,7 @@ data class IDERunContext( val commands: Iterable = listOf(), val codeBuilder: (CodeInjector.() -> Unit)? = null, val runTimeout: Duration = 10.minutes, - val traceStacksEvery: Duration = 10.minutes, + val dumpThreadInterval: Duration = 10.minutes, val useStartupScript: Boolean = true, val closeHandlers: List Unit> = listOf(), val verboseOutput: Boolean = false, @@ -72,12 +75,12 @@ data class IDERunContext( fun addCompletionHandler(handler: IDERunCloseContext.() -> Unit) = this.copy(closeHandlers = closeHandlers + handler) - fun uploadProfileResultsToTeamCity(profilerSnapshotsDir: Path, artifactName: String) = + fun uploadProfilerResultsToCIServer(profilerSnapshotsDir: Path, artifactName: String) = this.addCompletionHandler { testContext.publishArtifact(source = profilerSnapshotsDir, artifactName = artifactName) } - fun installProfiler(): IDERunContext { + private fun installProfiler(): IDERunContext { return when (val profilerType = testContext.profilerType) { ProfilerType.ASYNC, ProfilerType.YOURKIT -> { val profiler = di.direct.instance(tag = profilerType) @@ -91,7 +94,7 @@ data class IDERunContext( } } - // TODO: refactor this + // TODO: refactor this https://youtrack.jetbrains.com/issue/AT-18/Simplify-refactor-code-for-starting-IDE-in-IdeRunContext private fun prepareToRunIDE(): IDEStartResult { StarterBus.post(IdeLaunchEvent(EventState.BEFORE, this)) @@ -109,7 +112,7 @@ data class IDERunContext( val stdout = if (verboseOutput) ExecOutputRedirect.ToStdOut("[ide-${contextName}-out]") else ExecOutputRedirect.ToString() val stderr = ExecOutputRedirect.ToStdOut("[ide-${contextName}-err]") - var successfulRun = true + var isRunSuccessful = true val host by lazy { di.direct.instance() } try { @@ -182,8 +185,8 @@ data class IDERunContext( File(finalArgs.first()).setExecutable(true) val executionTime = measureTime { - exec( - presentablePurpose = "run-ide-$contextName", + ProcessExecutor( + presentableName = "run-ide-$contextName", workDir = startConfig.workDir, environmentVariables = extendedEnvVariablesWithJavaHome, timeout = runTimeout, @@ -193,17 +196,16 @@ data class IDERunContext( stderrRedirect = stderr, onProcessCreated = { process, pid -> val javaProcessId by lazy { getJavaProcessId(jdkHome, startConfig.workDir, pid, process) } - thread(name = "jstack-${testContext.testName}", isDaemon = true) { - var cnt = 0 - while (process.isAlive) { - Thread.sleep(traceStacksEvery.inWholeMilliseconds) - if (!process.isAlive) break + val monitoringThreadDumpDir = logsDir.resolve("monitoring-thread-dumps").createDirectories() - val dumpFile = logsDir.resolve("threadDump-${++cnt}-${System.currentTimeMillis()}" + ".txt") - logOutput("Dumping threads to $dumpFile") - logOutput(Runtime.getRuntime().getRuntimeInfo()) - catchAll { collectJavaThreadDump(jdkHome, startConfig.workDir, javaProcessId, dumpFile, false) } - } + var cnt = 0 + while (process.isAlive) { + delay(dumpThreadInterval) + if (!process.isAlive) break + + val dumpFile = monitoringThreadDumpDir.resolve("threadDump-${++cnt}-${System.currentTimeMillis()}" + ".txt") + logOutput("Dumping threads to $dumpFile") + catchAll { collectJavaThreadDump(jdkHome, startConfig.workDir, javaProcessId, dumpFile, false) } } }, onBeforeKilled = { process, pid -> @@ -212,7 +214,7 @@ data class IDERunContext( if (collectNativeThreads) { val fileToStoreNativeThreads = logsDir.resolve("native-thread-dumps.txt") startProfileNativeThreads(javaProcessId.toString()) - Thread.sleep(15.seconds.inWholeMilliseconds) + delay(15.seconds) stopProfileNativeThreads(javaProcessId.toString(), fileToStoreNativeThreads.toAbsolutePath().toString()) } val dumpFile = logsDir.resolve("threadDump-before-kill-${System.currentTimeMillis()}" + ".txt") @@ -220,7 +222,7 @@ data class IDERunContext( } takeScreenshot(logsDir) } - ) + ).start() } logOutput("IDE run $contextName completed in $executionTime") @@ -247,7 +249,7 @@ data class IDERunContext( return IDEStartResult(runContext = this, executionTime = executionTime, vmOptionsDiff = vmOptionsDiff, logsDir = logsDir) } catch (t: Throwable) { - successfulRun = false + isRunSuccessful = false if (t is ExecTimeoutException && !expectedKill) { error("Timeout of IDE run $contextName for $runTimeout") } @@ -281,14 +283,14 @@ data class IDERunContext( }.forEach { it.toFile().deleteRecursively() } ErrorReporter.reportErrorsAsFailedTests(logsDir / "script-errors", contextName) - val (artifactPath, artifactName) = if (successfulRun) contextName to "logs" else "run/$contextName" to "crash" - testContext.publishArtifact(logsDir, artifactPath, formatArtifactName(artifactName, testContext.testName)) + publishArtifacts(isRunSuccessful) + if (codeBuilder != null) { host.tearDown(testContext) } val closeContext = object : IDERunCloseContext { - override val wasRunSuccessful: Boolean = successfulRun + override val wasRunSuccessful: Boolean = isRunSuccessful } closeHandlers.forEach { @@ -307,9 +309,24 @@ data class IDERunContext( } } + private fun publishArtifacts(isRunSuccessful: Boolean) { + // publish artifacts to directory with a test in any case + testContext.publishArtifact( + source = testContext.paths.logsDir, + artifactPath = contextName, + artifactName = formatArtifactName("logs", testContext.testName) + ) + + if (!isRunSuccessful) + testContext.publishArtifact( + source = testContext.paths.logsDir, + artifactPath = "_crashes/$contextName", + artifactName = formatArtifactName("crash", testContext.testName) + ) + } + fun runIDE(): IDEStartResult { - return installProfiler() - .prepareToRunIDE() + return installProfiler().prepareToRunIDE() } private fun deleteSavedAppStateOnMac() { diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/runner/TestContainer.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/runner/TestContainer.kt index 055c0b82f5f0..f087228f2ffa 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/runner/TestContainer.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/runner/TestContainer.kt @@ -1,5 +1,7 @@ package com.intellij.ide.starter.runner +import com.intellij.ide.starter.bus.EventState +import com.intellij.ide.starter.bus.StarterBus import com.intellij.ide.starter.ci.CIServer import com.intellij.ide.starter.di.di import com.intellij.ide.starter.ide.* @@ -22,13 +24,11 @@ import kotlin.io.path.div interface TestContainer : Closeable { val ciServer: CIServer var useLatestDownloadedIdeBuild: Boolean - val allContexts: MutableList + var testContext: IDETestContext val setupHooks: MutableList IDETestContext> override fun close() { - for (context in allContexts) { - catchAll { context.paths.close() } - } + catchAll { testContext.paths.close() } logOutput("TestContainer $this disposed") } @@ -48,6 +48,9 @@ interface TestContainer : Closeable { useLatestDownloadedIdeBuild = true } as T + /** + * @return + */ fun resolveIDE(ideInfo: IdeInfo): Pair { return di.direct.factory().invoke(ideInfo).install(ideInfo) } @@ -63,10 +66,8 @@ interface TestContainer : Closeable { } /** Starting point to run your test */ - fun initializeTestRunner(testName: String, testCase: TestCase): IDETestContext { - check(allContexts.none { it.testName == testName }) { "Test $testName is already initialized. Use another name." } + fun initializeTestContext(testName: String, testCase: TestCase): IDETestContext { logOutput("Resolving IDE build for $testName...") - val (buildNumber, ide) = resolveIDE(testCase.ideInfo) require(ide.productCode == testCase.ideInfo.productCode) { "Product code of $ide must be the same as for $testCase" } @@ -78,16 +79,15 @@ interface TestContainer : Closeable { logOutput("IDE to run for $testName: $ide") val projectHome = testCase.projectInfo?.downloadAndUnpackProject() - val context = IDETestContext(paths, ide, testCase, testName, projectHome, patchVMOptions = { this }, ciServer = ciServer) - allContexts += context + testContext = IDETestContext(paths, ide, testCase, testName, projectHome, patchVMOptions = { this }, ciServer = ciServer) - val baseContext = when (testCase.ideInfo == IdeProductProvider.AI) { - true -> context + testContext = when (testCase.ideInfo == IdeProductProvider.AI) { + true -> testContext .addVMOptionsPatch { overrideDirectories(paths) .withEnv("STUDIO_VM_OPTIONS", ide.patchedVMOptionsFile.toString()) } - false -> context + false -> testContext .disableInstantIdeShutdown() .disableFusSendingOnIdeClose() .disableJcef() @@ -101,8 +101,12 @@ interface TestContainer : Closeable { } } - return setupHooks - .fold(baseContext.updateGeneralSettings()) { acc, hook -> acc.hook() } + val contextWithAppliedHooks = setupHooks + .fold(testContext.updateGeneralSettings()) { acc, hook -> acc.hook() } .apply { installPerformanceTestingPluginIfMissing(this) } + + StarterBus.post(TestContextInitializedEvent(EventState.AFTER, contextWithAppliedHooks)) + + return contextWithAppliedHooks } } \ No newline at end of file diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/runner/TestContainerImpl.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/runner/TestContainerImpl.kt index 63bc87873398..15ef6ea0f201 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/runner/TestContainerImpl.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/runner/TestContainerImpl.kt @@ -9,6 +9,7 @@ import org.kodein.di.instance class TestContainerImpl( override val ciServer: CIServer = di.direct.instance(), override var useLatestDownloadedIdeBuild: Boolean = false, - override val allContexts: MutableList = mutableListOf(), override val setupHooks: MutableList IDETestContext> = mutableListOf() -) : TestContainer \ No newline at end of file +) : TestContainer { + override lateinit var testContext: IDETestContext +} \ No newline at end of file diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/runner/TestContextInitializedEvent.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/runner/TestContextInitializedEvent.kt new file mode 100644 index 000000000000..405040e7e4f4 --- /dev/null +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/runner/TestContextInitializedEvent.kt @@ -0,0 +1,7 @@ +package com.intellij.ide.starter.runner + +import com.intellij.ide.starter.bus.Event +import com.intellij.ide.starter.bus.EventState +import com.intellij.ide.starter.ide.IDETestContext + +class TestContextInitializedEvent(state: EventState, testContext: IDETestContext) : Event(state, testContext) \ No newline at end of file diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/runner/TestWatcherActions.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/runner/TestWatcherActions.kt new file mode 100644 index 000000000000..4465721174ae --- /dev/null +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/runner/TestWatcherActions.kt @@ -0,0 +1,44 @@ +package com.intellij.ide.starter.runner + +import com.intellij.ide.starter.ide.IDETestContext +import com.intellij.ide.starter.utils.catchAll +import com.intellij.ide.starter.utils.logOutput +import com.intellij.util.io.exists +import java.util.* + +class TestWatcherActions { + private val _onFailureActions: MutableList<(IDETestContext) -> Unit> = Collections.synchronizedList(mutableListOf()) + val onFailureActions: List<(IDETestContext) -> Unit> + get() = synchronized(_onFailureActions) { _onFailureActions.toList() } + + private val _onFinishedActions: MutableList<(IDETestContext) -> Unit> = Collections.synchronizedList(mutableListOf()) + val onFinishedActions: List<(IDETestContext) -> Unit> + get() = synchronized(_onFinishedActions) { _onFinishedActions.toList() } + + companion object { + /** Archive and add to test artifact entire ide's `system` dir */ + fun getSystemDirAsArtifactAction(): (IDETestContext) -> Unit = { testContext -> + catchAll { + logOutput("Archive with system directory created and will be published to artifacts") + testContext.publishArtifact(source = testContext.paths.systemDir, artifactName = "testSystemDirSnapshot.zip") + + val ideaDirPath = testContext.resolvedProjectHome.resolve(".idea") + + if (ideaDirPath.exists()) { + logOutput("Archive with .idea dir created and will be published to artifacts") + testContext.publishArtifact(source = ideaDirPath, artifactName = ".idea.zip") + } + } + } + } + + fun addOnFailureAction(action: (IDETestContext) -> Unit): TestWatcherActions { + synchronized(_onFailureActions) { _onFailureActions.add(action) } + return this + } + + fun addOnFinishedAction(action: (IDETestContext) -> Unit): TestWatcherActions { + synchronized(_onFinishedActions) { _onFinishedActions.add(action) } + return this + } +} \ No newline at end of file diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/sdk/JdkDownloaderFacade.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/sdk/JdkDownloaderFacade.kt index 1b535d5948d5..2af632579d2c 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/sdk/JdkDownloaderFacade.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/sdk/JdkDownloaderFacade.kt @@ -73,18 +73,22 @@ object JdkDownloaderFacade { } private fun downloadJdkItem(jdk: JdkItem, predicate: JdkPredicate): JdkItemPaths { - val targetJdkHome = when (predicate) { - JdkPredicate.forWSL() -> { - if (SystemInfo.isWindows && WslDistributionManager.getInstance().installedDistributions.isNotEmpty()) { - val wslDistribution = WslDistributionManager.getInstance().installedDistributions[0] - Path.of(wslDistribution.getWindowsPath("/tmp/jdks/${jdk.installFolderName}")) - } - else { - throw WslDistributionNotFoundException() - } + val targetJdkHome: Path + + // hack for wsl on windows + if (predicate == JdkPredicate.forWSL() && SystemInfo.isWindows && WslDistributionManager.getInstance().installedDistributions.isNotEmpty()) { + try { + val wslDistribution = WslDistributionManager.getInstance().installedDistributions[0] + targetJdkHome = Path.of(wslDistribution.getWindowsPath("/tmp/jdks/${jdk.installFolderName}")) + } + catch (_: Exception) { + throw WslDistributionNotFoundException(predicate) } - else -> di.direct.instance().getCacheDirectoryFor("jdks").resolve(jdk.installFolderName) } + else { + targetJdkHome = di.direct.instance().getCacheDirectoryFor("jdks").resolve(jdk.installFolderName) + } + val targetHomeMarker = targetJdkHome.resolve("home.link") logOutput("Checking JDK at $targetJdkHome") diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/system/SystemInfo.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/system/SystemInfo.kt index 088fe93798f2..245e84ec7790 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/system/SystemInfo.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/system/SystemInfo.kt @@ -19,6 +19,8 @@ object SystemInfo { val isUnix = !isWindows val isXWindow = isUnix && !isMac + val isAarch64: Boolean = OS_ARCH == "aarch64" + private fun getRtVersion(fallback: String): String? { val rtVersion = System.getProperty("java.runtime.version") return if (Character.isDigit(rtVersion[0])) rtVersion else fallback diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/utils/FileSystem.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/utils/FileSystem.kt index 01b40ba4c8e6..92089e05b454 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/utils/FileSystem.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/utils/FileSystem.kt @@ -1,9 +1,9 @@ package com.intellij.ide.starter.utils import com.intellij.ide.starter.di.di -import com.intellij.ide.starter.exec.ExecOutputRedirect -import com.intellij.ide.starter.exec.exec import com.intellij.ide.starter.path.GlobalPaths +import com.intellij.ide.starter.process.exec.ExecOutputRedirect +import com.intellij.ide.starter.process.exec.ProcessExecutor import com.intellij.ide.starter.system.SystemInfo import org.kodein.di.instance import org.rauschig.jarchivelib.ArchiveFormat @@ -129,13 +129,13 @@ object FileSystem { } else if (SystemInfo.isLinux || SystemInfo.isMac) { Files.createDirectories(targetDir) - exec( - presentablePurpose = "extract-tar", + ProcessExecutor( + presentableName = "extract-tar", workDir = targetDir, timeout = 10.minutes, stderrRedirect = ExecOutputRedirect.ToStdOut("tar"), args = listOf("tar", "-z", "-x", "-f", tarFile.toAbsolutePath().toString(), "-C", targetDir.toAbsolutePath().toString()) - ) + ).start() } } catch (e: Exception) { diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/utils/Git.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/utils/Git.kt index de0978122c64..3030d0729c0c 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/utils/Git.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/utils/Git.kt @@ -1,7 +1,7 @@ package com.intellij.ide.starter.utils -import com.intellij.ide.starter.exec.ExecOutputRedirect -import com.intellij.ide.starter.exec.exec +import com.intellij.ide.starter.process.exec.ExecOutputRedirect +import com.intellij.ide.starter.process.exec.ProcessExecutor import java.io.IOException import java.nio.file.Path import java.nio.file.Paths @@ -15,12 +15,14 @@ object Git { @Throws(IOException::class, InterruptedException::class) private fun getLocalGitBranch(): String { val stdout = ExecOutputRedirect.ToString() - exec( + + ProcessExecutor( "git-local-branch-get", workDir = null, timeout = 1.minutes, args = listOf("git", "rev-parse", "--abbrev-ref", "HEAD"), stdoutRedirect = stdout - ) + ).start() + return stdout.read().trim() } @@ -41,12 +43,12 @@ object Git { val stdout = ExecOutputRedirect.ToString() try { - exec( + ProcessExecutor( "git-repo-root-get", workDir = null, timeout = 1.minutes, args = listOf("git", "rev-parse", "--show-toplevel", "HEAD"), stdoutRedirect = stdout - ) + ).start() } catch (e: Exception) { val workDir = Paths.get("").toAbsolutePath() diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/utils/testNameExtension.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/utils/testNameExtension.kt index e8703ddbcfc1..fbee9d5ae2f1 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/utils/testNameExtension.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/utils/testNameExtension.kt @@ -11,6 +11,7 @@ import kotlin.io.path.name fun String.hyphenateTestName(): String { fun hyphenateString(input: String) = input + .replace(Regex("( )+"), "-") .replace(" ", "-").trim() .replaceFirstChar { it.lowercase(Locale.getDefault()) }.toCharArray() .map { diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/utils/utils.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/utils/utils.kt index 946da7d1dcc6..715cb558ba78 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/utils/utils.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/utils/utils.kt @@ -1,9 +1,9 @@ package com.intellij.ide.starter.utils import com.intellij.ide.starter.di.di -import com.intellij.ide.starter.exec.ExecOutputRedirect -import com.intellij.ide.starter.exec.exec import com.intellij.ide.starter.path.GlobalPaths +import com.intellij.ide.starter.process.exec.ExecOutputRedirect +import com.intellij.ide.starter.process.exec.ProcessExecutor import com.intellij.ide.starter.system.SystemInfo import org.kodein.di.direct import org.kodein.di.instance @@ -31,13 +31,18 @@ fun getThrowableText(t: Throwable): String { return writer.buffer.toString() } -inline fun catchAll(action: () -> Unit) { +/** + * In case of success - return T + * In case of error - print error to stderr and return null + */ +inline fun catchAll(action: () -> T): T? { try { - action() + return action() } catch (t: Throwable) { logError("CatchAll swallowed error: ${t.message}") logError(getThrowableText(t)) + return null } } @@ -71,14 +76,14 @@ fun execJavaCmd(javaHome: Path, args: Iterable = listOf()): List val processArguments = listOf(java.toString()).plus(args) - exec( - presentablePurpose = prefix, + ProcessExecutor( + presentableName = prefix, workDir = javaHome, timeout = 1.minutes, args = processArguments, stdoutRedirect = stdout, stderrRedirect = stderr - ) + ).start() val mergedOutput = listOf(stdout, stderr) .flatMap { it.read().split(System.lineSeparator()) } @@ -184,14 +189,14 @@ fun takeScreenshot(logsDir: Path) { val toolPath = screenshotTool.resolve("$toolName.jar") val javaPath = ProcessHandle.current().info().command().orElseThrow().toString() - exec( - presentablePurpose = "take-screenshot", + ProcessExecutor( + presentableName = "take-screenshot", workDir = toolsDir, timeout = 15.seconds, args = mutableListOf(javaPath, "-jar", toolPath.absolutePathString(), screenshotFile.toString()), environmentVariables = mapOf("DISPLAY" to ":88"), onlyEnrichExistedEnvVariables = true - ) + ).start() if (screenshotFile.exists()) { logOutput("Screenshot saved in $screenshotFile") @@ -204,32 +209,38 @@ fun takeScreenshot(logsDir: Path) { fun startProfileNativeThreads(pid: String) { if (!SystemInfo.isWindows) { val toolsDir = di.direct.instance().getCacheDirectoryFor("tools") - val toolName = "async-profiler-2.7-macos" + val toolName = when { + SystemInfo.isMac -> "async-profiler-2.7-macos" + SystemInfo.isLinux -> "async-profiler-2.7-linux-x64" + else -> error("Not supported OS") + } val profiler = toolsDir / toolName downloadAsyncProfilerIfNeeded(profiler, toolsDir) givePermissionsToExecutables(profiler) - exec( - presentablePurpose = "start-profile", + + ProcessExecutor( + presentableName = "start-profile", workDir = profiler, timeout = 15.seconds, args = mutableListOf("./profiler.sh", "start", pid) - ) + ).start() } } private fun givePermissionsToExecutables(profiler: Path) { - exec( - presentablePurpose = "give-permissions-to-jattach", + ProcessExecutor( + presentableName = "give-permissions-to-jattach", workDir = profiler.resolve("build"), timeout = 10.seconds, args = mutableListOf("chmod", "+x", "jattach") - ) - exec( - presentablePurpose = "give-permissions-to-profiler", + ).start() + + ProcessExecutor( + presentableName = "give-permissions-to-profiler", workDir = profiler, timeout = 10.seconds, args = mutableListOf("chmod", "+x", "profiler.sh") - ) + ).start() } fun stopProfileNativeThreads(pid: String, fileToStoreInfo: String) { @@ -237,12 +248,13 @@ fun stopProfileNativeThreads(pid: String, fileToStoreInfo: String) { val toolsDir = di.direct.instance().getCacheDirectoryFor("tools") val toolName = "async-profiler-2.7-macos" val profiler = toolsDir / toolName - exec( - presentablePurpose = "stop-profile", + + ProcessExecutor( + presentableName = "stop-profile", workDir = profiler, timeout = 15.seconds, args = mutableListOf("./profiler.sh", "stop", pid, "-f", fileToStoreInfo) - ) + ).start() } } diff --git a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/wsl/WslDistributionNotFoundException.kt b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/wsl/WslDistributionNotFoundException.kt index 0d4cbd391aad..bd09e2f04058 100644 --- a/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/wsl/WslDistributionNotFoundException.kt +++ b/tools/ideTestingFramework/intellij.tools.ide.starter/src/com/intellij/ide/starter/wsl/WslDistributionNotFoundException.kt @@ -3,8 +3,9 @@ package com.intellij.ide.starter.wsl import com.intellij.execution.wsl.WslDistributionManager import com.intellij.ide.starter.system.SystemInfo import com.intellij.ide.starter.utils.logOutput +import com.intellij.openapi.projectRoots.impl.jdkDownloader.JdkPredicate -class WslDistributionNotFoundException() : Exception() { +class WslDistributionNotFoundException(val jdkPredicate: JdkPredicate? = null) : Exception() { override val message: String get() { val red = "\u001b[31m" @@ -31,6 +32,7 @@ class WslDistributionNotFoundException() : Exception() { appendLine("Can't run test on WSL") appendLine("Current OS: ${SystemInfo.OS_NAME} ${SystemInfo.OS_VERSION}") appendLine("WSL distributions installed: ${WslDistributionManager.getInstance().installedDistributions}") + jdkPredicate?.let { appendLine(jdkPredicate) } } } } \ No newline at end of file