(gtw) [CWM-5109] fixing tabs coloring when vcs changing rev changes

GitOrigin-RevId: fc6053e5c0a51eb4b886191ee06c15fd7f503af4
This commit is contained in:
Olga Klisho
2022-07-15 14:02:01 +02:00
committed by intellij-monorepo-bot
parent 939d5881da
commit 7b010bbabb
131 changed files with 18 additions and 7143 deletions

2
.gitignore vendored
View File

@@ -18,4 +18,4 @@ edu/dependencies/build
native/**/build/
stale_outputs_checked
/android
/tools/intellij.ide.starter
/tools/ideTestingFramework/intellij.tools.ide.starter

View File

@@ -1,6 +1,7 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.openapi.fileEditor.impl;
import com.intellij.openapi.editor.colors.ColorKey;
import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
@@ -34,7 +35,7 @@ public interface EditorTabColorProvider {
*/
@Nullable
@ApiStatus.Experimental
default Color getEditorTabForegroundColor(@NotNull Project project, @NotNull VirtualFile file) { return null; }
default ColorKey getEditorTabForegroundColor(@NotNull Project project, @NotNull VirtualFile file) { return null; }
/**
*

View File

@@ -2,11 +2,13 @@
package com.intellij.ui.tabs;
import com.intellij.openapi.editor.colors.ColorKey;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.impl.EditorTabColorProvider;
import com.intellij.openapi.fileEditor.impl.FileEditorManagerImpl;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vcs.FileStatusManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.FileColorManager;
import org.jetbrains.annotations.NotNull;
@@ -27,11 +29,11 @@ public class EditorTabColorProviderImpl implements EditorTabColorProvider, DumbA
}
@Override
public @Nullable Color getEditorTabForegroundColor(@NotNull Project project, @NotNull VirtualFile file) {
public @Nullable ColorKey getEditorTabForegroundColor(@NotNull Project project, @NotNull VirtualFile file) {
FileEditorManager manger = FileEditorManager.getInstance(project);
if (!(manger instanceof FileEditorManagerImpl)) return null;
FileEditorManagerImpl fileEditorManager = (FileEditorManagerImpl)FileEditorManager.getInstance(project);
return fileEditorManager.getFileColor(file);
FileStatusManager fileStatusManager = FileStatusManager.getInstance(project);
return fileStatusManager.getStatus(file).getColorKey();
}
@Nullable

View File

@@ -434,7 +434,8 @@ open class EditorsSplitters internal constructor(val manager: FileEditorManagerI
val manager = manager
window.setForegroundAt(index, manager.getFileColor(file))
var resultAttributes = TextAttributes()
resultAttributes.foregroundColor = getForegroundColorForFile(manager.project, file)
resultAttributes.foregroundColor = colorScheme.getColor(getForegroundColorForFile(manager.project, file))
var attributes = if (manager.isProblem(file)) colorScheme.getAttributes(CodeInsightColors.ERRORS_ATTRIBUTES) else null
if (composite.isPreview) {
val italic = TextAttributes(null, null, null, null, Font.ITALIC)
@@ -1014,4 +1015,4 @@ private fun getSplittersForProject(activeWindow: Window?, project: Project?): Ed
?: return null
val splitters = if (activeWindow == null) null else fileEditorManager.getSplittersFor(activeWindow)
return splitters ?: fileEditorManager.splitters
}
}

View File

@@ -1,11 +1,15 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.fileEditor.impl
import com.intellij.openapi.editor.colors.ColorKey
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import java.awt.Color
fun getForegroundColorForFile(project: Project, file: VirtualFile): Color? {
return EditorTabColorProvider.EP_NAME.extensionList.firstOrNull()?.getEditorTabForegroundColor(project, file)
fun getForegroundColorForFile(project: Project, file: VirtualFile): ColorKey? {
val first: EditorTabColorProvider = EditorTabColorProvider.EP_NAME.extensionList.first {
val editorTabColor = it.getEditorTabForegroundColor(project, file)
return@first editorTabColor != null
}
return first.getEditorTabForegroundColor(project, file)
}

View File

@@ -1,4 +0,0 @@
.idea
out
.gradle
build

View File

@@ -1,158 +0,0 @@
### 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.
Aside from that, you may find useful functionality as below:
* execution commands in plugins (list of available commands described below)
* implementing your custom command to be invoked later in tests
* execution custom code (though, you cannot use external libraries here)
* integration with CI (optional)
* collecting test artifacts
* reporting of artifacts to CI (optional)
* run a test with a profiler (not yet included)
#### Supported products
* IDEA
* GoLand
* WebStorm
* PhpStorm
* DataGrip
* PyCharm
* RubyMine
* Android Studio
##### How to setup
Configure maven repositories in your `build.gradle` file
```
repositories {
maven { url = "https://cache-redirector.jetbrains.com/maven-central" }
maven { url = "https://cache-redirector.jetbrains.com/intellij-dependencies" }
maven { url = "https://www.jetbrains.com/intellij-repository/releases" }
}
```
Instead of `maven { url = "https://cache-redirector.jetbrains.com/maven-central" }` you may use `mavenCentral()`
If you're sure, that you need more recent version of packages, you might use
`maven { url = "https://www.jetbrains.com/intellij-repository/snapshots" }`
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
Minimal setup example:
```
dependencies {
testImplementation("com.jetbrains.intellij.ide:ide-starter:LATEST-EAP-SNAPSHOT")
testImplementation("com.jetbrains.intellij.performanceTesting:performance-testing-commands:LATEST-EAP-SNAPSHOT")
testImplementation("junit:junit:4.13.2")
}
```
To make sure, that you will not get problem with Kotlin Duration, add the following
```
compileTestKotlin {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
freeCompilerArgs += [
"-Xopt-in=kotlin.time.ExperimentalTime"
]
}
}
```
##### Run with JUnit4
[Example of simple test, that will download IntelliJ IDEA and start import of gradle project](https://github.com/JetBrains/intellij-community/tree/master/tools/intellij.ide.starter/testSrc/com/intellij/ide/starter/tests/examples)
Don't forget to create appropriate classes for JUnit4StarterRule, IdeaCases.
##### Run with JUnit5
The key difference between running tests on JUnit4 and JUnit5 here will be migration from ExternalResource (in JUnit4) to TempDir (in JUnit5).
Though, for now we don't have an example for JUnit5StarterRule.
[JUnit4StarterRule implementation example is here](https://github.com/JetBrains/intellij-community/blob/master/tools/intellij.ide.starter/testSrc/com/intellij/ide/starter/tests/examples/junit4/JUnit4StarterRule.kt)
##### Available commands from plugins
Dependency `performance-testing-commands`
- waitForSmartMode()
- flushIndexes()
- setupProjectSdk(sdkName: String, sdkType: String, sdkPath: String)
- setupProjectSdk(sdkObject: SdkObject)
- setupProjectJdk(sdkName: String, sdkPath: String) = setupProjectSdk(sdkName, "JavaSDK", sdkPath)
- openFile(relativePath: String)
- openProject(projectPath: Path)
- reopenProject()
- goto(line: String, column: String)
- findUsages()
- inspectCode()
- checkOnRedCode()
- exitApp(forceExit: Boolean = true)
- exitAppWithTimeout(timeoutInSeconds: Long)
- memoryDump()
- dumpProjectFiles()
- compareProjectFiles(firstDir: String, secondDir: String)
- cleanCaches()
- doComplete()
- doComplete(times: Int)
- openProjectView()
- pressKey(key: String)
- delayType(command: String)
- doLocalInspection()
- altEnter(intention: String)
- callAltEnter(times: Int, intention: String = "")
- createAllServicesAndExtensions()
- runConfiguration(command: String)
- openFileWithTerminate(relativePath: String, terminateIdeInSeconds: Long)
- searchEverywhere(text: String)
- storeIndices()
- compareIndices()
- recoveryAction(action: RecoveryActionType)
- ... **TBD**
Dependency `performance-testing-maven-commands`
- importMavenProject()
Dependency `performance-testing-gradle-commands`
- importGradleProject()
#### What behaviour might be extended / modified
Everything, that initializes via DI framework (Kodein DI) might be modified in your code for your need.
[DI container initialization](https://github.com/JetBrains/intellij-community/blob/master/tools/intellij.ide.starter/src/com/intellij/ide/starter/di/diContainer.kt)
For example, you might write your own implementation of CIServer and provide it via DI.
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)
bindSingleton<CIServer>(overrides = true) { YourImplementationOfCI() }
}
```

View File

@@ -1,132 +0,0 @@
apply plugin: 'java'
apply plugin: 'kotlin'
buildscript {
ext.kotlin_version = '1.7.0'
ext.jackson_version = '2.13.3'
repositories {
maven { url = "https://cache-redirector.jetbrains.com/maven-central" }
maven { url = "https://cache-redirector.jetbrains.com/intellij-dependencies" }
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
sourceSets {
main.kotlin.srcDirs += 'src'
test.kotlin.srcDirs += 'testSrc'
test.resources {
srcDirs "testResources"
includes = ["**/*.*"]
}
}
repositories {
maven { url = "https://cache-redirector.jetbrains.com/maven-central" }
maven { url = "https://cache-redirector.jetbrains.com/intellij-dependencies" }
maven { url = "https://www.jetbrains.com/intellij-repository/releases" }
maven { url = "https://www.jetbrains.com/intellij-repository/snapshots" }
maven { url = "https://www.jetbrains.com/intellij-repository/nightly" }
maven { url = "https://cache-redirector.jetbrains.com/packages.jetbrains.team/maven/p/grazi/grazie-platform-public" }
}
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.6.2"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation "com.fasterxml.jackson.core:jackson-core:$jackson_version"
implementation "com.fasterxml.jackson.core:jackson-databind:$jackson_version"
implementation "com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_version"
implementation "org.apache.httpcomponents:httpmime:4.5.13"
implementation "org.jetbrains.intellij.deps:gradle-api:7.4"
implementation "org.kodein.di:kodein-di-jvm:7.13.0"
implementation "org.rauschig:jarchivelib:1.2.0"
implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.3"
implementation "commons-logging:commons-logging:1.2"
implementation "com.jetbrains.qodana:qodana-sarif:0.1.99"
implementation('com.jetbrains.intellij.platform:lang-impl:LATEST-TRUNK-SNAPSHOT') {
exclude group: 'org.jetbrains.kotlinx'
}
implementation('com.jetbrains.intellij.platform:ide-impl:LATEST-TRUNK-SNAPSHOT') {
exclude group: 'org.jetbrains.kotlinx'
exclude group: 'com.jetbrains.intellij.remoteDev'
}
implementation 'com.jetbrains.intellij.performanceTesting:performance-testing-commands:LATEST-TRUNK-SNAPSHOT'
testImplementation 'com.jetbrains.intellij.performanceTesting:performance-testing-maven-commands:LATEST-TRUNK-SNAPSHOT'
testImplementation 'com.jetbrains.intellij.performanceTesting:performance-testing-gradle-commands:LATEST-TRUNK-SNAPSHOT'
testImplementation 'com.jetbrains.intellij.dynamicPlugins:dynamic-plugins-performance-testing-commands:LATEST-TRUNK-SNAPSHOT'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.7.0'
testImplementation 'org.junit.vintage:junit-vintage-engine:5.8.1'
testImplementation 'org.junit.platform:junit-platform-launcher:1.8.2'
testImplementation 'io.kotest:kotest-assertions-core-jvm:5.3.2'
testImplementation 'org.mockito:mockito-core:3.12.4'
testImplementation 'org.mockito:mockito-junit-jupiter:3.12.4'
}
configurations {
testImplementation.exclude(group: 'com.jetbrains.intellij.remoteDev')
}
compileKotlin {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
freeCompilerArgs += [
"-version",
"-Xjvm-default=enable",
"-Xopt-in=kotlin.time.ExperimentalTime"
]
}
}
compileTestKotlin {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
freeCompilerArgs += [
"-version",
"-Xjvm-default=enable",
"-Xopt-in=kotlin.time.ExperimentalTime"
]
}
}
test {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
}
}
task runOnlyUnitTests(type: Test) {
filter {
includeTestsMatching "com.intellij.ide.starter.tests.unit.*"
}
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
}
}

View File

@@ -1,5 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://cache-redirector.jetbrains.com/services.gradle.org/distributions/gradle-7.4.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -1,234 +0,0 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# 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
#
# https://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.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

View File

@@ -1,89 +0,0 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -1,113 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="kotlin-language" name="Kotlin">
<configuration version="3" platform="JVM 11" allPlatforms="JVM [11]" useProjectSettings="false">
<compilerSettings>
<option name="additionalArguments" value="-version -Xjvm-default=enable -Xopt-in=kotlin.time.ExperimentalTime" />
</compilerSettings>
<compilerArguments>
<option name="jvmTarget" value="11" />
<option name="languageVersion" value="1.7" />
<option name="apiVersion" value="1.7" />
</compilerArguments>
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/testSrc" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/testResources" type="java-test-resource" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="kotlinx-coroutines-jdk8" level="project" />
<orderEntry type="library" name="kotlin-stdlib-jdk8" level="project" />
<orderEntry type="library" name="kotlin-reflect" level="project" />
<orderEntry type="library" name="jackson" level="project" />
<orderEntry type="library" name="jackson-databind" level="project" />
<orderEntry type="library" name="jackson-module-kotlin" level="project" />
<orderEntry type="library" name="http-client" level="project" />
<orderEntry type="library" name="Gradle" level="project" />
<orderEntry type="library" name="kodein-di-jvm" level="project" />
<orderEntry type="module-library">
<library name="rauschig.jarchivelib" type="repository">
<properties maven-id="org.rauschig:jarchivelib:1.2.0" />
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/rauschig/jarchivelib/1.2.0/jarchivelib-1.2.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/apache/commons/commons-compress/1.21/commons-compress-1.21.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
<excluded>
<root url="jar://$MAVEN_REPOSITORY$/org/apache/commons/commons-compress/1.21/commons-compress-1.21.jar!/" />
</excluded>
</library>
</orderEntry>
<orderEntry type="module" module-name="intellij.performanceTesting.commands" scope="TEST" />
<orderEntry type="module" module-name="intellij.performancePlugin.extended.maven.commands" scope="TEST" />
<orderEntry type="module" module-name="intellij.performancePlugin.extended.gradle.commands" scope="TEST" />
<orderEntry type="library" name="commons-logging" level="project" />
<orderEntry type="library" scope="TEST" name="JUnit4" level="project" />
<orderEntry type="library" scope="TEST" name="JUnit5" level="project" />
<orderEntry type="library" scope="TEST" name="JUnit5Jupiter" level="project" />
<orderEntry type="library" scope="TEST" name="JUnit5Params" level="project" />
<orderEntry type="library" scope="TEST" name="mockito" level="project" />
<orderEntry type="module-library" scope="TEST">
<library name="mockito-junit-jupiter" type="repository">
<properties maven-id="org.mockito:mockito-junit-jupiter:3.12.4" />
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/mockito/mockito-junit-jupiter/3.12.4/mockito-junit-jupiter-3.12.4.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/mockito/mockito-core/3.12.4/mockito-core-3.12.4.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/net/bytebuddy/byte-buddy/1.11.13/byte-buddy-1.11.13.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/net/bytebuddy/byte-buddy-agent/1.11.13/byte-buddy-agent-1.11.13.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/objenesis/objenesis/3.2/objenesis-3.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/junit/jupiter/junit-jupiter-api/5.7.2/junit-jupiter-api-5.7.2.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/apiguardian/apiguardian-api/1.1.0/apiguardian-api-1.1.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/opentest4j/opentest4j/1.2.0/opentest4j-1.2.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/junit/platform/junit-platform-commons/1.7.2/junit-platform-commons-1.7.2.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/mockito/mockito-junit-jupiter/3.12.4/mockito-junit-jupiter-3.12.4-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/mockito/mockito-core/3.12.4/mockito-core-3.12.4-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/net/bytebuddy/byte-buddy/1.11.13/byte-buddy-1.11.13-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/net/bytebuddy/byte-buddy-agent/1.11.13/byte-buddy-agent-1.11.13-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/objenesis/objenesis/3.2/objenesis-3.2-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/junit/jupiter/junit-jupiter-api/5.7.2/junit-jupiter-api-5.7.2-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/apiguardian/apiguardian-api/1.1.0/apiguardian-api-1.1.0-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/opentest4j/opentest4j/1.2.0/opentest4j-1.2.0-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/junit/platform/junit-platform-commons/1.7.2/junit-platform-commons-1.7.2-javadoc.jar!/" />
</JAVADOC>
<SOURCES />
</library>
</orderEntry>
<orderEntry type="module-library">
<library name="jackson-datatype-jsr310" type="repository">
<properties include-transitive-deps="false" maven-id="com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.1" />
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.13.1/jackson-datatype-jsr310-2.13.1.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="library" scope="TEST" name="kotlin-test-assertions-core-jvm" level="project" />
<orderEntry type="library" scope="TEST" name="java-diff-utils" level="project" />
<orderEntry type="module-library">
<library name="qodana-sarif" type="repository">
<properties include-transitive-deps="false" maven-id="com.jetbrains.qodana:qodana-sarif:0.1.95" />
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/com/jetbrains/qodana/qodana-sarif/0.1.95/qodana-sarif-0.1.95.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="library" name="gson" level="project" />
<orderEntry type="module" module-name="intellij.platform.lang.impl" />
<orderEntry type="module" module-name="intellij.platform.ide.impl" />
</component>
</module>

View File

@@ -1,13 +0,0 @@
<application>
<component name="GeneralSettings">
<option name="useSafeWrite" value="false" />
<option name="confirmExit" value="false"/>
<option name="showTipsOnStartup" value="false"/>
</component>
<component name="Registry">
<entry key="compiler.automake.allow.parallel" value="true"/>
<entry key="vcs.log.index.git" value="false"/>
<entry key="vcs.log.keep.up.to.date" value="false"/>
<entry key="actionSystem.playback.typecommand.delay" value="0"/>
</component>
</application>

View File

@@ -1,34 +0,0 @@
{
"version": "2.1.0",
"runs": [
{
"tool": {
"driver": {
"name": "intellij.ide.starter",
"fullName": "Intellij ide starter framework",
"version": "",
"taxa": [],
"language": "en-US",
"contents": [
"localizedData",
"nonLocalizedData"
],
"isComprehensive": false
}
},
"invocations": [],
"language": "en-US",
"versionControlProvenance": [],
"results": [],
"automationDetails": {
"id": "",
"guid": ""
},
"newlineSequences": [
"\r\n",
"\n"
],
"properties": {}
}
]
}

View File

@@ -1,162 +0,0 @@
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.system.SystemInfo
import com.intellij.ide.starter.utils.FileSystem
import com.intellij.ide.starter.utils.HttpClient
import com.intellij.ide.starter.utils.logOutput
import com.intellij.ide.starter.utils.resolveInstalledJdk11
import org.gradle.internal.hash.Hashing
import org.kodein.di.direct
import org.kodein.di.instance
import java.io.File
import java.nio.file.Path
import kotlin.io.path.div
import kotlin.io.path.isDirectory
import kotlin.time.Duration.Companion.minutes
/**
* Resolve platform specific android studio installer and return paths
* @return Pair<InstallDir / InstallerFile>
*/
fun downloadAndroidStudio(): Pair<Path, File> {
val ext = when {
SystemInfo.isWindows -> "-windows.zip"
SystemInfo.isMac -> "-mac.zip"
SystemInfo.isLinux -> "-linux.tar.gz"
else -> error("Not supported OS")
}
val downloadUrl = "https://redirector.gvt1.com/edgedl/android/studio/ide-zips/2021.1.1.11/android-studio-2021.1.1.11$ext"
val asFileName = downloadUrl.split("/").last()
val globalPaths by di.instance<GlobalPaths>()
val zipFile = globalPaths.getCacheDirectoryFor("android-studio").resolve(asFileName)
HttpClient.downloadIfMissing(downloadUrl, zipFile)
val installDir = globalPaths.getCacheDirectoryFor("builds") / "AI-211"
installDir.toFile().deleteRecursively()
val installerFile = zipFile.toFile()
return Pair(installDir, installerFile)
}
fun downloadLatestAndroidSdk(javaHome: Path): Path {
val packages = listOf(
"build-tools;28.0.3",
//"cmake;3.10.2.4988404",
//"docs",
//"ndk;20.0.5594570",
"platforms;android-28",
"sources;android-28",
"platform-tools"
)
val sdkManager = downloadSdkManager()
// we use unique home folder per installation to ensure only expected
// packages are included into the SDK home path
val packagesHash = Hashing.sha1().hashString(packages.joinToString("$"))
val home = di.direct.instance<GlobalPaths>().getCacheDirectoryFor("android-sdk") / "sdk-roots" / "sdk-root-$packagesHash"
if (home.isDirectory() && home.toFile().walk().count() > 10) return home
val envVariablesWithJavaHome = System.getenv() + ("JAVA_HOME" to javaHome.toAbsolutePath().toString())
try {
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
)
//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
)
//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
)
return home
}
catch (t: Throwable) {
home.toFile().deleteRecursively()
throw Exception("Failed to prepare Android SDK to $home. ${t.message}", t)
}
}
private fun downloadSdkManager(): Path {
val url = when {
SystemInfo.isMac -> "https://dl.google.com/android/repository/commandlinetools-mac-6200805_latest.zip"
SystemInfo.isWindows -> "https://dl.google.com/android/repository/commandlinetools-win-6200805_latest.zip"
SystemInfo.isLinux -> "https://dl.google.com/android/repository/commandlinetools-linux-6200805_latest.zip"
else -> error("Unsupported OS: ${SystemInfo.OS_NAME} ${SystemInfo.OS_VERSION}")
}
val name = url.split("/").last()
val androidSdkCache = di.direct.instance<GlobalPaths>().getCacheDirectoryFor("android-sdk")
val targetArchive = androidSdkCache / "archives" / name
val targetUnpack = androidSdkCache / "builds" / name
HttpClient.downloadIfMissing(url, targetArchive)
FileSystem.unpackIfMissing(targetArchive, targetUnpack)
val ext = if (SystemInfo.isWindows) ".bat" else ""
@Suppress("SpellCheckingInspection")
val sdkManager = targetUnpack.toFile().walk().first { it.endsWith("tools/bin/sdkmanager$ext") }
if (SystemInfo.isMac || SystemInfo.isLinux) {
sdkManager.setExecutable(true)
}
return sdkManager.toPath()
}
fun main() {
downloadLatestAndroidSdk(resolveInstalledJdk11())
}
fun IDETestContext.downloadAndroidPluginProject(): IDETestContext {
val projectHome = resolvedProjectHome
if (projectHome.toFile().name == "intellij-community-master" && !(projectHome / "android").toFile().exists()) {
val scriptName = "getPlugins.sh"
val script = (projectHome / scriptName).toFile()
assert(script.exists()) { "File $script does not exist" }
val scriptContent = script.readText()
val stdout = ExecOutputRedirect.ToString()
val stderr = ExecOutputRedirect.ToString()
exec(
"git-clone-android-plugin",
workDir = projectHome, timeout = 10.minutes,
args = scriptContent.split(" "),
stdoutRedirect = stdout,
stderrRedirect = stderr
)
logOutput(stdout.read().trim())
}
return this
}

View File

@@ -1,9 +0,0 @@
package com.intellij.ide.starter.build.tool
import com.intellij.ide.starter.ide.IDETestContext
/**
* Handles different stuff, that is related to that particular build tool
*/
open class BuildTool(val type: BuildToolType, val testContext: IDETestContext)

View File

@@ -1,9 +0,0 @@
package com.intellij.ide.starter.build.tool
import com.intellij.ide.starter.ide.IDETestContext
open class BuildToolDefaultProvider(testContext: IDETestContext) : BuildToolProvider(testContext) {
override val maven: MavenBuildTool = MavenBuildTool(testContext)
override val gradle: GradleBuildTool = GradleBuildTool(testContext)
}

View File

@@ -1,9 +0,0 @@
package com.intellij.ide.starter.build.tool
import com.intellij.ide.starter.ide.IDETestContext
abstract class BuildToolProvider(val testContext: IDETestContext) {
abstract val maven: MavenBuildTool
abstract val gradle: GradleBuildTool
}

View File

@@ -1,6 +0,0 @@
package com.intellij.ide.starter.build.tool
enum class BuildToolType {
MAVEN,
GRADLE
}

View File

@@ -1,110 +0,0 @@
package com.intellij.ide.starter.build.tool
import com.intellij.ide.starter.ide.IDETestContext
import com.intellij.ide.starter.utils.logError
import com.intellij.ide.starter.utils.logOutput
import org.w3c.dom.Element
import java.io.File
import java.io.FileOutputStream
import java.nio.file.Path
import java.util.stream.IntStream
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
import kotlin.io.path.*
open class GradleBuildTool(testContext: IDETestContext) : BuildTool(BuildToolType.GRADLE, testContext) {
val localGradleRepo: Path
get() = testContext.paths.tempDir.resolve("gradle")
fun useNewGradleLocalCache(): GradleBuildTool {
localGradleRepo.toFile().mkdirs()
testContext.addVMOptionsPatch { addSystemProperty("gradle.user.home", localGradleRepo.toString()) }
return this
}
fun removeGradleConfigFiles(): GradleBuildTool {
logOutput("Removing Gradle config files in ${testContext.resolvedProjectHome} ...")
testContext.resolvedProjectHome.toFile().walkTopDown()
.forEach {
if (it.isFile && (it.extension == "gradle" || (it.name in listOf("gradlew", "gradlew.bat", "gradle.properties")))) {
it.delete()
logOutput("File ${it.path} is deleted")
}
}
return this
}
fun addPropertyToGradleProperties(property: String, value: String): GradleBuildTool {
val projectDir = testContext.resolvedProjectHome
val gradleProperties = projectDir.resolve("gradle.properties")
val lineWithTheSameProperty = gradleProperties.readLines().singleOrNull { it.contains(property) }
if (lineWithTheSameProperty != null) {
if (lineWithTheSameProperty.contains(value)) {
return this
}
val newValue = lineWithTheSameProperty.substringAfter("$property=") + " $value"
val tempFile = File.createTempFile("newContent", ".txt").toPath()
gradleProperties.forEachLine { line ->
tempFile.appendText(when {
line.contains(property) -> "$property=$newValue" + System.getProperty("line.separator")
else -> line + System.getProperty("line.separator")
})
}
gradleProperties.writeText(tempFile.readText())
}
else {
gradleProperties.appendLines(listOf("$property=$value"))
}
return this
}
fun setGradleJvmInProject(useJavaHomeAsGradleJvm: Boolean = true): GradleBuildTool {
try {
val ideaDir = testContext.resolvedProjectHome.resolve(".idea")
val gradleXml = ideaDir.resolve("gradle.xml")
if (gradleXml.toFile().exists()) {
val xmlDoc = DocumentBuilderFactory.newDefaultInstance().newDocumentBuilder().parse(gradleXml.toFile())
xmlDoc.documentElement.normalize()
val gradleSettings = xmlDoc.getElementsByTagName("GradleProjectSettings")
if (gradleSettings.length == 1) {
val options = (gradleSettings.item(0) as Element).getElementsByTagName("option")
IntStream
.range(0, options.length)
.mapToObj { i -> options.item(i) as Element }
.filter { it.getAttribute("name") == "gradleJvm" }
.findAny()
.ifPresent { node -> gradleSettings.item(0).removeChild(node) }
if (useJavaHomeAsGradleJvm) {
val option = xmlDoc.createElement("option")
option.setAttribute("name", "gradleJvm")
option.setAttribute("value", "#JAVA_HOME")
gradleSettings.item(0).appendChild(option)
}
val source = DOMSource(xmlDoc)
val outputStream = FileOutputStream(gradleXml.toFile())
val result = StreamResult(outputStream)
val transformerFactory = TransformerFactory.newInstance()
val transformer = transformerFactory.newTransformer()
transformer.transform(source, result)
outputStream.close()
}
}
}
catch (e: Exception) {
logError(e)
}
return this
}
}

View File

@@ -1,30 +0,0 @@
package com.intellij.ide.starter.build.tool
import com.intellij.ide.starter.ide.IDETestContext
import com.intellij.ide.starter.utils.logOutput
import java.nio.file.Path
open class MavenBuildTool(testContext: IDETestContext) : BuildTool(BuildToolType.MAVEN, testContext) {
val localMavenRepo: Path
get() = testContext.paths.tempDir.resolve(".m3").resolve("repository")
fun useNewMavenLocalRepository(): MavenBuildTool {
localMavenRepo.toFile().mkdirs()
testContext.addVMOptionsPatch { addSystemProperty("idea.force.m2.home", localMavenRepo.toString()) }
return this
}
fun removeMavenConfigFiles(): MavenBuildTool {
logOutput("Removing Maven config files in ${testContext.resolvedProjectHome} ...")
testContext.resolvedProjectHome.toFile().walkTopDown()
.forEach {
if (it.isFile && it.name == "pom.xml") {
it.delete()
logOutput("File ${it.path} is deleted")
}
}
return this
}
}

View File

@@ -1,6 +0,0 @@
package com.intellij.ide.starter.bus
open class Event<T>(
state: EventState = EventState.UNDEFINED,
val data: T
) : Signal(state)

View File

@@ -1,14 +0,0 @@
package com.intellij.ide.starter.bus
/**
* @author https://github.com/Kosert/FlowBus
* @license Apache 2.0 https://github.com/Kosert/FlowBus/blob/master/LICENSE
* */
interface EventCallback<T> {
/**
* This function will be called for received event
*/
fun onEvent(event: T)
}

View File

@@ -1,11 +0,0 @@
package com.intellij.ide.starter.bus
enum class EventState {
UNDEFINED,
/** Right before the action */
BEFORE,
/** After the action was completed */
AFTER
}

View File

@@ -1,103 +0,0 @@
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
/**
* @author https://github.com/Kosert/FlowBus
* @license Apache 2.0 https://github.com/Kosert/FlowBus/blob/master/LICENSE
* Class for receiving events posted to [FlowBus]
*
* @param bus [FlowBus] instance to subscribe to. If not set, [StarterBus] will be used
*/
open class EventsReceiver @JvmOverloads constructor(
private val bus: FlowBus = StarterBus
) {
private val jobs = mutableMapOf<Class<*>, Job>()
private var returnDispatcher: CoroutineDispatcher = Dispatchers.Default
/**
* Set the `CoroutineDispatcher` which will be used to launch your callbacks.
*
* If this [EventsReceiver] was created on the main thread the default dispatcher will be [Dispatchers.Main].
* In any other case [Dispatchers.Default] will be used.
*/
fun returnOn(dispatcher: CoroutineDispatcher): EventsReceiver {
returnDispatcher = dispatcher
return this
}
/**
* Subscribe to events that are type of [clazz] with the given [callback] function.
* The [callback] can be called immediately if event of type [clazz] is present in the flow.
*
* @param clazz Type of event to subscribe to
* @param skipRetained Skips event already present in the flow. This is `false` by default
* @param callback The callback function
* @return This instance of [EventsReceiver] for chaining
*/
@JvmOverloads
fun <T : Any> subscribeTo(
clazz: Class<T>,
skipRetained: Boolean = false,
callback: suspend (event: T) -> Unit
): EventsReceiver {
if (jobs.containsKey(clazz))
throw IllegalArgumentException("Already subscribed for event type: $clazz")
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
throw throwable
}
val job = CoroutineScope(Job() + Dispatchers.Default + exceptionHandler).launch {
bus.forEvent(clazz)
.drop(if (skipRetained) 1 else 0)
.filterNotNull()
.collect {
catchAll {
withContext(returnDispatcher) { callback(it) }
}
}
}
jobs[clazz] = job
return this
}
/**
* A variant of [subscribeTo] that uses an instance of [EventCallback] as callback.
*
* @param clazz Type of event to subscribe to
* @param skipRetained Skips event already present in the flow. This is `false` by default
* @param callback Interface with implemented callback function
* @return This instance of [EventsReceiver] for chaining
* @see [subscribeTo]
*/
@JvmOverloads
fun <T : Any> subscribeTo(
clazz: Class<T>,
callback: EventCallback<T>,
skipRetained: Boolean = false
): EventsReceiver = subscribeTo(clazz, skipRetained) { callback.onEvent(it) }
/**
* Unsubscribe from events type of [clazz]
*/
fun <T : Any> unsubscribe(clazz: Class<T>) {
jobs.remove(clazz)?.cancel()
}
/**
* Unsubscribe from all events
*/
fun unsubscribe() {
jobs.values.forEach { it.cancel() }
jobs.clear()
}
}

View File

@@ -1,40 +0,0 @@
package com.intellij.ide.starter.bus
/**
* @author https://github.com/Kosert/FlowBus
* @license Apache 2.0 https://github.com/Kosert/FlowBus/blob/master/LICENSE
**/
/**
* @see FlowBus.dropEvent
*/
inline fun <reified T : Any> FlowBus.dropEvent() = dropEvent(T::class.java)
/**
* @see FlowBus.getFlow
*/
inline fun <reified T : Any> FlowBus.getFlow() = getFlow(T::class.java)
/**
* Simplified [EventsReceiver.subscribeTo] for Kotlin.
* Type of event is automatically inferred from [callback] parameter type.
*
* @param skipRetained Skips event already present in the flow. This is `false` by default
* @param callback The callback function
* @return This instance of [EventsReceiver] for chaining
*/
inline fun <reified T : Any> EventsReceiver.subscribe(skipRetained: Boolean = false, noinline callback: suspend (event: T) -> Unit): EventsReceiver {
return subscribeTo(T::class.java, skipRetained, callback)
}
/**
* A variant of [subscribe] that uses an instance of [EventCallback] as callback.
*
* @param skipRetained Skips event already present in the flow. This is `false` by default
* @param callback Interface with implemented callback function
* @return This instance of [EventsReceiver] for chaining
* @see [subscribe]
*/
inline fun <reified T : Any> EventsReceiver.subscribe(callback: EventCallback<T>, skipRetained: Boolean = false): EventsReceiver {
return subscribeTo(T::class.java, callback, skipRetained)
}

View File

@@ -1,82 +0,0 @@
package com.intellij.ide.starter.bus
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
/**
* @author https://github.com/Kosert/FlowBus
* @license Apache 2.0 https://github.com/Kosert/FlowBus/blob/master/LICENSE
*
* This class holds all shared flows and handles event posting.
* You can use [StarterBus] that is just plain instance of this class or create your own implementation.
*/
open class FlowBus {
private val flows = mutableMapOf<Class<*>, MutableSharedFlow<*>>()
/**
* Gets a MutableSharedFlow for events of the given type. Creates new if one doesn't exist.
* @return MutableSharedFlow for events that are instances of clazz
*/
internal fun <T : Any> forEvent(clazz: Class<T>): MutableSharedFlow<T?> {
return flows.getOrPut(clazz) {
MutableSharedFlow<T?>(extraBufferCapacity = 5000)
} as MutableSharedFlow<T?>
}
/**
* Gets a Flow for events of the given type.
*
* **This flow never completes.**
*
* The returned Flow is _hot_ as it is based on a [SharedFlow]. This means a call to [collect] never completes normally, calling [toList] will suspend forever, etc.
*
* You are entirely responsible to cancel this flow. To cancel this flow, the scope in which the coroutine is running needs to be cancelled.
* @see [SharedFlow]
*/
fun <T : Any> getFlow(clazz: Class<T>): Flow<T> {
return forEvent(clazz).filterNotNull()
}
/**
* Posts new event to SharedFlow of the [event] type.
* @param retain If the [event] should be retained in the flow for future subscribers. This is true by default.
*/
@JvmOverloads
fun <T : Any> post(event: T, retain: Boolean = true) {
val flow = forEvent(event.javaClass)
flow.tryEmit(event).also {
if (!it)
throw IllegalStateException("SharedFlow cannot take element, this should never happen")
}
if (!retain) {
// without starting a coroutine here, the event is dropped immediately
// and not delivered to subscribers
CoroutineScope(Job() + Dispatchers.Default).launch {
dropEvent(event.javaClass)
}
}
}
/**
* Removes retained event of type [clazz]
*/
fun <T> dropEvent(clazz: Class<T>) {
if (!flows.contains(clazz)) return
val channel = flows[clazz] as MutableSharedFlow<T?>
channel.tryEmit(null)
}
/**
* Removes all retained events
*/
fun dropAll() {
flows.values.forEach {
(it as MutableSharedFlow<Any?>).tryEmit(null)
}
}
}

View File

@@ -1,3 +0,0 @@
package com.intellij.ide.starter.bus
open class Signal(val state: EventState = EventState.UNDEFINED)

View File

@@ -1,3 +0,0 @@
package com.intellij.ide.starter.bus
object StarterBus : FlowBus()

View File

@@ -1,3 +0,0 @@
package com.intellij.ide.starter.bus
object StarterListener : EventsReceiver()

View File

@@ -1,17 +0,0 @@
package com.intellij.ide.starter.ci
import java.nio.file.Path
//TODO(Describe different approaches for different CIs about publishing artifacts)
interface CIServer {
val isBuildRunningOnCI: Boolean
val buildNumber: String
val branchName: String
val buildParams: Map<String, String>
fun publishArtifact(source: Path,
artifactPath: String,
artifactName: String = source.fileName.toString())
fun reportTestFailure(testName: String, message: String, details: String)
}

View File

@@ -1,20 +0,0 @@
package com.intellij.ide.starter.ci
import com.intellij.ide.starter.utils.logError
import java.nio.file.Path
/** Dummy for CI server */
object NoCIServer : CIServer {
override val isBuildRunningOnCI: Boolean = false
override val buildNumber: String = ""
override val branchName: String = ""
override val buildParams: Map<String, String> = mapOf()
override fun publishArtifact(source: Path, artifactPath: String, artifactName: String) {
logError("No logic for publishing artifacts has been implemented")
}
override fun reportTestFailure(testName: String, message: String, details: String) {
logError("No logic for reporting test failure has been implemented")
}
}

View File

@@ -1,30 +0,0 @@
package com.intellij.ide.starter.community
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.intellij.ide.starter.community.model.ReleaseInfo
import com.intellij.ide.starter.utils.HttpClient
import org.apache.http.client.methods.HttpGet
class JetBrainsDataServiceClient {
companion object {
private const val DATA_SERVICE_URL = "https://data.services.jetbrains.com"
fun getReleases(request: ProductInfoRequestParameters): Map<String, List<ReleaseInfo>> {
return HttpClient.sendRequest(
HttpGet("$DATA_SERVICE_URL/products/releases$request").apply {
addHeader("Content-Type", "application/json")
addHeader("Accept", "application/json")
}) {
jacksonObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModule(JavaTimeModule())
.readValue(it.entity.content, object : TypeReference<Map<String, List<ReleaseInfo>>>() {})
}
}
}
}

View File

@@ -1,23 +0,0 @@
package com.intellij.ide.starter.community
import org.apache.http.client.utils.URIBuilder
data class ProductInfoRequestParameters(val code: String,
val type: String = "release",
val majorVersion: String = "",
val build: String = "") {
fun toUriQuery(): URIBuilder {
val builder = URIBuilder()
if (code.isNotBlank()) builder.addParameter("code", code)
if (type.isNotBlank()) builder.addParameter("type", type)
if (majorVersion.isNotBlank()) builder.addParameter("majorVersion", majorVersion)
if (build.isNotBlank()) builder.addParameter("build", build)
return builder
}
override fun toString(): String {
return toUriQuery().toString()
}
}

View File

@@ -1,52 +0,0 @@
package com.intellij.ide.starter.community
import com.intellij.ide.starter.ide.IdeDownloader
import com.intellij.ide.starter.ide.IdeInstaller
import com.intellij.ide.starter.models.IdeInfo
import com.intellij.ide.starter.system.OsType
import com.intellij.ide.starter.system.SystemInfo
import com.intellij.ide.starter.utils.HttpClient
import com.intellij.ide.starter.utils.logOutput
import java.nio.file.Path
import kotlin.io.path.exists
object PublicIdeDownloader : IdeDownloader {
override fun downloadIdeInstaller(ideInfo: IdeInfo, installerDirectory: Path): IdeInstaller {
val releaseInfoMap = JetBrainsDataServiceClient.getReleases(
ProductInfoRequestParameters(code = ideInfo.productCode,
type = ideInfo.buildType,
build = ideInfo.buildNumber)
)
if (releaseInfoMap.size == 1) {
//Find the latest build
val possibleBuild = releaseInfoMap.values.first().sortedByDescending { it.date }.first()
val downloadLink: String = when (SystemInfo.getOsType()) {
OsType.Linux -> possibleBuild.downloads.linux!!.link
OsType.MacOS -> {
if (SystemInfo.OS_ARCH == "aarch64") possibleBuild.downloads.macM1!!.link // macM1
else possibleBuild.downloads.mac!!.link
}
OsType.Windows -> possibleBuild.downloads.windows!!.link
else -> throw RuntimeException("Unsupported OS ${SystemInfo.getOsType()}")
}
val installerFile = installerDirectory.resolve(
"${ideInfo.installerFilePrefix}-" + possibleBuild.build.replace(".", "") + ideInfo.installerFileExt
)
if (!installerFile.exists()) {
logOutput("Downloading $ideInfo ...")
HttpClient.download(downloadLink, installerFile)
}
else logOutput("Installer file $installerFile already exists. Skipping download.")
return IdeInstaller(installerFile, possibleBuild.build)
}
else {
throw RuntimeException("Only one product can be handled. Found ${releaseInfoMap.keys}")
}
}
}

View File

@@ -1,6 +0,0 @@
package com.intellij.ide.starter.community.model
enum class BuildType(val type: String) {
RELEASE("release"),
EAP("eap")
}

View File

@@ -1,3 +0,0 @@
package com.intellij.ide.starter.community.model
data class IdeModel(val releaseInfo: Map<Int, ReleaseInfo>)

View File

@@ -1,19 +0,0 @@
package com.intellij.ide.starter.community.model
import java.time.LocalDate
data class ReleaseInfo(val date: LocalDate,
val type: String,
val version: String,
val majorVersion: String,
val build: String,
val downloads: Download)
data class Download(val linux: OperatingSystem?,
val mac: OperatingSystem?,
val macM1: OperatingSystem?,
val windows: OperatingSystem?)
data class OperatingSystem(val link: String)

View File

@@ -1,9 +0,0 @@
package com.intellij.ide.starter.data
import com.intellij.ide.starter.models.IdeInfo
import com.intellij.ide.starter.models.TestCase
abstract class TestCaseTemplate(val ideInfo: IdeInfo) {
protected fun getTemplate() = TestCase(ideInfo = ideInfo)
}

View File

@@ -1,55 +0,0 @@
package com.intellij.ide.starter.di
import com.intellij.ide.starter.build.tool.BuildToolDefaultProvider
import com.intellij.ide.starter.build.tool.BuildToolProvider
import com.intellij.ide.starter.ci.CIServer
import com.intellij.ide.starter.ci.NoCIServer
import com.intellij.ide.starter.community.PublicIdeDownloader
import com.intellij.ide.starter.ide.*
import com.intellij.ide.starter.models.IdeInfo
import com.intellij.ide.starter.models.IdeProduct
import com.intellij.ide.starter.models.IdeProductImp
import com.intellij.ide.starter.path.GlobalPaths
import com.intellij.ide.starter.path.InstallerGlobalPaths
import com.intellij.ide.starter.plugins.PluginConfigurator
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.utils.logOutput
import org.kodein.di.DI
import org.kodein.di.bindFactory
import org.kodein.di.bindSingleton
/**
* Reinitialize / override bindings for this DI container in your module before executing tests
* https://docs.kodein.org/kodein-di/7.9/core/bindings.html
*
* E.g:
* ```
* di = DI {
* extend(di)
* bindSingleton<GlobalPaths>(overrides = true) { YourImplementationOfPaths() }
* }
* ```
* */
var di = DI {
bindSingleton<GlobalPaths> { InstallerGlobalPaths() }
bindSingleton<CIServer> { NoCIServer }
bindSingleton<CodeInjector> { CodeBuilderHost() }
bindFactory { testContext: IDETestContext -> PluginConfigurator(testContext) }
bindSingleton<IdeDownloader> { PublicIdeDownloader }
bindFactory<IdeInfo, IdeInstallator> { ideInfo ->
if (ideInfo.productCode == "AI") {
AndroidInstaller()
}
else {
SimpleInstaller()
}
}
bindFactory<IDETestContext, BuildToolProvider> { testContext: IDETestContext -> BuildToolDefaultProvider(testContext) }
bindSingleton<List<ReportPublisher>> { listOf(ConsoleTestResultPublisher, QodanaTestResultPublisher) }
bindSingleton<IdeProduct> { IdeProductImp }
}.apply {
logOutput("DI was initialized")
}

View File

@@ -1,101 +0,0 @@
package com.intellij.ide.starter.exec
import com.intellij.ide.starter.exec.ExecOutputRedirect.*
import com.intellij.ide.starter.utils.logOutput
import java.io.File
import java.io.PrintWriter
import kotlin.io.path.createDirectories
/**
* Specifies how a child process' stdout or stderr must be redirected in the current process:
* - [NoRedirect]
* - [ToFile]
* - [ToStdOut]
*/
sealed class ExecOutputRedirect {
open fun open() = Unit
open fun close() = Unit
open fun redirectLine(line: String) = Unit
abstract fun read(): String
abstract override fun toString(): String
protected fun reportOnStdoutIfNecessary(line: String) {
// Propagate the IDE debugger attach service message.
if (line.contains("Listening for transport dt_socket")) {
println(line)
}
}
object NoRedirect : ExecOutputRedirect() {
override fun redirectLine(line: String) {
reportOnStdoutIfNecessary(line)
}
override fun read() = ""
override fun toString() = "ignored"
}
data class ToFile(val outputFile: File) : ExecOutputRedirect() {
private lateinit var writer: PrintWriter
override fun open() {
outputFile.apply {
toPath().parent.createDirectories()
createNewFile()
}
writer = outputFile.printWriter()
}
override fun close() {
writer.close()
}
override fun redirectLine(line: String) {
reportOnStdoutIfNecessary(line)
writer.println(line)
}
override fun read(): String {
if (!outputFile.exists()) {
logOutput("File $outputFile doesn't exist")
return ""
}
return outputFile.readText()
}
override fun toString() = "file $outputFile"
}
data class ToStdOut(val prefix: String) : ExecOutputRedirect() {
override fun redirectLine(line: String) {
reportOnStdoutIfNecessary(line)
logOutput(" $prefix $line")
}
override fun read() = ""
override fun toString() = "stdout"
}
class ToString : ExecOutputRedirect() {
private val stringBuilder = StringBuilder()
override fun redirectLine(line: String) {
reportOnStdoutIfNecessary(line)
stringBuilder.appendLine(line)
}
override fun read() = stringBuilder.toString()
override fun toString() = "string"
}
}

View File

@@ -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<String, String> = 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<String, String> = System.getenv(),
args: List<String>,
errorDiagnosticFiles: List<Path> = 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<String>) {
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
)
}

View File

@@ -1,35 +0,0 @@
package com.intellij.ide.starter
import com.intellij.ide.starter.di.di
import com.intellij.ide.starter.path.GlobalPaths
import com.intellij.ide.starter.system.SystemInfo
import com.intellij.ide.starter.utils.FileSystem
import com.intellij.ide.starter.utils.HttpClient
import org.kodein.di.direct
import org.kodein.di.instance
import java.nio.file.Path
fun downloadGoSdk(version: String): Path {
val os = when {
SystemInfo.isWindows -> "windows"
SystemInfo.isLinux -> "linux"
SystemInfo.isMac -> "darwin"
else -> error("Unknown OS")
}
val extension = when {
SystemInfo.isWindows -> ".zip"
SystemInfo.isLinux || SystemInfo.isMac -> ".tar.gz"
else -> error("Unknown OS")
}
val url = "https://cache-redirector.jetbrains.com/dl.google.com/go/go$version.$os-amd64$extension"
val dirToDownload = di.direct.instance<GlobalPaths>().getCacheDirectoryFor("go-sdk/$version")
val downloadedFile = dirToDownload.resolve("go$version.$os-amd64$extension")
val goRoot = dirToDownload.resolve("go-roots")
if (goRoot.toFile().exists()) {
return goRoot.resolve("go")
}
HttpClient.download(url, downloadedFile)
FileSystem.unpack(downloadedFile, goRoot)
return goRoot.resolve("go")
}

View File

@@ -1,28 +0,0 @@
package com.intellij.ide.starter.ide
import com.intellij.ide.starter.downloadAndroidStudio
import com.intellij.ide.starter.models.IdeInfo
import com.intellij.ide.starter.system.SystemInfo
import java.io.File
import java.nio.file.Path
class AndroidInstaller : IdeInstallator {
override fun install(ideInfo: IdeInfo): Pair<String, InstalledIde> {
val installDir: Path
val installerFile: File
downloadAndroidStudio().also {
installDir = it.first
installerFile = it.second
}
IdeArchiveExtractor.unpackIdeIfNeeded(installerFile, installDir.toFile())
val installationPath = when (!SystemInfo.isMac) {
true -> installDir.resolve("android-studio")
false -> installDir
}
val ide = IdeDistributionFactory.installIDE(installationPath.toFile(), ideInfo.executableFileName)
return Pair(ide.build, ide)
}
}

View File

@@ -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<BuildNumber> {
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");
}
}

View File

@@ -1,21 +0,0 @@
package com.intellij.ide.starter.ide
import com.intellij.ide.starter.models.VMOptionsDiff
import java.nio.file.Path
interface IDEStartConfig {
val workDir: Path
val environmentVariables: Map<String, String>
get() = System.getenv().filterKeys {
// don't inherit these environment variables from parent process
it != "IDEA_PROPERTIES" && !it.endsWith("VM_OPTIONS") && it != "JAVA_HOME" && !it.endsWith("_JDK")
}
val commandLine: List<String>
fun vmOptionsDiff(): VMOptionsDiff? = null
val errorDiagnosticFiles: List<Path>
get() = emptyList()
}

View File

@@ -1,548 +0,0 @@
package com.intellij.ide.starter.ide
import com.intellij.ide.starter.build.tool.BuildToolProvider
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.path.IDEDataPaths
import com.intellij.ide.starter.plugins.PluginConfigurator
import com.intellij.ide.starter.profiler.ProfilerType
import com.intellij.ide.starter.report.publisher.ReportPublisher
import com.intellij.ide.starter.runner.IDECommandLine
import com.intellij.ide.starter.runner.IDERunContext
import com.intellij.ide.starter.system.SystemInfo
import com.intellij.ide.starter.utils.logOutput
import org.kodein.di.direct
import org.kodein.di.factory
import org.kodein.di.instance
import org.kodein.di.newInstance
import org.w3c.dom.Node
import org.w3c.dom.NodeList
import java.io.FileOutputStream
import java.nio.file.Path
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
import kotlin.io.path.*
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
data class IDETestContext(
val paths: IDEDataPaths,
val ide: InstalledIde,
val testCase: TestCase,
val testName: String,
private val _resolvedProjectHome: Path?,
var patchVMOptions: VMOptions.() -> VMOptions,
val ciServer: CIServer,
var profilerType: ProfilerType = ProfilerType.NONE,
val publishers: List<ReportPublisher> = di.direct.instance(),
var isReportPublishingEnabled: Boolean = false
) {
companion object {
const val TEST_RESULT_FILE_PATH_PROPERTY = "test.result.file.path"
const val OPENTELEMETRY_FILE = "opentelemetry.json"
}
val resolvedProjectHome: Path
get() = checkNotNull(_resolvedProjectHome) { "Project is not found for the test $testName" }
val pluginConfigurator: PluginConfigurator by di.newInstance { factory<IDETestContext, PluginConfigurator>().invoke(this@IDETestContext) }
val buildTools: BuildToolProvider by di.newInstance { factory<IDETestContext, BuildToolProvider>().invoke(this@IDETestContext) }
fun addVMOptionsPatch(patchVMOptions: VMOptions.() -> VMOptions): IDETestContext {
this.patchVMOptions = this.patchVMOptions.andThen(patchVMOptions)
return this
}
fun addLockFileForUITest(fileName: String): IDETestContext =
addVMOptionsPatch {
addSystemProperty("uiLockTempFile", paths.tempDir / fileName)
}
fun disableJcef(): IDETestContext =
addVMOptionsPatch {
// Disable JCEF (IDEA-243147). Otherwise, tests will fail with LOG.error
addSystemProperty("ide.browser.jcef.enabled", false)
}
fun disableLinuxNativeMenuForce(): IDETestContext =
addVMOptionsPatch {
addSystemProperty("linux.native.menu.force.disable", true)
}
fun setMemorySize(sizeMb: Int): IDETestContext =
addVMOptionsPatch {
this
.withXmx(sizeMb)
}
fun disableGitLogIndexing(): IDETestContext =
addVMOptionsPatch {
addSystemProperty("vcs.log.index.git", false)
}
fun executeAfterProjectOpening(executeAfterProjectOpening: Boolean = true) = addVMOptionsPatch {
addSystemProperty("performance.execute.script.after.project.opened", executeAfterProjectOpening)
}
fun executeDuringIndexing(): IDETestContext =
addVMOptionsPatch {
addSystemProperty("performance.execute.script.after.scanning", true)
}
fun withGtk2OnLinux(): IDETestContext =
addVMOptionsPatch {
this
.addSystemProperty("jdk.gtk.verbose", true)
.let {
// Desperate attempt to fix JBR-2783
if (SystemInfo.isLinux) {
it.addSystemProperty("jdk.gtk.version", 2)
}
else {
it
}
}
}
fun disableInstantIdeShutdown(): IDETestContext =
addVMOptionsPatch {
addSystemProperty("ide.instant.shutdown", false)
}
fun enableSlowOperationsInEdtInTests(): IDETestContext =
addVMOptionsPatch {
addSystemProperty("ide.slow.operations.assertion", false)
}
/**
* Does not allow IDE to fork a process that sends FUS statistics on IDE shutdown.
* On Windows that forked process may prevent some files from removing.
* See [com.intellij.internal.statistic.EventLogApplicationLifecycleListener]
*/
fun disableFusSendingOnIdeClose(): IDETestContext =
addVMOptionsPatch {
addSystemProperty("feature.usage.event.log.send.on.ide.close", false)
}
fun withVerboseIndexingDiagnostics(dumpPaths: Boolean = false): IDETestContext =
addVMOptionsPatch {
this
.addSystemProperty("intellij.indexes.diagnostics.should.dump.for.interrupted.index.updaters", true)
.addSystemProperty("intellij.indexes.diagnostics.limit.of.files", 10000)
.addSystemProperty("intellij.indexes.diagnostics.should.dump.paths.of.indexed.files", dumpPaths)
// Dumping of lists of indexed file paths may require a lot of memory.
.withXmx(4 * 1024)
}
fun setPathForMemorySnapshot(): IDETestContext =
addVMOptionsPatch {
addSystemProperty("memory.snapshots.path", paths.logsDir)
}
fun setPathForSnapshots(): IDETestContext =
addVMOptionsPatch {
addSystemProperty("snapshots.path", paths.snapshotsDir)
}
fun collectMemorySnapshotOnFailedPluginUnload(): IDETestContext =
addVMOptionsPatch {
addSystemProperty("ide.plugins.snapshot.on.unload.fail", true)
}
fun setPerProjectDynamicPluginsFlag(): IDETestContext =
addVMOptionsPatch {
addSystemProperty("ide.plugins.per.project", true)
}
fun disableAutoImport(disabled: Boolean = true) = addVMOptionsPatch {
addSystemProperty("external.system.auto.import.disabled", disabled)
}
fun disableOrdinaryIndexes() = addVMOptionsPatch {
addSystemProperty("idea.use.only.index.infrastructure.extension", true)
}
fun setSharedIndexesDownload(enable: Boolean = true) = addVMOptionsPatch {
addSystemProperty("shared.indexes.bundled", enable)
.addSystemProperty("shared.indexes.download", enable)
.addSystemProperty("shared.indexes.download.auto.consent", enable)
}
fun skipIndicesInitialization() = addVMOptionsPatch {
addSystemProperty("idea.skip.indices.initialization", true)
}
fun addTestResultFilePath() = addVMOptionsPatch {
addSystemProperty(TEST_RESULT_FILE_PATH_PROPERTY, paths.tempDir.resolve("testResult.txt"))
}
fun collectImportProjectPerfMetrics() = addVMOptionsPatch {
addSystemProperty("idea.collect.project.import.performance", true)
}
fun collectOpenTelemetry() = addVMOptionsPatch {
addSystemProperty("idea.diagnostic.opentelemetry.file", paths.logsDir.resolve(OPENTELEMETRY_FILE))
}
fun enableWorkspaceModelVerboseLogs() = addVMOptionsPatch {
configureLoggers(traceLoggers = listOf("com.intellij.workspaceModel"))
}
fun wipeSystemDir() = apply {
//TODO: it would be better to allocate a new context instead of wiping the folder
logOutput("Cleaning system dir for $this at $paths")
paths.systemDir.toFile().deleteRecursively()
}
fun wipeLogsDir() = apply {
//TODO: it would be better to allocate a new context instead of wiping the folder
logOutput("Cleaning logs dir for $this at $paths")
paths.logsDir.toFile().deleteRecursively()
}
fun wipeProjectsDir() = apply {
val path = paths.systemDir / "projects"
logOutput("Cleaning project cache dir for $this at $path")
path.toFile().deleteRecursively()
}
fun wipeEventLogDataDir() = apply {
val path = paths.systemDir / "event-log-data"
logOutput("Cleaning event-log-data dir for $this at $path")
path.toFile().deleteRecursively()
}
fun wipeSnapshotDir() = apply {
val path = paths.snapshotsDir
logOutput("Cleaning snapshot dir for $this at $path")
path.toFile().deleteRecursively()
}
fun runContext(
patchVMOptions: VMOptions.() -> VMOptions = { this },
commandLine: IDECommandLine? = null,
commands: Iterable<MarshallableCommand> = CommandChain(),
codeBuilder: (CodeInjector.() -> Unit)? = null,
runTimeout: Duration = 10.minutes,
useStartupScript: Boolean = true,
launchName: String = "",
expectedKill: Boolean = false,
collectNativeThreads: Boolean = false
): IDERunContext {
return IDERunContext(testContext = this)
.copy(
commandLine = commandLine,
commands = commands,
codeBuilder = codeBuilder,
runTimeout = runTimeout,
useStartupScript = useStartupScript,
launchName = launchName,
expectedKill = expectedKill,
collectNativeThreads = collectNativeThreads
)
.addVMOptionsPatch(patchVMOptions)
}
/**
* Setup profiler injection
*/
fun setProfiler(profilerType: ProfilerType): IDETestContext {
this.profilerType = profilerType
return this
}
fun internalMode() = addVMOptionsPatch { addSystemProperty("idea.is.internal", true) }
fun prepareProjectCleanImport(): IDETestContext {
return removeIdeaProjectDirectory().removeAllImlFilesInProject()
}
fun disableAutoSetupJavaProject() = addVMOptionsPatch {
addSystemProperty("idea.java.project.setup.disabled", true)
}
fun disablePackageSearchBuildFiles() = addVMOptionsPatch {
addSystemProperty("idea.pkgs.disableLoading", true)
}
fun removeIdeaProjectDirectory(): IDETestContext {
val ideaDirPath = resolvedProjectHome.resolve(".idea")
logOutput("Removing $ideaDirPath ...")
if (ideaDirPath.notExists()) {
logOutput("Idea project directory $ideaDirPath doesn't exist. So, it will not be deleted")
return this
}
ideaDirPath.toFile().deleteRecursively()
return this
}
fun removeAllImlFilesInProject(): IDETestContext {
val projectDir = resolvedProjectHome
logOutput("Removing all .iml files in $projectDir ...")
projectDir.toFile().walkTopDown()
.forEach {
if (it.isFile && it.extension == "iml") {
it.delete()
logOutput("File ${it.path} is deleted")
}
}
return this
}
fun runIDE(
patchVMOptions: VMOptions.() -> VMOptions = { this },
commandLine: IDECommandLine? = null,
commands: Iterable<MarshallableCommand>,
codeBuilder: (CodeInjector.() -> Unit)? = null,
runTimeout: Duration = 10.minutes,
useStartupScript: Boolean = true,
launchName: String = "",
expectedKill: Boolean = false,
collectNativeThreads: Boolean = false
): IDEStartResult {
val ideRunResult = runContext(
commandLine = commandLine,
commands = commands,
codeBuilder = codeBuilder,
runTimeout = runTimeout,
useStartupScript = useStartupScript,
launchName = launchName,
expectedKill = expectedKill,
collectNativeThreads = collectNativeThreads,
patchVMOptions = patchVMOptions
).runIDE()
if (isReportPublishingEnabled) publishers.forEach {
it.publish(ideRunResult)
}
if (ideRunResult.failureError != null) throw ideRunResult.failureError
return ideRunResult
}
fun warmUp(
patchVMOptions: VMOptions.() -> VMOptions = { this },
commands: Iterable<MarshallableCommand>,
runTimeout: Duration = 10.minutes,
storeClassReport: Boolean = false
): IDEStartResult {
return runIDE(
patchVMOptions = {
val warmupReports = IDEStartupReports(paths.reportsDir / "warmUp")
if (storeClassReport) {
this.enableStartupPerformanceLog(warmupReports).enableClassLoadingReport(
paths.reportsDir / "warmUp" / "class-report.txt").patchVMOptions()
}
else {
this
}
},
commands = testCase.commands.plus(commands),
runTimeout = runTimeout,
launchName = "warmUp"
)
}
fun removeAndUnpackProject(): IDETestContext {
testCase.markNotReusable().projectInfo?.downloadAndUnpackProject()
return this
}
fun setProviderMemoryOnlyOnLinux(): IDETestContext {
if (SystemInfo.isLinux) {
val optionsConfig = paths.configDir.resolve("options")
optionsConfig.toFile().mkdirs()
val securityXml = optionsConfig.resolve("security.xml")
securityXml.toFile().createNewFile()
securityXml.toFile().writeText("""<application>
<component name="PasswordSafe">
<option name="PROVIDER" value="MEMORY_ONLY" />
</component>
</application>""")
}
return this
}
fun addBuildProcessProfiling(): IDETestContext {
if (_resolvedProjectHome != null) {
val ideaDir = resolvedProjectHome.resolve(".idea")
val workspace = ideaDir.resolve("workspace.xml")
if (workspace.toFile().exists()) {
val newContent = StringBuilder()
val readText = workspace.toFile().readText()
val userLocalBuildProcessVmOptions = when {
(testName.contains(
"intellij_sources")) -> "-Dprofiling.mode=true -Dgroovyc.in.process=true -Dgroovyc.asm.resolving.only=false"
else -> "-Dprofiling.mode=true"
}
if (readText.contains("CompilerWorkspaceConfiguration")) {
workspace.toFile().readLines().forEach {
if (it.contains("<component name=\"CompilerWorkspaceConfiguration\">")) {
val newLine = "<component name=\"CompilerWorkspaceConfiguration\">\n<option name=\"COMPILER_PROCESS_ADDITIONAL_VM_OPTIONS\" value=\"$userLocalBuildProcessVmOptions\" />"
newContent.appendLine(newLine)
}
else {
newContent.appendLine(it)
}
}
workspace.writeText(newContent.toString())
}
else {
val xmlDoc = DocumentBuilderFactory.newDefaultInstance().newDocumentBuilder().parse(workspace.toFile())
xmlDoc.documentElement.normalize()
val firstElement = xmlDoc.firstChild
val componentElement = xmlDoc.createElement("component")
componentElement.setAttribute("name", "CompilerWorkspaceConfiguration")
val optionElement = xmlDoc.createElement("option")
optionElement.setAttribute("name", "COMPILER_PROCESS_ADDITIONAL_VM_OPTIONS")
optionElement.setAttribute("value", userLocalBuildProcessVmOptions)
firstElement.appendChild(componentElement).appendChild(optionElement)
val source = DOMSource(xmlDoc)
val outputStream = FileOutputStream(workspace.toFile())
val result = StreamResult(outputStream)
val transformerFactory = TransformerFactory.newInstance()
val transformer = transformerFactory.newTransformer()
transformer.transform(source, result)
}
}
}
return this
}
fun checkThatBuildRunByIdea(): IDETestContext {
if (_resolvedProjectHome != null) {
val ideaDir = resolvedProjectHome.resolve(".idea")
val gradle = ideaDir.resolve("gradle.xml")
if (gradle.toFile().exists()) {
val readText = gradle.toFile().readText()
if (!readText.contains("<option name=\"delegatedBuild\" value=\"false\"/>")) {
val xmlDoc = DocumentBuilderFactory.newDefaultInstance().newDocumentBuilder().parse(gradle.toFile())
xmlDoc.documentElement.normalize()
val gradleProjectSettingsElements: NodeList = xmlDoc.getElementsByTagName("GradleProjectSettings")
if (gradleProjectSettingsElements.length == 1) {
for (i in 0 until gradleProjectSettingsElements.length) {
val component: Node = gradleProjectSettingsElements.item(i)
if (component.nodeType == Node.ELEMENT_NODE) {
val optionElement = xmlDoc.createElement("option")
optionElement.setAttribute("name", "delegatedBuild")
optionElement.setAttribute("value", "false")
component.appendChild(optionElement)
}
}
val source = DOMSource(xmlDoc)
val outputStream = FileOutputStream(gradle.toFile())
val result = StreamResult(outputStream)
val transformerFactory = TransformerFactory.newInstance()
val transformer = transformerFactory.newTransformer()
transformer.transform(source, result)
}
}
}
}
return this
}
fun setBuildProcessHeapSize(heapSizeValue: String): IDETestContext {
if (_resolvedProjectHome != null) {
val heapSize = when (heapSizeValue.isEmpty()) {
true -> "2000"
else -> heapSizeValue
}
val ideaDir = resolvedProjectHome.resolve(".idea")
val compilerXml = ideaDir.resolve("compiler.xml")
if (compilerXml.toFile().exists()) {
val newContent = StringBuilder()
val readText = compilerXml.toFile().readText()
if (!readText.contains("BUILD_PROCESS_HEAP_SIZE")) {
compilerXml.toFile().readLines().forEach {
if (it.contains("<component name=\"CompilerConfiguration\">")) {
val newLine = "<component name=\"CompilerConfiguration\">\n<option name=\"BUILD_PROCESS_HEAP_SIZE\" value=\"$heapSize\" />"
newContent.appendLine(newLine)
}
else {
newContent.appendLine(it)
}
}
compilerXml.writeText(newContent.toString())
}
else if (heapSizeValue.isNotEmpty()) {
compilerXml.toFile().readLines().forEach {
if (it.contains("BUILD_PROCESS_HEAP_SIZE")) {
val newLine = it.replace("value=\"\\d*\"".toRegex(), "value=\"$heapSize\"")
newContent.appendLine(newLine)
}
else {
newContent.appendLine(it)
}
}
compilerXml.writeText(newContent.toString())
}
}
}
return this
}
fun updateGeneralSettings(): IDETestContext {
val patchedIdeGeneralXml = this::class.java.classLoader.getResourceAsStream("ide.general.xml")
val pathToGeneralXml = paths.configDir.toAbsolutePath().resolve("options/ide.general.xml")
if (!pathToGeneralXml.exists()) {
pathToGeneralXml.parent.createDirectories()
patchedIdeGeneralXml.use {
if (it != null) {
pathToGeneralXml.writeBytes(it.readAllBytes())
}
}
}
return this
}
@Suppress("unused")
fun setLicense(pathToFileWithLicense: Path): IDETestContext {
val licenseKeyFileName: String = when(this.ide.productCode) {
"IU" -> "idea.key"
"RM" -> "rubymine.key"
"WS" -> "webstorm.key"
"PS" -> "phpstorm.key"
"GO" -> "goland.key"
"PY" -> "pycharm.key"
"DG" -> "datagrip.key"
else -> error("Setting license to the product ${this.ide.productCode} is not supported")
}
val keyFile = paths.configDir.resolve(licenseKeyFileName)
keyFile.toFile().createNewFile()
keyFile.toFile().writeText(pathToFileWithLicense.toFile().readText())
return this
}
fun publishArtifact(source: Path,
artifactPath: String = testName,
artifactName: String = source.fileName.toString()) = ciServer.publishArtifact(source, artifactPath, artifactName)
fun withReportPublishing(isEnabled: Boolean): IDETestContext {
isReportPublishingEnabled = isEnabled
return this
}
}

View File

@@ -1,107 +0,0 @@
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.utils.FileSystem
import com.intellij.ide.starter.utils.HttpClient
import com.intellij.ide.starter.utils.catchAll
import com.intellij.ide.starter.utils.logOutput
import org.kodein.di.direct
import org.kodein.di.instance
import java.io.File
import java.nio.file.Path
import kotlin.io.path.createDirectories
import kotlin.io.path.div
import kotlin.io.path.nameWithoutExtension
import kotlin.time.Duration.Companion.minutes
object IdeArchiveExtractor {
fun unpackIdeIfNeeded(ideInstallerFile: File, unpackDir: File) {
if (unpackDir.isDirectory && unpackDir.listFiles()?.isNotEmpty() == true) {
logOutput("Build directory $unpackDir already exists for the binary $ideInstallerFile")
return
}
logOutput("Extracting application into $unpackDir")
when {
ideInstallerFile.extension == "dmg" -> unpackDmg(ideInstallerFile, unpackDir.toPath())
ideInstallerFile.extension == "exe" -> unpackWin(ideInstallerFile, unpackDir)
ideInstallerFile.extension == "zip" -> FileSystem.unpack(ideInstallerFile.toPath(), unpackDir.toPath())
ideInstallerFile.name.endsWith(".tar.gz") -> FileSystem.unpackTarGz(ideInstallerFile, unpackDir)
else -> error("Unsupported build file: $ideInstallerFile")
}
}
private fun unpackDmg(dmgFile: File, target: Path): Path {
target.toFile().deleteRecursively()
target.createDirectories()
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"))
}
catch (t: Throwable) {
dmgFile.delete()
throw Error("Failed to mount $dmgFile. ${t.message}.", t)
}
try {
val appDir = mountDir.listFiles()?.singleOrNull { it.name.endsWith(".app") }
?: error("Failed to find the only one .app folder in $dmgFile")
val targetAppDir = target / appDir.name
exec(
presentablePurpose = "copy-dmg",
workDir = target,
timeout = 10.minutes,
stderrRedirect = ExecOutputRedirect.ToStdOut("cp"),
args = listOf("cp", "-R", "$appDir", "$targetAppDir"))
return targetAppDir
}
finally {
catchAll {
exec(
presentablePurpose = "hdiutil",
workDir = target,
timeout = 10.minutes,
stdoutRedirect = ExecOutputRedirect.ToStdOut("hdiutil"),
stderrRedirect = ExecOutputRedirect.ToStdOut("hdiutil"),
args = listOf("hdiutil", "detach", "-force", "$mountDir"))
}
}
}
private fun unpackWin(exeFile: File, targetDir: File) {
targetDir.deleteRecursively()
//we use 7-Zip to unpack NSIS binaries, same way as in Toolbox App
val sevenZipUrl = "https://repo.labs.intellij.net/thirdparty/7z-cmdline-15.06.zip"
val sevenZipCacheDir = di.direct.instance<GlobalPaths>().getCacheDirectoryFor("7zip")
val sevenZipFile = sevenZipCacheDir / sevenZipUrl.split("/").last()
val sevenZipTool = sevenZipCacheDir / sevenZipFile.fileName.nameWithoutExtension
HttpClient.downloadIfMissing(sevenZipUrl, sevenZipFile)
FileSystem.unpackIfMissing(sevenZipFile, sevenZipTool)
val severZipToolExe = sevenZipTool.resolve("7z.exe")
targetDir.mkdirs()
exec(
presentablePurpose = "unpack-zip",
workDir = targetDir.toPath(),
timeout = 10.minutes,
args = listOf(severZipToolExe.toAbsolutePath().toString(), "x", "-y", "-o$targetDir", exeFile.path)
)
}
}

View File

@@ -1,81 +0,0 @@
package com.intellij.ide.starter.ide
import com.intellij.ide.starter.di.di
import com.intellij.ide.starter.path.GlobalPaths
import com.intellij.ide.starter.system.SystemInfo
import com.intellij.ide.starter.utils.FileSystem
import com.intellij.ide.starter.utils.HttpClient
import com.intellij.ide.starter.utils.logError
import com.intellij.ide.starter.utils.logOutput
import org.kodein.di.direct
import org.kodein.di.instance
import java.nio.file.Path
import kotlin.io.path.div
abstract class IdeDistribution {
abstract fun installIde(unpackDir: Path, executableFileName: String): InstalledIde
private fun downloadJbr(jbrFileName: String): Pair<Boolean, Path> {
val downloadUrl = "https://cache-redirector.jetbrains.com/intellij-jbr/$jbrFileName"
val jbrCacheDirectory = di.direct.instance<GlobalPaths>().getCacheDirectoryFor("jbr")
val localFile = jbrCacheDirectory / jbrFileName
val localDir = jbrCacheDirectory / jbrFileName.removeSuffix(".tar.gz")
val isSuccess = HttpClient.downloadIfMissing(downloadUrl, localFile, retries = 1)
if (!isSuccess) return Pair(isSuccess, localDir)
FileSystem.unpackIfMissing(localFile, localDir)
return Pair(isSuccess, localDir)
}
protected fun downloadAndUnpackJbrIfNeeded(jbrFullVersion: String): Path {
val jbrVersion = jbrFullVersion.split(".")[0].toInt()
var majorVersion = jbrFullVersion.split("+").firstOrNull()
if (jbrVersion < 17) {
majorVersion = majorVersion?.replace(".", "_")
}
requireNotNull(majorVersion) {
{ "majorVersion is: $majorVersion" }
}
val buildNumber = jbrFullVersion.split("-b").drop(1).singleOrNull()
requireNotNull(buildNumber) {
{ "buildNumber is: $buildNumber" }
}
logOutput("Detected JBR version $jbrFullVersion with parts: $majorVersion and build $buildNumber")
val os = when {
SystemInfo.isWindows -> "windows"
SystemInfo.isLinux -> "linux"
SystemInfo.isMac -> "osx"
else -> error("Unknown OS")
}
val arch = when (SystemInfo.isMac) {
true -> when (System.getProperty("os.arch")) {
"x86_64" -> "x64"
"aarch64" -> "aarch64"
else -> error("Unsupported architecture of Mac OS")
}
false -> "x64"
}
var jbrFileName = "jbrsdk-$majorVersion-$os-$arch-b$buildNumber.tar.gz"
var downloadResult = downloadJbr(jbrFileName)
if (!downloadResult.first) {
logError("Couldn't download '$jbrFileName'. Trying download jbrsdk_nomod")
downloadResult = downloadJbr("jbrsdk_nomod-$majorVersion-$os-$arch-b$buildNumber.tar.gz")
}
val appHome = (downloadResult.second.toFile().listFiles() ?: arrayOf()).singleOrNull { it.isDirectory }?.toPath()
requireNotNull(appHome) {
"appHome is null: $appHome"
}
when {
SystemInfo.isMac -> return appHome / "Contents" / "Home"
}
return appHome
}
}

View File

@@ -1,17 +0,0 @@
package com.intellij.ide.starter.ide
import com.intellij.ide.starter.system.SystemInfo
import java.io.File
object IdeDistributionFactory {
fun installIDE(unpackDir: File, executableFileName: String): InstalledIde {
val distribution = when {
SystemInfo.isMac -> MacOsIdeDistribution()
SystemInfo.isWindows -> WindowsIdeDistribution()
SystemInfo.isLinux -> LinuxIdeDistribution()
else -> error("Not supported app: $unpackDir")
}
return distribution.installIde(unpackDir.toPath(), executableFileName)
}
}

View File

@@ -1,8 +0,0 @@
package com.intellij.ide.starter.ide
import com.intellij.ide.starter.models.IdeInfo
import java.nio.file.Path
interface IdeDownloader {
fun downloadIdeInstaller(ideInfo: IdeInfo, installerPath: Path): IdeInstaller
}

View File

@@ -1,7 +0,0 @@
package com.intellij.ide.starter.ide
import com.intellij.ide.starter.models.IdeInfo
interface IdeInstallator {
fun install(ideInfo: IdeInfo): Pair<String, InstalledIde>
}

View File

@@ -1,7 +0,0 @@
package com.intellij.ide.starter.ide
import java.nio.file.Path
open class IdeInstaller(val installerFile: Path, val buildNumber: String) {
companion object
}

View File

@@ -1,39 +0,0 @@
package com.intellij.ide.starter.ide
import com.intellij.ide.starter.di.di
import com.intellij.ide.starter.models.IdeInfo
import com.intellij.ide.starter.models.IdeProduct
import org.kodein.di.direct
import org.kodein.di.instance
import kotlin.reflect.full.declaredMemberProperties
object IdeProductProvider {
/** GoLand */
val GO: IdeInfo = di.direct.instance<IdeProduct>().GO
/** IntelliJ Ultimate */
val IU: IdeInfo = di.direct.instance<IdeProduct>().IU
/** IntelliJ Community */
val IC: IdeInfo = di.direct.instance<IdeProduct>().IC
/** Android Studio */
val AI: IdeInfo = di.direct.instance<IdeProduct>().AI
/** WebStorm */
val WS: IdeInfo = di.direct.instance<IdeProduct>().WS
/** PhpStorm */
val PS: IdeInfo = di.direct.instance<IdeProduct>().PS
/** DataGrip */
val DB: IdeInfo = di.direct.instance<IdeProduct>().DB
/** RubyMine */
val RM: IdeInfo = di.direct.instance<IdeProduct>().RM
/** PyCharm Professional */
val PY: IdeInfo = di.direct.instance<IdeProduct>().PY
fun getProducts(): List<IdeInfo> = IdeProductProvider::class.declaredMemberProperties.map { it.get(IdeProductProvider) as IdeInfo }
}

View File

@@ -1,16 +0,0 @@
package com.intellij.ide.starter.ide
import com.intellij.ide.starter.models.VMOptions
import com.intellij.ide.starter.models.VMOptionsDiff
import java.nio.file.Path
internal abstract class InstalledBackedIDEStartConfig(
private val patchedVMOptionsFile: Path,
private val finalVMOptions: VMOptions
) : IDEStartConfig {
init {
finalVMOptions.writeIntelliJVmOptionFile(patchedVMOptionsFile)
}
final override fun vmOptionsDiff(): VMOptionsDiff = finalVMOptions.diffIntelliJVmOptionFile(patchedVMOptionsFile)
}

View File

@@ -1,26 +0,0 @@
package com.intellij.ide.starter.ide
import com.intellij.ide.starter.models.VMOptions
import java.nio.file.Path
interface InstalledIde {
val originalVMOptions: VMOptions
val build: String
val os: String
val productCode: String
val isFromSources: Boolean
/** Bundled plugins directory, if supported **/
val bundledPluginsDir: Path?
get() = null
val patchedVMOptionsFile: Path?
get() = null
fun startConfig(vmOptions: VMOptions, logsDir: Path): IDEStartConfig
fun resolveAndDownloadTheSameJDK(): Path
fun isMajorVersionAtLeast(v: Int) = build.substringBefore(".").toIntOrNull()?.let { it >= v } ?: true
}

View File

@@ -1,129 +0,0 @@
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.system.SystemInfo
import com.intellij.ide.starter.utils.callJavaVersion
import com.intellij.ide.starter.utils.logOutput
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.*
import kotlin.time.Duration.Companion.seconds
class LinuxIdeDistribution : IdeDistribution() {
companion object {
private val xvfbRunTool by lazy {
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"))
toolName
}
fun linuxCommandLine(xvfbRunLog: Path): List<String> {
return when {
System.getenv("DISPLAY") != null -> listOf()
else ->
//hint https://gist.github.com/tullmann/2d8d38444c5e81a41b6d
listOf(
xvfbRunTool,
"--error-file=" + xvfbRunLog.toAbsolutePath().toString(),
"--server-args=-ac -screen 0 1920x1080x24",
"--auto-servernum",
"--server-num=88"
)
}
}
fun createXvfbRunLog(logsDir: Path): Path {
val logTxt = logsDir.resolve("xvfb-log.txt")
logTxt.deleteIfExists()
return Files.createFile(logTxt)
}
}
override fun installIde(unpackDir: Path, executableFileName: String): InstalledIde {
require(SystemInfo.isLinux) { "Can only run on Linux, docker is possible, please PR" }
val appHome = (unpackDir.toFile().listFiles()?.singleOrNull { it.isDirectory }?.toPath() ?: unpackDir)
val buildTxtPath = appHome.resolve("build.txt")
require(buildTxtPath.isRegularFile()) { "Cannot find LinuxOS IDE vmoptions file in $unpackDir" }
val (productCode, build) = buildTxtPath.readText().trim().split("-", limit = 2)
val binDir = appHome / "bin"
val allBinFiles = binDir.listDirectoryEntries()
val executablePath = allBinFiles.singleOrNull { file ->
file.fileName.toString() == "$executableFileName.sh"
} ?: error("Failed to detect IDE executable .sh in:\n${allBinFiles.joinToString("\n")}")
return object : InstalledIde {
override val bundledPluginsDir = appHome.resolve("plugins")
val originalVMOptionsFile = executablePath.parent.resolve(
executablePath.fileName.toString().removeSuffix(".sh") + "64.vmoptions") //TODO: which file to pick with 64 or without?
override val originalVMOptions = VMOptions.readIdeVMOptions(this, originalVMOptionsFile)
override val patchedVMOptionsFile = appHome.parent.resolve("${appHome.fileName}.vmoptions")
override fun startConfig(vmOptions: VMOptions, logsDir: Path) =
object : InstalledBackedIDEStartConfig(patchedVMOptionsFile, vmOptions) {
override val environmentVariables: Map<String, String>
get() = super.environmentVariables.filterKeys {
when {
it.startsWith("DESKTOP") -> false
it.startsWith("DBUS") -> false
it.startsWith("APPIMAGE") -> false
it.startsWith("DEFAULTS_PATH") -> false
it.startsWith("GDM") -> false
it.startsWith("GNOME") -> false
it.startsWith("GTK") -> false
it.startsWith("MANDATORY_PATH") -> false
it.startsWith("QT") -> false
it.startsWith("SESSION") -> false
it.startsWith("TOOLBOX_VERSION") -> false
it.startsWith("XAUTHORITY") -> false
it.startsWith("XDG") -> false
it.startsWith("XMODIFIERS") -> false
it.startsWith("GPG_") -> false
it.startsWith("CLUTTER_IM_MODULE") -> false
it.startsWith("APPDIR") -> false
it.startsWith("LC") -> false
it.startsWith("SSH") -> false
else -> true
}
} + ("LC_ALL" to "en_US.UTF-8")
val xvfbRunLog = createXvfbRunLog(logsDir)
override val errorDiagnosticFiles = listOf(xvfbRunLog)
override val workDir = appHome
override val commandLine: List<String> = linuxCommandLine(xvfbRunLog) + executablePath.toAbsolutePath().toString()
}
override val build = build
override val os = "linux"
override val productCode = productCode
override val isFromSources = false
override fun toString() = "IDE{$productCode, $build, $os, home=$unpackDir}"
override fun resolveAndDownloadTheSameJDK(): Path {
val jbrHome = appHome.resolve("jbr")
require(jbrHome.isDirectory()) {
"JbrHome is not found under $jbrHome"
}
val jbrFullVersion = callJavaVersion(jbrHome).substringAfter("build ").substringBefore(")")
logOutput("Found following $jbrFullVersion in the product: $productCode $build")
// in Android Studio bundled only JRE
if (productCode == IdeProductProvider.AI.productCode) return jbrHome
return downloadAndUnpackJbrIfNeeded(jbrFullVersion)
}
}
}
}

View File

@@ -1,105 +0,0 @@
package com.intellij.ide.starter.ide
import com.intellij.ide.starter.models.VMOptions
import com.intellij.ide.starter.utils.callJavaVersion
import com.intellij.ide.starter.utils.logOutput
import org.w3c.dom.Node
import java.io.File
import java.nio.file.Path
import javax.xml.parsers.DocumentBuilderFactory
import kotlin.io.path.div
import kotlin.io.path.isDirectory
import kotlin.io.path.isRegularFile
import kotlin.io.path.readText
class MacOsIdeDistribution : IdeDistribution() {
private fun getExecutableNameFromInfoPlist(appDir: File, @Suppress("SameParameterValue") keyName: String): String {
val infoPlistFile = appDir.resolve("Contents/Info.plist")
val xmlFactory = DocumentBuilderFactory.newInstance()
infoPlistFile.inputStream().use {
val xmlBuilder = xmlFactory.newDocumentBuilder()
val document = xmlBuilder.parse(it)
val keys = document.getElementsByTagName("key")
for (index in 0 until keys.length) {
val keyItem = keys.item(index)
// found the node we are looking for
if (keyItem.firstChild.nodeValue == keyName) {
// lets find the value - it will be the next sibling
var sibling: Node = keyItem.nextSibling
while (sibling.nodeType != Node.ELEMENT_NODE) {
sibling = sibling.nextSibling
}
return sibling.textContent
}
}
}
error("Failed to resolve key: $keyName in $infoPlistFile")
}
override fun installIde(unpackDir: Path, executableFileName: String): InstalledIde {
val appDir = unpackDir.toFile().listFiles()?.singleOrNull { it.name.endsWith(".app") }?.toPath()
?: error("Invalid macOS application directory: $unpackDir")
val executableName = getExecutableNameFromInfoPlist(appDir.toFile(), "CFBundleExecutable")
val appHome = appDir.resolve("Contents")
val executablePath = appHome / "MacOS" / executableName
require(executablePath.isRegularFile()) { "Cannot find macOS IDE executable file in $executablePath" }
val originalVMOptions = appHome / "bin" / "$executableName.vmoptions"
require(originalVMOptions.isRegularFile()) { "Cannot find macOS IDE vmoptions file in $executablePath" }
val buildTxtPath = appHome / "Resources" / "build.txt"
require(buildTxtPath.isRegularFile()) { "Cannot find macOS IDE vmoptions file in $executablePath" }
val (productCode, build) = buildTxtPath.readText().trim().split("-", limit = 2)
return object : InstalledIde {
override val bundledPluginsDir = appHome / "plugins"
override val originalVMOptions = VMOptions.readIdeVMOptions(this, originalVMOptions)
override val patchedVMOptionsFile = appDir.parent.resolve("${appDir.fileName}.vmoptions") //see IDEA-220286
override fun startConfig(vmOptions: VMOptions, logsDir: Path) = object : InstalledBackedIDEStartConfig(patchedVMOptionsFile,
vmOptions) {
override val workDir = appDir
override val commandLine = listOf(executablePath.toAbsolutePath().toString())
}
override val build = build
override val os = "mac"
override val productCode = productCode
override val isFromSources = false
override fun toString() = "IDE{$productCode, $build, $os, home=$appDir}"
override fun resolveAndDownloadTheSameJDK(): Path {
val jbrHome = appHome / "jbr"
require(jbrHome.isDirectory()) {
"JbrHome is not found under $jbrHome"
}
val javaHome = jbrHome / "Contents" / "Home"
require(javaHome.isDirectory()) {
"JavaHome is not found under $javaHome"
}
val jbrFullVersion = callJavaVersion(javaHome).substringAfter("build ").substringBefore(")")
logOutput("Found following $jbrFullVersion in the product: $productCode $build")
// in Android Studio bundled only JRE
if (productCode == IdeProductProvider.AI.productCode) return jbrHome
return downloadAndUnpackJbrIfNeeded(jbrFullVersion)
}
}
}
}

View File

@@ -1,23 +0,0 @@
package com.intellij.ide.starter.ide
import com.intellij.ide.starter.di.di
import com.intellij.ide.starter.models.IdeInfo
import com.intellij.ide.starter.path.GlobalPaths
import org.kodein.di.direct
import org.kodein.di.instance
import kotlin.io.path.createDirectories
import kotlin.io.path.div
class SimpleInstaller : IdeInstallator {
override fun install(ideInfo: IdeInfo): Pair<String, InstalledIde> {
val installersDirectory = (di.direct.instance<GlobalPaths>().installersDirectory / ideInfo.productCode).createDirectories()
//Download
val ideInstaller = di.direct.instance<IdeDownloader>().downloadIdeInstaller(ideInfo, installersDirectory)
val installDir = di.direct.instance<GlobalPaths>().getCacheDirectoryFor("builds") / "${ideInfo.productCode}-${ideInstaller.buildNumber}"
//Unpack
IdeArchiveExtractor.unpackIdeIfNeeded(ideInstaller.installerFile.toFile(), installDir.toFile())
//Install
return Pair(ideInstaller.buildNumber, IdeDistributionFactory.installIDE(installDir.toFile(), ideInfo.executableFileName))
}
}

View File

@@ -1,58 +0,0 @@
package com.intellij.ide.starter.ide
import com.intellij.ide.starter.models.VMOptions
import com.intellij.ide.starter.utils.callJavaVersion
import com.intellij.ide.starter.utils.logOutput
import java.nio.file.Path
import kotlin.io.path.*
class WindowsIdeDistribution : IdeDistribution() {
override fun installIde(unpackDir: Path, executableFileName: String): InstalledIde {
val buildTxtPath = unpackDir.resolve("build.txt")
require(buildTxtPath.isRegularFile()) { "Cannot find WindowsOS IDE vmoptions file in $unpackDir" }
val (productCode, build) = buildTxtPath.readText().trim().split("-", limit = 2)
val binDir = unpackDir / "bin"
val allBinFiles = binDir.listDirectoryEntries()
val executablePath = allBinFiles.singleOrNull { file ->
file.fileName.toString() == "${executableFileName}64.exe"
} ?: error("Failed to detect executable name, ending with 64.exe in:\n${allBinFiles.joinToString("\n")}")
val originalVMOptionsFile = executablePath.parent.resolve("${executablePath.fileName}.vmoptions")
return object : InstalledIde {
override val bundledPluginsDir = unpackDir.resolve("plugins")
override val originalVMOptions = VMOptions.readIdeVMOptions(this, originalVMOptionsFile)
override val patchedVMOptionsFile = unpackDir.parent.resolve("${unpackDir.fileName}.vmoptions")
override fun startConfig(vmOptions: VMOptions, logsDir: Path) = object : InstalledBackedIDEStartConfig(patchedVMOptionsFile,
vmOptions) {
override val workDir = unpackDir
override val commandLine = listOf(executablePath.toAbsolutePath().toString())
}
override val build = build
override val os = "windows"
override val productCode = productCode
override val isFromSources = false
override fun toString() = "IDE{$productCode, $build, $os, home=$unpackDir}"
override fun resolveAndDownloadTheSameJDK(): Path {
val jbrHome = unpackDir / "jbr"
require(jbrHome.isDirectory()) {
"JbrHome is not found under $jbrHome"
}
val jbrFullVersion = callJavaVersion(jbrHome).substringAfter("build ").substringBefore(")")
logOutput("Found following $jbrFullVersion in the product: $productCode $build")
// in Android Studio bundled only JRE
if (productCode == IdeProductProvider.AI.productCode) return jbrHome
return downloadAndUnpackJbrIfNeeded(jbrFullVersion)
}
}
}
}

View File

@@ -1,10 +0,0 @@
package com.intellij.ide.starter.ide
interface CodeInjector {
fun setup(context: IDETestContext)
fun tearDown(context: IDETestContext)
}
interface PlainCodeInjector : CodeInjector {
fun addGroovyCode(code: String)
}

View File

@@ -1,42 +0,0 @@
package com.intellij.ide.starter.ide.command
/**
* One or more commands, that will be "played" in sequence by IDE
*/
open class CommandChain : MarshallableCommand, Iterable<MarshallableCommand> {
private val _chain = mutableListOf<MarshallableCommand>()
override fun iterator(): Iterator<MarshallableCommand> = _chain.iterator()
override fun storeToString(): String {
return _chain.joinToString(separator = System.lineSeparator()) { it.storeToString() }
}
/**
* Pattern for adding a command: %YOUR_COMMAND_PREFIX COMMAND_PARAMS
*/
fun addCommand(command: String) {
_chain.add(initMarshallableCommand(command))
}
fun addCommand(command: MarshallableCommand) {
_chain.add(command)
}
/**
* Pattern for adding a command: %YOUR_COMMAND_PREFIX COMMAND_PARAM_1 .. COMMAND_PARAM_N
*/
fun addCommand(vararg commandArgs: String) {
val command = initMarshallableCommand(commandArgs.joinToString(separator = " "))
_chain.add(command)
}
fun addCommandChain(commandChain: CommandChain) {
_chain.addAll(commandChain)
}
private fun initMarshallableCommand(content: String): MarshallableCommand =
object : MarshallableCommand {
override fun storeToString(): String = content
}
}

View File

@@ -1,5 +0,0 @@
package com.intellij.ide.starter.ide.command
interface MarshallableCommand {
fun storeToString(): String
}

View File

@@ -1,18 +0,0 @@
package com.intellij.ide.starter.models
import com.intellij.ide.starter.ide.IDETestContext
import com.intellij.ide.starter.runner.IDERunContext
import java.nio.file.Path
import kotlin.time.Duration
data class IDEStartResult(
val runContext: IDERunContext,
val logsDir: Path,
val executionTime: Duration,
val vmOptionsDiff: VMOptionsDiff? = null,
val failureError: Throwable? = null
) {
val context: IDETestContext get() = runContext.testContext
val extraAttributes: MutableMap<String, String> = mutableMapOf()
}

View File

@@ -1,16 +0,0 @@
package com.intellij.ide.starter.models
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ObjectNode
import java.nio.file.Path
class IDEStartupReports(startupReportsDir: Path) {
val statsJSON: Path = startupReportsDir.resolve("startup-stats.json")
val statsObject
get() = ObjectMapper().readTree(statsJSON.toFile()) as ObjectNode
override fun toString(): String {
return "IDEStartupReports(statsJSON=$statsJSON)"
}
}

View File

@@ -1,49 +0,0 @@
package com.intellij.ide.starter.models
import com.intellij.ide.starter.community.model.BuildType
import com.intellij.ide.starter.system.SystemInfo
data class IdeInfo(
val productCode: String,
val platformPrefix: String,
val executableFileName: String,
val buildType: String = BuildType.EAP.type,
val buildNumber: String = "",
val tag: String? = null
) {
companion object
val installerFilePrefix
get() = when (productCode) {
"IU" -> "ideaIU"
"IC" -> "ideaIC"
"WS" -> "WebStorm"
"PS" -> "PhpStorm"
"DB" -> "datagrip"
"GO" -> "goland"
"RM" -> "RubyMine"
"PY" -> "pycharmPY"
else -> error("Unknown product code: $productCode")
}
val installerProductName
get() = when (productCode) {
"IU" -> "intellij"
"IC" -> "intellij.ce"
"RM" -> "rubymine"
"PY" -> "pycharm"
else -> installerFilePrefix
}
val installerFileExt
get() = when {
SystemInfo.isWindows -> ".win.zip"
SystemInfo.isLinux -> ".tar.gz"
SystemInfo.isMac -> when (System.getProperty("os.arch")) {
"x86_64" -> ".dmg"
"aarch64" -> "-aarch64.dmg"
else -> error("Unknown architecture of Mac OS")
}
else -> error("Unknown OS")
}
}

View File

@@ -1,30 +0,0 @@
package com.intellij.ide.starter.models
interface IdeProduct {
/** GoLand */
val GO: IdeInfo
/** IntelliJ Ultimate */
val IU: IdeInfo
/** IntelliJ Community */
val IC: IdeInfo
/** Android Studio */
val AI: IdeInfo
/** WebStorm */
val WS: IdeInfo
/** PhpStorm */
val PS: IdeInfo
/** DataGrip */
val DB: IdeInfo
/** RubyMine */
val RM: IdeInfo
/** PyCharm Professional */
val PY: IdeInfo
}

View File

@@ -1,66 +0,0 @@
package com.intellij.ide.starter.models
object IdeProductImp : IdeProduct {
/** GoLand */
override val GO = IdeInfo(
productCode = "GO",
platformPrefix = "GoLand",
executableFileName = "goland",
)
/** IntelliJ Ultimate */
override val IU = IdeInfo(
productCode = "IU",
platformPrefix = "idea",
executableFileName = "idea"
)
/** IntelliJ Community */
override val IC = IdeInfo(
productCode = "IC",
platformPrefix = "Idea",
executableFileName = "idea"
)
/** Android Studio */
override val AI = IdeInfo(
productCode = "AI",
platformPrefix = "AndroidStudio",
executableFileName = "studio"
)
/** WebStorm */
override val WS = IdeInfo(
productCode = "WS",
platformPrefix = "WebStorm",
executableFileName = "webstorm"
)
/** PhpStorm */
override val PS = IdeInfo(
productCode = "PS",
platformPrefix = "PhpStorm",
executableFileName = "phpstorm"
)
/** DataGrip */
override val DB = IdeInfo(
productCode = "DB",
platformPrefix = "DataGrip",
executableFileName = "datagrip"
)
/** RubyMine */
override val RM = IdeInfo(
productCode = "RM",
platformPrefix = "Ruby",
executableFileName = "rubymine"
)
/** PyCharm Professional */
override val PY = IdeInfo(
productCode = "PY",
platformPrefix = "Python",
executableFileName = "pycharm"
)
}

View File

@@ -1,22 +0,0 @@
package com.intellij.ide.starter.models
import com.intellij.ide.starter.system.OsType
import com.intellij.ide.starter.system.SystemInfo
class OsDataStorage<T>(vararg val items: Pair<OsType, T>) {
val get: T
get() {
require(items.isNotEmpty()) { "Os dependent data should not be empty when accessing it" }
val osSpecificData = when {
SystemInfo.isMac -> items.firstOrNull { it.first == OsType.MacOS }
SystemInfo.isWindows -> items.firstOrNull { it.first == OsType.Windows }
SystemInfo.isLinux -> items.firstOrNull { it.first == OsType.Linux }
else -> items.first { it.first == OsType.Other }
}
osSpecificData?.let { return it.second }
return items.first { it.first == OsType.Other }.second
}
}

View File

@@ -1,46 +0,0 @@
package com.intellij.ide.starter.models
import com.intellij.ide.starter.community.model.BuildType
import com.intellij.ide.starter.ide.command.MarshallableCommand
import com.intellij.ide.starter.project.ProjectInfo
import com.intellij.ide.starter.project.ProjectInfoSpec
data class TestCase(
val ideInfo: IdeInfo,
val projectInfo: ProjectInfoSpec? = null,
val commands: Iterable<MarshallableCommand> = listOf(),
val vmOptionsFix: VMOptions.() -> VMOptions = { this },
val useInMemoryFileSystem: Boolean = false
) {
fun withProject(projectInfo: ProjectInfoSpec): TestCase = copy(projectInfo = projectInfo)
fun withCommands(commands: Iterable<MarshallableCommand> = 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))
/**
* [buildNumber] - EAP build number to download
* E.g: "222.3244.1"
* If empty - the latest EAP will be downloaded.
* [Downloads for IDEA Ultimate](https://www.jetbrains.com/idea/download/other.html)
**/
fun useEAP(buildNumber: String = ""): TestCase {
return copy(ideInfo = ideInfo.copy(buildType = BuildType.EAP.type, buildNumber = buildNumber))
}
/**
* [buildNumber] - Release build number to download
* E.g: "222.3244.1"
* If empty - the latest release will be downloaded.
* [Downloads for IDEA Ultimate](https://www.jetbrains.com/idea/download/other.html)
**/
fun useRelease(buildNumber: String = ""): TestCase {
return copy(ideInfo = ideInfo.copy(buildType = BuildType.RELEASE.type, buildNumber = buildNumber))
}
}

View File

@@ -1,11 +0,0 @@
package com.intellij.ide.starter.models
import java.io.File
data class VMLogsInfo(val cdsLoadedClassesCount: Int,
val classLoadLogFile: File,
val classPathLogFile: File,
val startResult: IDEStartResult) {
val classPathLog get() = classPathLogFile.readText()
}

View File

@@ -1,237 +0,0 @@
package com.intellij.ide.starter.models
import com.intellij.ide.starter.ide.InstalledIde
import com.intellij.ide.starter.ide.command.MarshallableCommand
import com.intellij.ide.starter.path.IDEDataPaths
import com.intellij.ide.starter.system.SystemInfo
import com.intellij.ide.starter.utils.FileSystem.cleanPathFromSlashes
import com.intellij.ide.starter.utils.logOutput
import com.intellij.ide.starter.utils.writeJvmArgsFile
import java.io.File
import java.nio.file.Path
import kotlin.io.path.createDirectories
import kotlin.io.path.readLines
import kotlin.io.path.writeLines
import kotlin.io.path.writeText
/**
* allows to combine VMOptions mapping functions easily by calling this function as
* ```
* {}.andThen {} function
* ```
*/
fun (VMOptions.() -> VMOptions).andThen(right: VMOptions.() -> VMOptions): VMOptions.() -> VMOptions = {
val left = this@andThen
this.left().right()
}
data class VMOptions(
private val ide: InstalledIde,
private val data: List<String>,
val env: Map<String, String>
) {
companion object {
fun readIdeVMOptions(ide: InstalledIde, file: Path): VMOptions {
return VMOptions(
ide = ide,
data = file
.readLines()
.map { it.trim() }
.filter { it.isNotBlank() },
env = emptyMap()
)
}
}
override fun toString() = buildString {
appendLine("VMOptions{")
appendLine(" env=$env")
for (line in data) {
appendLine(" $line")
}
appendLine("} // VMOptions")
}
fun addSystemProperty(key: String, value: Boolean): VMOptions = addSystemProperty(key, value.toString())
fun addSystemProperty(key: String, value: Int): VMOptions = addSystemProperty(key, value.toString())
fun addSystemProperty(key: String, value: Long): VMOptions = addSystemProperty(key, value.toString())
fun addSystemProperty(key: String, value: Path): VMOptions = addSystemProperty(key, value.toAbsolutePath().toString())
fun addSystemProperty(key: String, value: String): VMOptions {
logOutput("Setting system property: [$key=$value]")
System.setProperty(key, value) // to synchronize behaviour in IDEA and on test runner side
return addLine(line = "-D$key=$value", filterPrefix = "-D$key=")
}
fun addLine(line: String, filterPrefix: String? = null): VMOptions {
if (data.contains(line)) return this
val copy = if (filterPrefix == null) data else data.filterNot { it.trim().startsWith(filterPrefix) }
return copy(data = copy + line)
}
private fun filterKeys(toRemove: (String) -> Boolean) = copy(data = data.filterNot(toRemove))
fun withEnv(key: String, value: String) = copy(env = env + (key to value))
fun writeIntelliJVmOptionFile(path: Path) {
path.writeLines(data)
logOutput("Write vmoptions patch to $path")
}
fun diffIntelliJVmOptionFile(theFile: Path): VMOptionsDiff {
val loadedOptions = readIdeVMOptions(this.ide, theFile).data
return VMOptionsDiff(originalLines = this.data, actualLines = loadedOptions)
}
fun writeJavaArgsFile(theFile: File) {
writeJvmArgsFile(theFile, this.data)
}
fun overrideDirectories(paths: IDEDataPaths) = this
.addSystemProperty("idea.config.path", paths.configDir)
.addSystemProperty("idea.system.path", paths.systemDir)
.addSystemProperty("idea.plugins.path", paths.pluginsDir)
.addSystemProperty("idea.log.path", paths.logsDir)
fun enableStartupPerformanceLog(perf: IDEStartupReports): VMOptions {
return this
.addSystemProperty("idea.log.perf.stats.file", perf.statsJSON)
}
fun enableClassLoadingReport(filePath: Path): VMOptions {
return this
.addSystemProperty("idea.log.class.list.file", filePath)
.addSystemProperty("idea.record.classpath.info", "true")
}
fun configureLoggers(
debugLoggers: List<String> = emptyList(),
traceLoggers: List<String> = emptyList()
): VMOptions {
val withDebug = if (debugLoggers.isNotEmpty()) {
this.addSystemProperty("idea.log.debug.categories", debugLoggers.joinToString(separator = ",") { "#" + it.removePrefix("#") })
}
else {
this
}
return if (traceLoggers.isNotEmpty()) {
withDebug.addSystemProperty("idea.log.trace.categories", traceLoggers.joinToString(separator = ",") { "#" + it.removePrefix("#") })
}
else {
withDebug
}
}
fun debug(port: Int = 5005, suspend: Boolean = true): VMOptions {
val suspendKey = if (suspend) "y" else "n"
val configLine = "-agentlib:jdwp=transport=dt_socket,server=y,suspend=${suspendKey},address=*:${port}"
return addLine(configLine, filterPrefix = "-agentlib:jdwp")
}
fun inHeadlessMode() = this
.addSystemProperty("java.awt.headless", true)
fun disableStartupDialogs() = this
.addSystemProperty("jb.consents.confirmation.enabled", false)
.addSystemProperty("jb.privacy.policy.text", "<!--999.999-->")
fun takeScreenshotIfFailure(logsDir: Path) = this
.addSystemProperty("ide.performance.screenshot.before.kill", logsDir.resolve("screenshot_beforeKill.jpg").toString())
fun installTestScript(testName: String,
paths: IDEDataPaths,
commands: Iterable<MarshallableCommand>): VMOptions {
val scriptText = commands.joinToString(separator = System.lineSeparator()) { it.storeToString() }
val scriptFileName = testName.cleanPathFromSlashes(replaceWith = "_") + ".text"
val scriptFile = paths.systemDir.resolve(scriptFileName).apply {
parent.createDirectories()
}
scriptFile.writeText(scriptText)
return this.addSystemProperty("testscript.filename", scriptFile)
// Use non-success status code 1 when running IDE as command line tool.
.addSystemProperty("testscript.must.exist.process.with.non.success.code.on.ide.error", "true")
// No need to report TeamCity test failure from within test script.
.addSystemProperty("testscript.must.report.teamcity.test.failure.on.error", "false")
}
/** @see com.intellij.startupTime.StartupTimeWithCDSonJDK13.runOnJDK13 **/
fun withCustomJRE(jre: Path): VMOptions {
if (SystemInfo.isLinux) {
val jrePath = jre.toAbsolutePath().toString()
val envKey = when (ide.productCode) {
"IU" -> "IDEA_JDK"
"WS" -> "WEBIDE_JDK"
else -> error("Not supported for product $ide")
}
return this.withEnv(envKey, jrePath)
}
if (SystemInfo.isMac) {
//Does not work -- https://intellij-support.jetbrains.com/hc/en-us/articles/206544879-Selecting-the-JDK-version-the-IDE-will-run-under
//see https://youtrack.jetbrains.com/issue/IDEA-223075
//see Launcher.m:226
val jrePath = jre.toAbsolutePath().toString()
val envKey = when (ide.productCode) {
"IU" -> "IDEA_JDK"
"WS" -> "WEBSTORM_JDK"
else -> error("Not supported for product $ide")
}
return this.withEnv(envKey, jrePath)
}
if (SystemInfo.isWindows) {
//see WinLauncher.rc and WinLauncher.cpp:294
//see https://youtrack.jetbrains.com/issue/IDEA-223348
val jrePath = jre.toRealPath().toString().replace("/", "\\")
val envKey = when (ide.productCode) {
"IU" -> "IDEA_JDK_64"
"WS" -> "WEBIDE_JDK_64"
else -> error("Not supported for product $ide")
}
return this.withEnv(envKey, jrePath)
}
error("Current OS is not supported")
}
fun usingStartupFramework() = this
.addSystemProperty("startup.performance.framework", true)
fun setFlagIntegrationTests() = this
.addSystemProperty("idea.is.integration.test", true)
fun setFatalErrorNotificationEnabled() = this
.addSystemProperty("idea.fatal.error.notification", true)
fun withJvmCrashLogDirectory(jvmCrashLogDirectory: Path) = this
.addLine("-XX:ErrorFile=${jvmCrashLogDirectory.toAbsolutePath()}${File.separator}java_error_in_idea_%p.log", "-XX:ErrorFile=")
fun withHeapDumpOnOutOfMemoryDirectory(directory: Path) = this
.addLine("-XX:HeapDumpPath=${directory.toAbsolutePath()}", "-XX:HeapDumpPath=")
fun withXmx(sizeMb: Int) = this
.addLine("-Xmx" + sizeMb + "m", "-Xmx")
fun withG1GC() = this
.filterKeys { it == "-XX:+UseConcMarkSweepGC" }
.filterKeys { it == "-XX:+UseG1GC" }
.addLine("-XX:+UseG1GC")
/** see [JEP 318](https://openjdk.org/jeps/318) **/
fun withEpsilonGC() = this
.filterKeys { it == "-XX:+UseConcMarkSweepGC" }
.filterKeys { it == "-XX:+UseG1GC" }
.addLine("-XX:+UnlockExperimentalVMOptions")
.addLine("-XX:+UseEpsilonGC")
.addLine("-Xmx16g", "-Xmx")
// a dummy wrapper to simplify expressions
fun id() = this
}

View File

@@ -1,13 +0,0 @@
package com.intellij.ide.starter.models
data class VMOptionsDiff(val originalLines: List<String>,
val actualLines: List<String>
) {
val newLines = actualLines - originalLines
val missingLines = originalLines - actualLines
val isEmpty = newLines.isEmpty() && missingLines.isEmpty()
override fun toString(): String {
return "VMOptionsDiff(newLines=$newLines, missingLines=$missingLines)"
}
}

View File

@@ -1,57 +0,0 @@
package com.intellij.ide.starter.path
import com.intellij.ide.starter.ci.CIServer
import com.intellij.ide.starter.di.di
import com.intellij.ide.starter.utils.FileSystem.getDirectoryTreePresentableSizes
import com.intellij.ide.starter.utils.getDiskInfo
import org.kodein.di.direct
import org.kodein.di.instance
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.io.path.createDirectories
import kotlin.io.path.div
abstract class GlobalPaths(val checkoutDir: Path) {
val intelliJOutDirectory: Path = checkoutDir.toAbsolutePath() / "out"
val artifactsDirectory: Path = intelliJOutDirectory / "artifacts"
/**
* Local => out
* CI => out/tests
*/
val compiledRootDirectory: Path = when (di.direct.instance<CIServer>().isBuildRunningOnCI) {
true -> intelliJOutDirectory / "tests"
false -> intelliJOutDirectory // Local run
}
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<CIServer>().isBuildRunningOnCI &&
!System.getProperty("agent.persistent.cache").isNullOrEmpty()
) {
(Paths.get(System.getProperty("agent.persistent.cache"), "perf-tests-cache")).createDirectories()
}
else {
(testHomePath / "cache").createDirectories()
}
fun getCacheDirectoryFor(entity: String): Path = (cacheDirectory / entity).createDirectories()
fun getDiskUsageDiagnostics(): String {
return buildString {
appendLine("Disk usage by integration tests (home $testHomePath)")
appendLine(Files.getFileStore(testHomePath).getDiskInfo())
appendLine()
appendLine(testHomePath.getDirectoryTreePresentableSizes(3))
if (cacheDirectory != testHomePath / "cache") {
appendLine("Agent persistent cache directory disk usage $cacheDirectory")
appendLine(cacheDirectory.getDirectoryTreePresentableSizes(2))
}
}
}
}

View File

@@ -1,89 +0,0 @@
package com.intellij.ide.starter.path
import com.intellij.ide.starter.ci.CIServer
import com.intellij.ide.starter.di.di
import com.intellij.ide.starter.utils.FileSystem.getFileOrDirectoryPresentableSize
import com.intellij.ide.starter.utils.createInMemoryDirectory
import com.intellij.ide.starter.utils.logOutput
import org.kodein.di.direct
import org.kodein.di.instance
import java.io.Closeable
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.createDirectories
import kotlin.io.path.div
import kotlin.io.path.exists
import kotlin.streams.toList
class IDEDataPaths(
private val testHome: Path,
private val inMemoryRoot: Path?
) : Closeable {
companion object {
fun createPaths(testName: String, testHome: Path, useInMemoryFs: Boolean): IDEDataPaths {
testHome.toFile().deleteRecursively()
testHome.createDirectories()
val inMemoryRoot = if (useInMemoryFs) {
createInMemoryDirectory("ide-integration-test-$testName")
}
else {
null
}
return IDEDataPaths(testHome = testHome, inMemoryRoot = inMemoryRoot)
}
}
val logsDir = (testHome / "log").createDirectories()
val reportsDir = (testHome / "reports").createDirectories()
val snapshotsDir = (testHome / "snapshots").createDirectories()
val tempDir = (testHome / "temp").createDirectories()
// Directory used to store TeamCity artifacts. To make sure the TeamCity publishes all artifacts
// files added to this directory must not be removed until the end of the tests execution .
val teamCityArtifacts = (testHome / "team-city-artifacts").createDirectories()
val configDir = ((inMemoryRoot ?: testHome) / "config").createDirectories()
val systemDir = ((inMemoryRoot ?: testHome) / "system").createDirectories()
val pluginsDir = (testHome / "plugins").createDirectories()
override fun close() {
if (inMemoryRoot != null) {
try {
inMemoryRoot.toFile().deleteRecursively()
}
catch (e: Exception) {
logOutput("! Failed to unmount in-memory FS at $inMemoryRoot")
e.stackTraceToString().lines().forEach { logOutput(" $it") }
}
}
if (di.direct.instance<CIServer>().isBuildRunningOnCI) {
deleteDirectories()
}
}
private fun deleteDirectories() {
val toDelete = getDirectoriesToDeleteAfterTest().filter { it.exists() }
if (toDelete.isNotEmpty()) {
logOutput(buildString {
appendLine("Removing directories of $testHome")
toDelete.forEach { path ->
appendLine(" $path: ${path.getFileOrDirectoryPresentableSize()}")
}
})
}
toDelete.forEach { runCatching { it.toFile().deleteRecursively() } }
}
private fun getDirectoriesToDeleteAfterTest() = if (testHome.exists()) {
Files.list(testHome).use { it.toList() } - setOf(teamCityArtifacts)
}
else {
emptyList()
}
override fun toString(): String = "IDE Test Paths at $testHome"
}

View File

@@ -1,5 +0,0 @@
package com.intellij.ide.starter.path
import com.intellij.ide.starter.utils.Git
class InstallerGlobalPaths : GlobalPaths(Git.getRepoRoot())

View File

@@ -1,128 +0,0 @@
package com.intellij.ide.starter.plugins
import com.intellij.ide.starter.di.di
import com.intellij.ide.starter.ide.IDETestContext
import com.intellij.ide.starter.path.GlobalPaths
import com.intellij.ide.starter.utils.FileSystem
import com.intellij.ide.starter.utils.HttpClient
import com.intellij.ide.starter.utils.logOutput
import org.kodein.di.direct
import org.kodein.di.instance
import java.nio.file.Path
import java.util.jar.JarFile
import kotlin.io.path.div
import kotlin.io.path.exists
import kotlin.io.path.readLines
import kotlin.io.path.writeLines
open class PluginConfigurator(val testContext: IDETestContext) {
val disabledPluginsPath: Path
get() = testContext.paths.configDir / "disabled_plugins.txt"
fun setupPluginFromPath(pathToPluginArchive: Path) = apply {
FileSystem.unpack(pathToPluginArchive, testContext.paths.pluginsDir)
}
fun setupPluginFromURL(urlToPluginZipFile: String) = apply {
val pluginRootDir = di.direct.instance<GlobalPaths>().getCacheDirectoryFor("plugins")
val pluginZip: Path = pluginRootDir / testContext.ide.build / urlToPluginZipFile.substringAfterLast("/")
HttpClient.download(urlToPluginZipFile, pluginZip)
FileSystem.unpack(pluginZip, testContext.paths.pluginsDir)
}
fun setupPluginFromPluginManager(
pluginId: String,
ideBuild: String,
channel: String? = null,
) = apply {
logOutput("Setting up plugin: $pluginId ...")
val fileName = pluginId.replace(".", "-") + ".zip"
val downloadedPlugin = di.direct.instance<GlobalPaths>().getCacheDirectoryFor("plugins") / testContext.ide.build / fileName
if (!downloadedPlugin.toFile().exists()) {
val url = buildString {
append("https://plugins.jetbrains.com/pluginManager/")
append("?action=download")
append("&id=${pluginId.replace(" ", "%20")}")
append("&noStatistic=false")
append("&build=$ideBuild")
channel?.let {
append("&channel=$it")
}
}
HttpClient.download(url, downloadedPlugin)
}
FileSystem.unpack(downloadedPlugin, testContext.paths.pluginsDir)
logOutput("Plugin $pluginId setup finished")
}
fun disablePlugins(vararg pluginIds: String) = disablePlugins(pluginIds.toSet())
fun disablePlugins(pluginIds: Set<String>) = also {
disabledPluginsPath.writeLines(disabledPluginIds + pluginIds)
}
fun enablePlugins(vararg pluginIds: String) = enablePlugins(pluginIds.toSet())
fun enablePlugins(pluginIds: Set<String>) = also {
disabledPluginsPath.writeLines(disabledPluginIds - pluginIds)
}
val disabledPluginIds: Set<String>
get() {
val file = disabledPluginsPath
return if (file.exists()) file.readLines().toSet() else emptySet()
}
fun findPluginXmlByPluginIdInAGivenDir(pluginId: String, bundledPluginsDir: Path): Boolean {
val jarFiles = bundledPluginsDir.toFile().walk().filter { it.name.endsWith(".jar") }.toList()
jarFiles.forEach {
val jarFile = JarFile(it)
val entry = jarFile.getJarEntry("META-INF/plugin.xml")
if (entry != null) {
val inputStream = jarFile.getInputStream(entry)
val text: String = inputStream.bufferedReader(Charsets.UTF_8).use { reader -> reader.readText() }
if (text.contains(" <id>$pluginId</id>")) {
return true
}
}
}
return false
}
fun getPluginInstalledState(pluginId: String): PluginInstalledState {
if (disabledPluginsPath.toFile().exists() && pluginId in disabledPluginIds) {
return PluginInstalledState.DISABLED
}
val installedPluginDir = testContext.paths.pluginsDir
if (findPluginXmlByPluginIdInAGivenDir(pluginId, installedPluginDir)) {
return PluginInstalledState.INSTALLED
}
val bundledPluginsDir = testContext.ide.bundledPluginsDir
if (bundledPluginsDir == null) {
logOutput("Cannot ensure a plugin '$pluginId' is installed in ${testContext.ide}. Consider it is installed.")
return PluginInstalledState.INSTALLED
}
if (findPluginXmlByPluginIdInAGivenDir(pluginId, bundledPluginsDir)) {
return PluginInstalledState.BUNDLED_TO_IDE
}
return PluginInstalledState.NOT_INSTALLED
}
fun assertPluginIsInstalled(pluginId: String): PluginConfigurator {
when (getPluginInstalledState(pluginId)) {
PluginInstalledState.DISABLED -> error("Plugin '$pluginId' must not be listed in the disabled plugins file ${disabledPluginsPath}")
PluginInstalledState.NOT_INSTALLED -> error("Plugin '$pluginId' must be installed")
PluginInstalledState.BUNDLED_TO_IDE -> return this
PluginInstalledState.INSTALLED -> return this
}
}
}

View File

@@ -1,8 +0,0 @@
package com.intellij.ide.starter.plugins
enum class PluginInstalledState {
BUNDLED_TO_IDE,
INSTALLED,
NOT_INSTALLED,
DISABLED
}

View File

@@ -1,3 +0,0 @@
package com.intellij.ide.starter.plugins

View File

@@ -1,10 +0,0 @@
package com.intellij.ide.starter.process
data class LinuxProcessMetaInfo(
override val pid: Int,
val vsz: Int,
val rss: Int,
override val command: String
) : ProcessMetaInfo(pid, command) {
override fun toString() = "$pid (vsz = $vsz) (rss = $rss) $command"
}

View File

@@ -1,6 +0,0 @@
package com.intellij.ide.starter.process
data class MacOsProcessMetaInfo(
override val pid: Int,
override val command: String
) : ProcessMetaInfo(pid, command)

View File

@@ -1,8 +0,0 @@
package com.intellij.ide.starter.process
abstract class ProcessMetaInfo(
open val pid: Int,
open val command: String
) {
override fun toString() = "$pid $command"
}

View File

@@ -1,255 +0,0 @@
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.system.SystemInfo
import com.intellij.ide.starter.utils.catchAll
import com.intellij.ide.starter.utils.logOutput
import org.kodein.di.direct
import org.kodein.di.instance
import java.nio.file.Path
import kotlin.io.path.isRegularFile
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
// TODO: consider using https://github.com/oshi/oshi for acquiring process info
/**
* TeamCity may not kill processes started during the build (TW-69045).
* They stay alive and consume resources after tests.
* This lead to OOM and other errors during tests, for example,
* IDEA-256265: shared-indexes tests on Linux suspiciously fail with 137 (killed by OOM)
*/
fun killOutdatedProcessesOnUnix(commandsToSearch: Iterable<String> = listOf("/perf-startup/")) {
if (SystemInfo.isWindows) {
logOutput("Current system is Windows. No logic for analysis of outdated processes is yet implemented.")
return
}
val processes = arrayListOf<ProcessMetaInfo>()
if (SystemInfo.isLinux) catchAll { processes += dumpListOfProcessesOnLinux() }
else catchAll { processes += dumpListOfProcessesOnMacOS() }
val processIdsToKill = processes.filter { process ->
commandsToSearch.any { process.command.contains(it) }
}.map { it.pid }
logOutput("These Unix processes must be killed before the next test run: [$processIdsToKill]")
for (pid in processIdsToKill) {
catchAll { killProcessOnUnix(pid) }
}
}
fun dumpListOfProcessesOnMacOS(): List<MacOsProcessMetaInfo> {
check(SystemInfo.isMac)
val stdoutRedirect = ExecOutputRedirect.ToString()
exec("ps",
di.direct.instance<GlobalPaths>().testsDirectory,
timeout = 1.minutes,
args = listOf("ps", "-ax"),
stdoutRedirect = stdoutRedirect)
val processLines = stdoutRedirect.read().lines().drop(1).map { it.trim() }.filterNot { it.isBlank() }
//PID TTY TIME CMD
// 1 ?? 0:43.67 /sbin/launchd
val processes = arrayListOf<MacOsProcessMetaInfo>()
for (line in processLines) {
var rest = line
fun nextString(): String {
val result = rest.substringBefore(" ").trim()
rest = rest.substringAfter(" ").dropWhile { it == ' ' }
return result
}
val pid = nextString().toInt()
nextString() //TTY
nextString() //TIME
val command = rest
processes += MacOsProcessMetaInfo(pid, command)
}
return processes
}
fun dumpListOfProcessesOnLinux(): List<LinuxProcessMetaInfo> {
check(SystemInfo.isLinux)
val stdoutRedirect = ExecOutputRedirect.ToString()
exec("ps",
di.direct.instance<GlobalPaths>().testsDirectory,
timeout = 1.minutes,
args = listOf("ps", "-aux"),
stdoutRedirect = stdoutRedirect)
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
val processes = arrayListOf<LinuxProcessMetaInfo>()
for (line in processLines) {
var rest = line
fun nextString(): String {
val result = rest.substringBefore(" ").trim()
rest = rest.substringAfter(" ").dropWhile { it == ' ' }
return result
}
nextString() //user
val pid = nextString().toInt()
nextString() //cpu
nextString() //mem
val vsz = nextString().toInt()
val rss = nextString().toInt()
nextString() //tty
nextString() //stat
nextString() //start
nextString() //time
val command = rest
processes += LinuxProcessMetaInfo(pid, vsz, rss, command)
}
return processes
}
private fun killProcessOnUnix(pid: Int) {
check(SystemInfo.isUnix)
logOutput("Killing process $pid")
exec(
"kill-process-$pid",
di.direct.instance<GlobalPaths>().testsDirectory,
timeout = 1.minutes,
args = listOf("kill", "-9", pid.toString()),
stdoutRedirect = ExecOutputRedirect.ToStdOut("[kill-$pid-out]"),
stderrRedirect = ExecOutputRedirect.ToStdOut("[kill-$pid-err]")
)
}
/**
* Workaround for IDEA-251643.
* On Linux we run IDE using `xvfb-run` tool wrapper.
* Thus we must guess the original java process ID for capturing the thread dumps.
* TODO: try to use java.lang.ProcessHandle to get the parent process ID.
*/
fun getJavaProcessId(javaHome: Path, workDir: Path, originalProcessId: Long, originalProcess: Process): Long {
if (!SystemInfo.isLinux) {
return originalProcessId
}
logOutput("Guessing java process ID on Linux (pid of the java process wrapper - $originalProcessId)")
val stdout = ExecOutputRedirect.ToString()
val stderr = ExecOutputRedirect.ToString()
exec(
"jcmd-run",
workDir,
timeout = 1.minutes,
args = listOf(javaHome.resolve("bin/jcmd").toAbsolutePath().toString()),
stdoutRedirect = stdout,
stderrRedirect = stderr
)
val mergedOutput = stdout.read() + "\n" + stderr.read()
val candidates = arrayListOf<Long>()
val candidatesFromProcessHandle = arrayListOf<Long>()
logOutput("List all java processes IDs:")
for (line in mergedOutput.lines().map { it.trim() }.filterNot { it.isEmpty() }) {
logOutput(line)
/*
An example of a process line:
1578401 com.intellij.idea.Main /home/sergey.patrikeev/Documents/intellij/out/perf-startup/tests/IU-211.1852/ijx-jdk-empty/verify-shared-index/temp/projects/idea-startup-performance-project-test-03/idea-startup-performance-project-test-03
An example from TC:
intellij project:
81413 com.intellij.idea.Main /opt/teamcity-agent/work/71b862de01f59e23
another project:
84318 com.intellij.idea.Main /opt/teamcity-agent/temp/buildTmp/startupPerformanceTests5985285665047908961/perf-startup/tests/IU-installer-from-file/spring_boot/indexing_oldProjectModel/projects/projects/spring-boot-master/spring-boot-master
An example from TC TestsDynamicBundledPluginsStableLinux
1879942 com.intellij.idea.Main /opt/teamcity-agent/temp/buildTmp/startupPerformanceTests4436006118811351792/perf-startup/cache/projects/unpacked/javaproject_1.0.0/java-design-patterns-master
*/
//TODO(Monitor case /opt/teamcity-agent/work/)
val pid = line.substringBefore(" ", "").toLongOrNull() ?: continue
if (line.contains("com.intellij.idea.Main") && (line.contains("/perf-startup/tests/") || line.contains(
"/perf-startup/cache/") || line.contains("/opt/teamcity-agent/work/"))) {
candidates.add(pid)
}
}
originalProcess.toHandle().descendants().forEach { desc ->
if (desc.info().command().get().contains("java")) {
logOutput("Candidate from ProcessHandle process: ${desc.pid()}")
logOutput("command: ${desc.info().command()}")
candidatesFromProcessHandle.add(desc.pid())
}
}
if (candidates.isEmpty() && candidatesFromProcessHandle.isNotEmpty()) {
logOutput("Candidates from jcmd are missing, will be used first one from ProcessHandle instead: " + candidatesFromProcessHandle.first())
candidates.add(candidatesFromProcessHandle.first())
}
if (candidates.isNotEmpty()) {
logOutput("Found the following java process ID candidates: " + candidates.joinToString())
if (originalProcessId in candidates) {
return originalProcessId
}
return candidates.first()
}
else {
return originalProcessId
}
}
fun collectJavaThreadDump(
javaHome: Path,
workDir: Path,
javaProcessId: Long,
dumpFile: Path,
includeStdout: Boolean = true
) {
val ext = if (SystemInfo.isWindows) ".exe" else ""
val jstackPath = listOf(
javaHome.resolve("bin/jstack$ext"),
javaHome.parent.resolve("bin/jstack$ext")
).map { it.toAbsolutePath() }.firstOrNull { it.isRegularFile() } ?: error("Failed to locate jstack under $javaHome")
val command = listOf(jstackPath.toAbsolutePath().toString(), "-l", javaProcessId.toString())
exec(
"jstack",
workDir,
timeout = 1.minutes,
args = command,
stdoutRedirect = ExecOutputRedirect.ToFile(dumpFile.toFile()),
stderrRedirect = ExecOutputRedirect.ToStdOut("[jstack-err]")
)
if (includeStdout) {
logOutput("jstack output:\n${dumpFile.toFile().readLines().joinToString("\n")}")
}
}
fun destroyGradleDaemonProcessIfExists() {
val stdout = ExecOutputRedirect.ToString()
exec(
"get jps process",
workDir = null,
timeout = 30.seconds,
args = listOf("jps", "-l"),
stdoutRedirect = stdout
)
logOutput("List of java processes: " + stdout.read())
if (stdout.read().contains("GradleDaemon")) {
val readLines = stdout.read().split('\n')
readLines.forEach {
if (it.contains("GradleDaemon")) {
logOutput("Killing GradleDaemon process")
val processId = it.split(" ").first().toLong()
// get up-to date process list on every iteration
ProcessHandle.allProcesses()
.filter { ph -> ph.isAlive && ph.pid() == processId }
.forEach { ph -> ph.destroy() }
}
}
}
}

View File

@@ -1,7 +0,0 @@
package com.intellij.ide.starter.profiler
import com.intellij.ide.starter.runner.IDERunContext
abstract class ProfilerInjector(val type: ProfilerType) {
abstract fun injectProfiler(runContext: IDERunContext): IDERunContext
}

View File

@@ -1,7 +0,0 @@
package com.intellij.ide.starter.profiler
enum class ProfilerType(val kind: String) {
YOURKIT("YOURKIT"),
ASYNC("ASYNC"),
NONE("NONE");
}

View File

@@ -1,92 +0,0 @@
package com.intellij.ide.starter.project
import com.intellij.ide.starter.di.di
import com.intellij.ide.starter.path.GlobalPaths
import com.intellij.ide.starter.utils.FileSystem
import com.intellij.ide.starter.utils.FileSystem.isDirUpToDate
import com.intellij.ide.starter.utils.HttpClient
import com.intellij.ide.starter.utils.logOutput
import org.kodein.di.instance
import java.nio.file.Path
import kotlin.io.path.div
import kotlin.io.path.isDirectory
import kotlin.io.path.isRegularFile
interface ProjectInfoSpec {
fun downloadAndUnpackProject(): Path?
}
data class ProjectInfo(
val testProjectURL: String? = null,
val testProjectDir: Path? = null,
val isReusable: Boolean = true,
/**
* can be either .zip/.tar.gz file or a directory.
* It will be copied to a temp path before test is run
*/
val testProjectImage: Path? = null,
/**
* relative path inside Image file, where project home is located
*/
val testProjectImageRelPath: (Path) -> Path = { it / (testProjectURL?.split("/")?.last()?.split(".zip")?.first() ?: "") }
) : ProjectInfoSpec {
init {
require(listOfNotNull(testProjectURL, testProjectDir, testProjectImage).size <= 1) {
"Only one of 'testProjectURL', 'testProjectDir' or 'testProjectImage' must be specified or none"
}
}
override fun downloadAndUnpackProject(): Path? {
if (testProjectImage == null && testProjectDir == null && testProjectURL == null) {
return null
}
if (testProjectDir != null && !testProjectDir.toFile().exists()) {
return null
}
if (testProjectDir != null) {
require(testProjectURL == null)
return testProjectDir.toAbsolutePath()
}
val globalPaths by di.instance<GlobalPaths>()
val projectsUnpacked = globalPaths.getCacheDirectoryFor("projects").resolve("unpacked")
val projectHome = projectsUnpacked.let(testProjectImageRelPath)
if (!isReusable) {
projectHome.toFile().deleteRecursively()
}
if (projectHome.isDirUpToDate()) {
logOutput("Already unpacked project $projectHome will be used in the test")
return projectHome
}
else {
projectHome.toFile().deleteRecursively()
}
val imagePath: Path = if (testProjectURL != null) {
val zipFile = when (testProjectURL.contains("https://github.com")) {
true -> globalPaths.getCacheDirectoryFor("projects").resolve("zip").resolve("${projectHome.toString().split("/").last()}.zip")
false -> globalPaths.getCacheDirectoryFor("projects").resolve("zip").resolve(testProjectURL.toString().split("/").last())
}
HttpClient.downloadIfMissing(testProjectURL, zipFile)
zipFile
}
else {
testProjectImage ?: error("Either testProjectImage or testProjectURL must be set")
}
when {
imagePath.isRegularFile() -> FileSystem.unpack(imagePath, projectsUnpacked)
imagePath.isDirectory() -> imagePath.toFile().copyRecursively(projectsUnpacked.toFile(), overwrite = true)
else -> error("$imagePath does not exist!")
}
return projectHome
}
}

View File

@@ -1,60 +0,0 @@
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.utils.convertToHashCodeWithOnlyLetters
import com.intellij.ide.starter.utils.generifyErrorMessage
import org.kodein.di.direct
import org.kodein.di.instance
import java.io.File
import java.nio.file.Path
import kotlin.io.path.isDirectory
import kotlin.io.path.listDirectoryEntries
object ErrorReporter {
private const val MAX_TEST_NAME_LENGTH = 250
/**
* 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<Pair<File, File>> {
return if (scriptErrorsDir.isDirectory()) {
val errorsDirectories = scriptErrorsDir.listDirectoryEntries()
errorsDirectories.map { errorDir ->
val messageFile = errorDir.resolve("message.txt").toFile()
val stacktraceFile = errorDir.resolve("stacktrace.txt").toFile()
if (messageFile.exists() && stacktraceFile.exists()) {
val messageText = generifyErrorMessage(messageFile.readText().trim())
val stackTraceContent = stacktraceFile.readText().trim()
var testName: String
val onlyLettersHash = convertToHashCodeWithOnlyLetters(generifyErrorMessage(stackTraceContent).hashCode())
if (stackTraceContent.startsWith(messageText)) {
val maxLength = (MAX_TEST_NAME_LENGTH - onlyLettersHash.length).coerceAtMost(stackTraceContent.length)
val extractedTestName = stackTraceContent.substring(0, maxLength).trim()
testName = "($onlyLettersHash $extractedTestName)"
}
else {
testName = "($onlyLettersHash ${messageText.substring(0, MAX_TEST_NAME_LENGTH.coerceAtMost(messageText.length)).trim()})"
}
val stackTrace = """
Test: $contextName
$stackTraceContent
""".trimIndent().trimMargin().trim()
di.direct.instance<CIServer>().reportTestFailure(generifyErrorMessage(testName), messageText, stackTrace)
}
Pair(messageFile, stacktraceFile)
}
}
else listOf()
}
}

View File

@@ -1,8 +0,0 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.starter.report.publisher
import com.intellij.ide.starter.models.IDEStartResult
interface ReportPublisher {
fun publish(ideStartResult: IDEStartResult)
}

View File

@@ -1,13 +0,0 @@
package com.intellij.ide.starter.report.publisher.impl
import com.intellij.ide.starter.models.IDEStartResult
import com.intellij.ide.starter.report.publisher.ReportPublisher
import com.intellij.ide.starter.utils.logOutput
object ConsoleTestResultPublisher : ReportPublisher {
override fun publish(ideStartResult: IDEStartResult) {
logOutput("Console publisher")
}
}

View File

@@ -1,15 +0,0 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.starter.report.publisher.impl
import com.intellij.ide.starter.models.IDEStartResult
import com.intellij.ide.starter.report.publisher.ReportPublisher
import com.intellij.ide.starter.report.qodana.QodanaClient
import com.intellij.ide.starter.report.sarif.TestContextToQodanaSarifMapper
object QodanaTestResultPublisher : ReportPublisher {
override fun publish(ideStartResult: IDEStartResult) {
QodanaClient.report(TestContextToQodanaSarifMapper.map(ideStartResult))
}
}

View File

@@ -1,13 +0,0 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.starter.report.qodana
import com.intellij.ide.starter.utils.logOutput
import com.jetbrains.qodana.sarif.model.SarifReport
object QodanaClient {
fun report(sarifReport: SarifReport) {
logOutput("QodanaClient report")
//TODO("Implement client")
}
}

View File

@@ -1,47 +0,0 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.starter.report.sarif
import com.jetbrains.qodana.sarif.model.*
fun sarifReport(sarif: SarifReport, init: SarifReport.() -> Unit): SarifReport {
sarif.init()
return sarif
}
fun sarifRun(run: Run, init: Run.() -> Unit): Run {
run.init()
return run
}
fun driver(driver: ToolComponent, init: ToolComponent.() -> Unit): ToolComponent {
driver.init()
return driver
}
fun taxa(init: ReportingDescriptor.() -> Unit): ReportingDescriptor {
val taxa = ReportingDescriptor()
taxa.init()
return taxa
}
fun invocation(init: Invocation.() -> Unit): Invocation {
val invocation = Invocation()
invocation.init()
return invocation
}
fun result(init: Result.() -> Unit): Result {
val result = Result()
result.init()
return result
}
fun result(result: Result, init: Result.() -> Unit): Result {
result.init()
return result
}
fun versionControlProvenance(init: VersionControlDetails.() -> Unit): VersionControlDetails {
val versionControl = VersionControlDetails()
versionControl.init()
return versionControl
}

View File

@@ -1,42 +0,0 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.starter.report.sarif
import com.intellij.ide.starter.models.IDEStartResult
import com.jetbrains.qodana.sarif.SarifUtil
import com.jetbrains.qodana.sarif.model.SarifReport
import java.nio.file.Path
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")
val sarifReport = SarifUtil.readReport(Path.of(defaultReportPath))
return sarifReport(sarifReport) {
sarifRun(runs[0]) {
driver(tool.driver) {
taxa.add(taxa {
id = ideStartResult.context.testName
name = ideStartResult.context.testName
})
}
invocations.add(invocation {
exitCode = if (ideStartResult.failureError == null) 0 else 1
executionSuccessful = ideStartResult.failureError == null
})
versionControlProvenance.add(versionControlProvenance {
})
results.add(result {
ruleId = "Perf test"
})
}
}
}
}

View File

@@ -1,33 +0,0 @@
package com.intellij.ide.starter.runner
import com.intellij.ide.starter.ide.IDETestContext
import com.intellij.ide.starter.ide.PlainCodeInjector
import kotlin.io.path.createDirectories
import kotlin.io.path.deleteIfExists
import kotlin.io.path.writeText
open class CodeBuilderHost : PlainCodeInjector {
private var codeToRun: String? = null
override fun addGroovyCode(code: String) {
require(codeToRun == null) { "Only one code block is supported" }
codeToRun = code
}
private val IDETestContext.targetFile
get() = paths.configDir.resolve("extensions/com.intellij/startup/init.groovy")
override fun setup(context: IDETestContext) {
val code = codeToRun
if (code != null) {
context.targetFile.apply {
parent.createDirectories()
writeText(code)
}
}
}
override fun tearDown(context: IDETestContext) {
context.targetFile.deleteIfExists()
}
}

View File

@@ -1,10 +0,0 @@
package com.intellij.ide.starter.runner
sealed class IDECommandLine {
data class Args(val args: List<String>) : IDECommandLine()
object OpenTestCaseProject : IDECommandLine()
}
fun args(vararg args: String) = IDECommandLine.Args(args.toList())
fun args(args: List<String>) = IDECommandLine.Args(args.toList())
operator fun IDECommandLine.Args.plus(params: List<String>) = copy(args = this.args + params)

View File

@@ -1,334 +0,0 @@
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
import com.intellij.ide.starter.models.IDEStartResult
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.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 org.kodein.di.direct
import org.kodein.di.instance
import java.io.Closeable
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.streams.toList
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
import kotlin.time.measureTime
interface IDERunCloseContext {
val wasRunSuccessful: Boolean
}
data class IDERunContext(
val testContext: IDETestContext,
val patchVMOptions: VMOptions.() -> VMOptions = { this },
val commandLine: IDECommandLine? = null,
val commands: Iterable<MarshallableCommand> = listOf(),
val codeBuilder: (CodeInjector.() -> Unit)? = null,
val runTimeout: Duration = 10.minutes,
val traceStacksEvery: Duration = 10.minutes,
val useStartupScript: Boolean = true,
val closeHandlers: List<IDERunCloseContext.() -> Unit> = listOf(),
val verboseOutput: Boolean = false,
val launchName: String = "",
val expectedKill: Boolean = false,
val collectNativeThreads: Boolean = false
) {
val contextName: String
get() = if (launchName.isNotEmpty()) {
"${testContext.testName}/$launchName"
}
else {
testContext.testName
}
fun verbose() = copy(verboseOutput = true)
@Suppress("unused")
fun withVMOptions(patchVMOptions: VMOptions.() -> VMOptions) = addVMOptionsPatch(patchVMOptions)
fun addVMOptionsPatch(patchVMOptions: VMOptions.() -> VMOptions) = copy(
patchVMOptions = this.patchVMOptions.andThen(patchVMOptions)
)
fun addCompletionHandler(handler: IDERunCloseContext.() -> Unit) = this.copy(closeHandlers = closeHandlers + handler)
fun uploadProfileResultsToTeamCity(profilerSnapshotsDir: Path, artifactName: String) =
this.addCompletionHandler {
testContext.publishArtifact(source = profilerSnapshotsDir, artifactName = artifactName)
}
fun installProfiler(): IDERunContext {
return when (val profilerType = testContext.profilerType) {
ProfilerType.ASYNC, ProfilerType.YOURKIT -> {
val profiler = di.direct.instance<ProfilerInjector>(tag = profilerType)
logOutput("Injecting profiler ${profiler.type.kind}")
profiler.injectProfiler(this)
}
ProfilerType.NONE -> {
logOutput("No profiler is specified. Skipping profiler setup")
return this
}
}
}
// TODO: refactor this
private fun prepareToRunIDE(): IDEStartResult {
StarterBus.post(IdeLaunchEvent(EventState.BEFORE, this))
deleteSavedAppStateOnMac()
val paths = testContext.paths
val logsDir = paths.logsDir.createDirectories()
paths.snapshotsDir.createDirectories()
val jvmCrashLogDirectory = logsDir.resolve("jvm-crash").createDirectories()
val heapDumpOnOomDirectory = logsDir.resolve("heap-dump").createDirectories()
val disabledPlugins = paths.configDir.resolve("disabled_plugins.txt")
if (disabledPlugins.toFile().exists()) {
logOutput("The list of disabled plugins: " + disabledPlugins.toFile().readText())
}
val stdout = if (verboseOutput) ExecOutputRedirect.ToStdOut("[ide-${contextName}-out]") else ExecOutputRedirect.ToString()
val stderr = ExecOutputRedirect.ToStdOut("[ide-${contextName}-err]")
var successfulRun = true
val host by lazy { di.direct.instance<CodeInjector>() }
try {
val codeBuilder = codeBuilder
if (codeBuilder != null) {
host.codeBuilder()
}
val finalOptions = testContext.ide.originalVMOptions
.disableStartupDialogs()
.usingStartupFramework()
.setFatalErrorNotificationEnabled()
.setFlagIntegrationTests()
.takeScreenshotIfFailure(logsDir)
.withJvmCrashLogDirectory(jvmCrashLogDirectory)
.withHeapDumpOnOutOfMemoryDirectory(heapDumpOnOomDirectory)
.let { testContext.testCase.vmOptionsFix(it) }
.let { testContext.patchVMOptions(it) }
.patchVMOptions()
.let {
if (!useStartupScript) {
require(commands.count() > 0) { "script builder is not allowed when useStartupScript is disabled" }
it
}
else
it.installTestScript(contextName, paths, commands)
}
if (codeBuilder != null) {
host.setup(testContext)
}
testContext.setProviderMemoryOnlyOnLinux()
val jdkHome: Path by lazy {
try {
testContext.ide.resolveAndDownloadTheSameJDK()
}
catch (e: Exception) {
logError("Failed to download the same JDK as in ${testContext.ide.build}")
logError(e.stackTraceToString())
val defaultJavaHome = resolveInstalledJdk11()
logOutput("JDK is not found in ${testContext.ide.build}. Fallback to default java: $defaultJavaHome")
defaultJavaHome
}
}
val startConfig = testContext.ide.startConfig(finalOptions, logsDir)
if (startConfig is Closeable) {
addCompletionHandler { startConfig.close() }
}
val commandLineArgs = when (val cmd = commandLine ?: IDECommandLine.OpenTestCaseProject) {
is IDECommandLine.Args -> cmd.args
is IDECommandLine.OpenTestCaseProject -> listOf(testContext.resolvedProjectHome.toAbsolutePath().toString())
}
val finalEnvVariables = startConfig.environmentVariables + finalOptions.env
val extendedEnvVariablesWithJavaHome = finalEnvVariables.toMutableMap()
extendedEnvVariablesWithJavaHome.putIfAbsent("JAVA_HOME", jdkHome.absolutePathString())
val finalArgs = startConfig.commandLine + commandLineArgs
logOutput(buildString {
appendLine("Starting IDE for ${contextName} with timeout $runTimeout")
appendLine(" Command line: [" + finalArgs.joinToString() + "]")
appendLine(" VM Options: [" + finalOptions.toString().lineSequence().map { it.trim() }.joinToString(" ") + "]")
appendLine(" On Java : [" + System.getProperty("java.home") + "]")
})
File(finalArgs.first()).setExecutable(true)
val executionTime = measureTime {
exec(
presentablePurpose = "run-ide-$contextName",
workDir = startConfig.workDir,
environmentVariables = extendedEnvVariablesWithJavaHome,
timeout = runTimeout,
args = finalArgs,
errorDiagnosticFiles = startConfig.errorDiagnosticFiles,
stdoutRedirect = stdout,
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 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) }
}
}
},
onBeforeKilled = { process, pid ->
if (!expectedKill) {
val javaProcessId by lazy { getJavaProcessId(jdkHome, startConfig.workDir, pid, process) }
if (collectNativeThreads) {
val fileToStoreNativeThreads = logsDir.resolve("native-thread-dumps.txt")
startProfileNativeThreads(javaProcessId.toString())
Thread.sleep(15.seconds.inWholeMilliseconds)
stopProfileNativeThreads(javaProcessId.toString(), fileToStoreNativeThreads.toAbsolutePath().toString())
}
val dumpFile = logsDir.resolve("threadDump-before-kill-${System.currentTimeMillis()}" + ".txt")
catchAll { collectJavaThreadDump(jdkHome, startConfig.workDir, javaProcessId, dumpFile) }
}
takeScreenshot(logsDir)
}
)
}
logOutput("IDE run $contextName completed in $executionTime")
require(FileSystem.countFiles(paths.configDir) > 3) {
"IDE must have created files under config directory at ${paths.configDir}. Were .vmoptions included correctly?"
}
require(FileSystem.countFiles(paths.systemDir) > 1) {
"IDE must have created files under system directory at ${paths.systemDir}. Were .vmoptions included correctly?"
}
val vmOptionsDiff = startConfig.vmOptionsDiff()
if (vmOptionsDiff != null && !vmOptionsDiff.isEmpty) {
logOutput("VMOptions were changed:")
logOutput("new lines:")
vmOptionsDiff.newLines.forEach { logOutput(" $it") }
logOutput("removed lines:")
vmOptionsDiff.missingLines.forEach { logOutput(" $it") }
logOutput()
}
return IDEStartResult(runContext = this, executionTime = executionTime, vmOptionsDiff = vmOptionsDiff, logsDir = logsDir)
}
catch (t: Throwable) {
successfulRun = false
if (t is ExecTimeoutException && !expectedKill) {
error("Timeout of IDE run $contextName for $runTimeout")
}
else {
logOutput("IDE run for $contextName has been expected to be killed after $runTimeout")
}
val failureCauseFile = testContext.paths.logsDir.resolve("failure_cause.txt")
val errorMessage = if (Files.exists(failureCauseFile)) {
Files.readString(failureCauseFile)
}
else {
t.message ?: t.javaClass.name
}
if (!expectedKill) {
throw Exception(errorMessage, t)
}
else {
return IDEStartResult(runContext = this, executionTime = runTimeout, logsDir = logsDir)
}
}
finally {
try {
if (SystemInfo.isWindows) {
destroyGradleDaemonProcessIfExists()
}
listOf(heapDumpOnOomDirectory, jvmCrashLogDirectory).filter { dir ->
dir.listDirectoryEntries().isEmpty()
}.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))
val snapshotFiles = Files.list(testContext.paths.snapshotsDir).use { it.filter { it.isRegularFile() }.toList() }
if (snapshotFiles.isNotEmpty()) {
testContext.publishArtifact(testContext.paths.snapshotsDir, contextName, formatArtifactName("snapshots", testContext.testName))
}
if (codeBuilder != null) {
host.tearDown(testContext)
}
val closeContext = object : IDERunCloseContext {
override val wasRunSuccessful: Boolean = successfulRun
}
closeHandlers.forEach {
try {
it.invoke(closeContext)
}
catch (t: Throwable) {
logOutput("Failed to complete close step. ${t.message}.\n" + t)
t.printStackTrace(System.err)
}
}
}
finally {
StarterBus.post(IdeLaunchEvent(EventState.AFTER, this))
}
}
}
fun runIDE(): IDEStartResult {
return installProfiler()
.prepareToRunIDE()
}
private fun deleteSavedAppStateOnMac() {
if (SystemInfo.isMac) {
val filesToBeDeleted = listOf(
"com.jetbrains.${testContext.testCase.ideInfo.installerProductName}-EAP.savedState",
"com.jetbrains.${testContext.testCase.ideInfo.installerProductName}.savedState"
)
val home = System.getProperty("user.home")
val savedAppStateDir = Paths.get(home).resolve("Library").resolve("Saved Application State")
savedAppStateDir.toFile()
.walkTopDown().maxDepth(1)
.filter { file -> filesToBeDeleted.any { fileToBeDeleted -> file.name == fileToBeDeleted } }
.forEach { it.deleteRecursively() }
}
}
}

View File

@@ -1,6 +0,0 @@
package com.intellij.ide.starter.runner
import com.intellij.ide.starter.bus.Event
import com.intellij.ide.starter.bus.EventState
class IdeLaunchEvent(state: EventState, runContext: IDERunContext) : Event<IDERunContext>(state, runContext)

View File

@@ -1,106 +0,0 @@
package com.intellij.ide.starter.runner
import com.intellij.ide.starter.ci.CIServer
import com.intellij.ide.starter.di.di
import com.intellij.ide.starter.ide.*
import com.intellij.ide.starter.models.IdeInfo
import com.intellij.ide.starter.models.TestCase
import com.intellij.ide.starter.path.GlobalPaths
import com.intellij.ide.starter.path.IDEDataPaths
import com.intellij.ide.starter.plugins.PluginInstalledState
import com.intellij.ide.starter.utils.catchAll
import com.intellij.ide.starter.utils.logOutput
import org.kodein.di.direct
import org.kodein.di.factory
import org.kodein.di.instance
import java.io.Closeable
import kotlin.io.path.div
/**
* [ciServer] - use [NoCIServer] for only local run. Otherwise - pass implementation of CIServer
*/
interface TestContainer<T> : Closeable {
val ciServer: CIServer
var useLatestDownloadedIdeBuild: Boolean
val allContexts: MutableList<IDETestContext>
val setupHooks: MutableList<IDETestContext.() -> IDETestContext>
override fun close() {
for (context in allContexts) {
catchAll { context.paths.close() }
}
}
/**
* Allows to apply the common configuration to all created IDETestContext instances
*/
fun withSetupHook(hook: IDETestContext.() -> IDETestContext): T = apply {
setupHooks += hook
} as T
/**
* Makes the test use the latest available locally IDE build for testing.
*/
fun useLatestDownloadedIdeBuild(): T = apply {
assert(!ciServer.isBuildRunningOnCI)
useLatestDownloadedIdeBuild = true
} as T
fun resolveIDE(ideInfo: IdeInfo): Pair<String, InstalledIde> {
return di.direct.factory<IdeInfo, IdeInstallator>().invoke(ideInfo).install(ideInfo)
}
fun installPerformanceTestingPluginIfMissing(context: IDETestContext) {
val performancePluginId = "com.jetbrains.performancePlugin"
context.pluginConfigurator.apply {
val pluginState = getPluginInstalledState(performancePluginId)
if (pluginState != PluginInstalledState.INSTALLED && pluginState != PluginInstalledState.BUNDLED_TO_IDE)
setupPluginFromPluginManager(performancePluginId, ideBuild = context.ide.build)
}
}
/** 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." }
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" }
val testDirectory = (di.direct.instance<GlobalPaths>().testsDirectory / "${testCase.ideInfo.productCode}-$buildNumber") / testName
val paths = IDEDataPaths.createPaths(testName, testDirectory, testCase.useInMemoryFileSystem)
logOutput("Using IDE paths for $testName: $paths")
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
val baseContext = when (testCase.ideInfo == IdeProductProvider.AI) {
true -> context
.addVMOptionsPatch {
overrideDirectories(paths)
.withEnv("STUDIO_VM_OPTIONS", ide.patchedVMOptionsFile.toString())
}
false -> context
.disableInstantIdeShutdown()
.disableFusSendingOnIdeClose()
.disableJcef()
.disableLinuxNativeMenuForce()
.withGtk2OnLinux()
.disableGitLogIndexing()
.enableSlowOperationsInEdtInTests()
.collectOpenTelemetry()
.addVMOptionsPatch {
overrideDirectories(paths)
}
}
return setupHooks
.fold(baseContext.updateGeneralSettings()) { acc, hook -> acc.hook() }
.apply { installPerformanceTestingPluginIfMissing(this) }
}
}

View File

@@ -1,23 +0,0 @@
package com.intellij.ide.starter.sdk
import com.intellij.openapi.projectRoots.impl.jdkDownloader.JdkItem
import java.nio.file.Path
data class JdkDownloadItem(
val jdk: JdkItem,
private val download: () -> JdkItemPaths
) {
private val installJdk = lazy { download() }
val home: Path get() = installJdk.value.homePath
val installPath: Path get() = installJdk.value.installPath
fun toSdk(sdkName: String) = SdkObject(
sdkName = sdkName,
sdkType = "JavaSDK",
sdkPath = home,
)
override fun equals(other: Any?) = other is JdkDownloadItem && other.jdk == jdk
override fun hashCode() = jdk.hashCode()
override fun toString(): String = "JdkDownloadItem(${jdk.fullPresentationText})"
}

Some files were not shown because too many files have changed in this diff Show More