Add testing support for MPP projects
This commit is contained in:
+100
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.idea.gradle.testing
|
||||
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.externalSystem.model.DataNode
|
||||
import com.intellij.openapi.externalSystem.model.ProjectKeys
|
||||
import com.intellij.openapi.externalSystem.model.project.ModuleData
|
||||
import com.intellij.openapi.externalSystem.model.project.ProjectData
|
||||
import com.intellij.openapi.externalSystem.model.task.TaskData
|
||||
import com.intellij.openapi.externalSystem.util.Order
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.util.Consumer
|
||||
import org.gradle.tooling.model.idea.IdeaModule
|
||||
import org.jetbrains.kotlin.gradle.KotlinMPPGradleModel
|
||||
import org.jetbrains.kotlin.gradle.KotlinMPPGradleModelBuilder
|
||||
import org.jetbrains.kotlin.idea.configuration.getMppModel
|
||||
import org.jetbrains.plugins.gradle.service.project.AbstractProjectResolverExtension
|
||||
|
||||
@Order(Int.MIN_VALUE)
|
||||
open class KotlinTestTasksResolver : AbstractProjectResolverExtension() {
|
||||
companion object {
|
||||
private const val ENABLED_REGISTRY_KEY = "kotlin.gradle.testing.enabled"
|
||||
}
|
||||
|
||||
private val LOG by lazy { Logger.getInstance(KotlinTestTasksResolver::class.java) }
|
||||
|
||||
override fun getToolingExtensionsClasses(): Set<Class<out Any>> {
|
||||
return setOf(KotlinMPPGradleModelBuilder::class.java, Unit::class.java)
|
||||
}
|
||||
|
||||
override fun populateModuleTasks(
|
||||
gradleModule: IdeaModule,
|
||||
ideModule: DataNode<ModuleData>,
|
||||
ideProject: DataNode<ProjectData>
|
||||
): MutableCollection<TaskData> {
|
||||
if (!Registry.`is`(ENABLED_REGISTRY_KEY))
|
||||
return super.populateModuleTasks(gradleModule, ideModule, ideProject)
|
||||
|
||||
val mppModel = resolverCtx.getMppModel(gradleModule)
|
||||
?: return super.populateModuleTasks(gradleModule, ideModule, ideProject)
|
||||
|
||||
return postprocessTaskData(mppModel, ideModule, nextResolver.populateModuleTasks(gradleModule, ideModule, ideProject))
|
||||
}
|
||||
|
||||
private fun postprocessTaskData(
|
||||
mppModel: KotlinMPPGradleModel,
|
||||
ideModule: DataNode<ModuleData>,
|
||||
originalTaskData: MutableCollection<TaskData>
|
||||
): MutableCollection<TaskData> {
|
||||
val testTaskNames = mutableSetOf<String>().apply {
|
||||
mppModel.targets.forEach { target ->
|
||||
target.testTasks.forEach { testTaskModel ->
|
||||
add(testTaskModel.taskName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun buildNewTaskDataMarkedAsTest(original: TaskData): TaskData =
|
||||
TaskData(original.owner, original.name, original.linkedExternalProjectPath, original.description).apply {
|
||||
group = original.group
|
||||
type = original.type
|
||||
isInherited = original.isInherited
|
||||
|
||||
isTest = true
|
||||
}
|
||||
|
||||
val replacementMap: Map<TaskData, TaskData> = mutableMapOf<TaskData, TaskData>().apply {
|
||||
originalTaskData.forEach {
|
||||
if (it.name in testTaskNames && !it.isTest) {
|
||||
put(it, buildNewTaskDataMarkedAsTest(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ideModule.children.filter { it.data in replacementMap }.forEach { it.clear(true) }
|
||||
replacementMap.values.forEach { ideModule.createChild(ProjectKeys.TASK, it) }
|
||||
|
||||
return originalTaskData.mapTo(arrayListOf<TaskData>()) { replacementMap[it] ?: it }
|
||||
}
|
||||
|
||||
override fun enhanceTaskProcessing(taskNames: MutableList<String>, jvmAgentSetup: String?, initScriptConsumer: Consumer<String>) {
|
||||
if (!Registry.`is`(ENABLED_REGISTRY_KEY))
|
||||
return
|
||||
|
||||
try {
|
||||
val testLoggerScript = javaClass
|
||||
.getResourceAsStream("/org/jetbrains/kotlin/idea/gradle/testing/KotlinMppTestLogger.groovy")
|
||||
.bufferedReader()
|
||||
.readText()
|
||||
|
||||
initScriptConsumer.consume(testLoggerScript)
|
||||
} catch (e: Exception) {
|
||||
LOG.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.idea.gradle.testing.common
|
||||
|
||||
import com.intellij.openapi.module.Module
|
||||
import org.jetbrains.kotlin.idea.run.AbstractKotlinMultiplatformTestClassGradleConfigurationProducer
|
||||
import org.jetbrains.kotlin.platform.TargetPlatform
|
||||
import org.jetbrains.kotlin.platform.isCommon
|
||||
|
||||
class KotlinMultiplatformCommonTestClassGradleConfigurationProducer : AbstractKotlinMultiplatformTestClassGradleConfigurationProducer() {
|
||||
override fun isApplicable(module: Module, platform: TargetPlatform) = platform.isCommon()
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.idea.gradle.testing.common
|
||||
|
||||
import com.intellij.openapi.module.Module
|
||||
import org.jetbrains.kotlin.idea.run.AbstractKotlinMultiplatformTestMethodGradleConfigurationProducer
|
||||
import org.jetbrains.kotlin.platform.TargetPlatform
|
||||
import org.jetbrains.kotlin.platform.isCommon
|
||||
|
||||
class KotlinMultiplatformCommonTestMethodGradleConfigurationProducer : AbstractKotlinMultiplatformTestMethodGradleConfigurationProducer() {
|
||||
override fun isApplicable(module: Module, platform: TargetPlatform) = platform.isCommon()
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.idea.gradle.testing.js
|
||||
|
||||
import com.intellij.execution.actions.ConfigurationContext
|
||||
import com.intellij.openapi.module.Module
|
||||
import org.jetbrains.kotlin.idea.js.KotlinJSRunConfigurationDataProvider
|
||||
import org.jetbrains.kotlin.idea.run.AbstractKotlinMultiplatformTestClassGradleConfigurationProducer
|
||||
import org.jetbrains.kotlin.platform.TargetPlatform
|
||||
import org.jetbrains.kotlin.platform.js.isJs
|
||||
|
||||
class KotlinMultiplatformJsTestClassGradleConfigurationProducer
|
||||
: AbstractKotlinMultiplatformTestClassGradleConfigurationProducer(), KotlinJSRunConfigurationDataProvider<Unit>
|
||||
{
|
||||
override val isForTests: Boolean get() = true
|
||||
override fun isApplicable(module: Module, platform: TargetPlatform) = platform.isJs()
|
||||
|
||||
override fun getConfigurationData(context: ConfigurationContext) = Unit
|
||||
override val hasTestFramework get() = true
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.idea.gradle.testing.js
|
||||
|
||||
import com.intellij.execution.actions.ConfigurationContext
|
||||
import com.intellij.openapi.module.Module
|
||||
import org.jetbrains.kotlin.idea.js.KotlinJSRunConfigurationDataProvider
|
||||
import org.jetbrains.kotlin.idea.run.AbstractKotlinMultiplatformTestMethodGradleConfigurationProducer
|
||||
import org.jetbrains.kotlin.platform.TargetPlatform
|
||||
import org.jetbrains.kotlin.platform.js.isJs
|
||||
|
||||
class KotlinMultiplatformJsTestMethodGradleConfigurationProducer
|
||||
: AbstractKotlinMultiplatformTestMethodGradleConfigurationProducer(), KotlinJSRunConfigurationDataProvider<Unit>
|
||||
{
|
||||
override val isForTests: Boolean get() = true
|
||||
override fun isApplicable(module: Module, platform: TargetPlatform) = platform.isJs()
|
||||
|
||||
override fun getConfigurationData(context: ConfigurationContext) = Unit
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.idea.gradle.testing.native
|
||||
|
||||
import com.intellij.openapi.module.Module
|
||||
import org.jetbrains.kotlin.ide.konan.KotlinNativeRunConfigurationProvider
|
||||
import org.jetbrains.kotlin.idea.run.AbstractKotlinMultiplatformTestClassGradleConfigurationProducer
|
||||
import org.jetbrains.kotlin.platform.TargetPlatform
|
||||
import org.jetbrains.kotlin.platform.konan.isNative
|
||||
|
||||
class KotlinMultiplatformNativeTestClassGradleConfigurationProducer
|
||||
: AbstractKotlinMultiplatformTestClassGradleConfigurationProducer(), KotlinNativeRunConfigurationProvider
|
||||
{
|
||||
override val isForTests: Boolean get() = true
|
||||
override fun isApplicable(module: Module, platform: TargetPlatform) = platform.isNative()
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.idea.gradle.testing.native
|
||||
|
||||
import com.intellij.openapi.module.Module
|
||||
import org.jetbrains.kotlin.ide.konan.KotlinNativeRunConfigurationProvider
|
||||
import org.jetbrains.kotlin.idea.run.AbstractKotlinMultiplatformTestMethodGradleConfigurationProducer
|
||||
import org.jetbrains.kotlin.platform.TargetPlatform
|
||||
import org.jetbrains.kotlin.platform.konan.isNative
|
||||
|
||||
class KotlinMultiplatformNativeTestMethodGradleConfigurationProducer
|
||||
: AbstractKotlinMultiplatformTestMethodGradleConfigurationProducer(), KotlinNativeRunConfigurationProvider
|
||||
{
|
||||
override val isForTests: Boolean get() = true
|
||||
override fun isApplicable(module: Module, platform: TargetPlatform) = platform.isNative()
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<idea-plugin>
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
<registryKey key="kotlin.gradle.testing.enabled"
|
||||
description="Import Kotlin Gradle test tasks and process test output"
|
||||
defaultValue="false"
|
||||
restartRequired="false"/>
|
||||
<registryKey key="kotlin.mpp.tests.force.gradle"
|
||||
description="Run multi-platform tests with Gradle runner even if the platform runner is used by default.
|
||||
This setting currently affects only HMPP projects. You may need to delete existing test configurations for the change to take place."
|
||||
defaultValue="true"
|
||||
restartRequired="false"/>
|
||||
<runConfigurationProducer implementation="org.jetbrains.kotlin.idea.gradle.testing.js.KotlinMultiplatformJsTestClassGradleConfigurationProducer"/>
|
||||
<runConfigurationProducer implementation="org.jetbrains.kotlin.idea.gradle.testing.js.KotlinMultiplatformJsTestMethodGradleConfigurationProducer"/>
|
||||
<runConfigurationProducer implementation="org.jetbrains.kotlin.idea.gradle.testing.native.KotlinMultiplatformNativeTestClassGradleConfigurationProducer"/>
|
||||
<runConfigurationProducer implementation="org.jetbrains.kotlin.idea.gradle.testing.native.KotlinMultiplatformNativeTestMethodGradleConfigurationProducer"/>
|
||||
<runConfigurationProducer implementation="org.jetbrains.kotlin.idea.gradle.testing.common.KotlinMultiplatformCommonTestClassGradleConfigurationProducer"/>
|
||||
<runConfigurationProducer implementation="org.jetbrains.kotlin.idea.gradle.testing.common.KotlinMultiplatformCommonTestMethodGradleConfigurationProducer"/>
|
||||
</extensions>
|
||||
<extensions defaultExtensionNs="org.jetbrains.plugins.gradle">
|
||||
<projectResolve implementation="org.jetbrains.kotlin.idea.gradle.testing.KotlinTestTasksResolver"/>
|
||||
</extensions>
|
||||
</idea-plugin>
|
||||
@@ -20,6 +20,7 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio.
|
||||
<depends optional="true" config-file="junit.xml">JUnit</depends>
|
||||
<depends optional="true" config-file="gradle.xml">com.intellij.gradle</depends>
|
||||
<depends optional="true" config-file="gradle-java.xml">org.jetbrains.plugins.gradle</depends>
|
||||
<depends optional="true" config-file="kotlin-gradle-testing.xml">org.jetbrains.plugins.gradle</depends>
|
||||
<depends optional="true" config-file="gradle-groovy.xml">org.intellij.groovy</depends>
|
||||
<depends optional="true" config-file="maven.xml">org.jetbrains.idea.maven</depends>
|
||||
<depends optional="true" config-file="testng-j.xml">TestNG-J</depends>
|
||||
|
||||
@@ -20,6 +20,7 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio.
|
||||
<depends optional="true" config-file="junit.xml">JUnit</depends>
|
||||
<depends optional="true" config-file="gradle.xml">org.jetbrains.plugins.gradle</depends>
|
||||
<depends optional="true" config-file="gradle-java.xml">org.jetbrains.plugins.gradle.java</depends>
|
||||
<depends optional="true" config-file="kotlin-gradle-testing.xml">org.jetbrains.plugins.gradle.java</depends>
|
||||
<depends optional="true" config-file="gradle-groovy.xml">org.intellij.groovy</depends>
|
||||
<depends optional="true" config-file="maven.xml">org.jetbrains.idea.maven</depends>
|
||||
<depends optional="true" config-file="testng-j.xml">TestNG-J</depends>
|
||||
|
||||
@@ -20,6 +20,7 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio.
|
||||
<depends optional="true" config-file="junit.xml">JUnit</depends>
|
||||
<depends optional="true" config-file="gradle.xml">org.jetbrains.plugins.gradle</depends>
|
||||
<depends optional="true" config-file="gradle-java.xml">org.jetbrains.plugins.gradle.java</depends>
|
||||
<depends optional="true" config-file="kotlin-gradle-testing.xml">org.jetbrains.plugins.gradle.java</depends>
|
||||
<depends optional="true" config-file="gradle-groovy.xml">org.intellij.groovy</depends>
|
||||
<depends optional="true" config-file="maven.xml">org.jetbrains.idea.maven</depends>
|
||||
<depends optional="true" config-file="testng-j.xml">TestNG-J</depends>
|
||||
|
||||
@@ -21,6 +21,7 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio.
|
||||
<depends optional="true" config-file="junit.xml">JUnit</depends>
|
||||
<depends optional="true" config-file="gradle.xml">org.jetbrains.plugins.gradle</depends>
|
||||
<depends optional="true" config-file="gradle-java.xml">org.jetbrains.plugins.gradle.java</depends>
|
||||
<depends optional="true" config-file="kotlin-gradle-testing.xml">org.jetbrains.plugins.gradle.java</depends>
|
||||
<depends optional="true" config-file="gradle-groovy.xml">org.intellij.groovy</depends>
|
||||
<depends optional="true" config-file="maven.xml">org.jetbrains.idea.maven</depends>
|
||||
<depends optional="true" config-file="testng-j.xml">TestNG-J</depends>
|
||||
|
||||
@@ -21,6 +21,7 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio.
|
||||
<depends optional="true" config-file="junit.xml">JUnit</depends>
|
||||
<depends optional="true" config-file="gradle.xml">org.jetbrains.plugins.gradle</depends>
|
||||
<depends optional="true" config-file="gradle-java.xml">org.jetbrains.plugins.gradle.java</depends>
|
||||
<depends optional="true" config-file="kotlin-gradle-testing.xml">org.jetbrains.plugins.gradle.java</depends>
|
||||
<depends optional="true" config-file="gradle-groovy.xml">org.intellij.groovy</depends>
|
||||
<depends optional="true" config-file="maven.xml">org.jetbrains.idea.maven</depends>
|
||||
<depends optional="true" config-file="testng-j.xml">TestNG-J</depends>
|
||||
|
||||
@@ -21,6 +21,7 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio.
|
||||
<depends optional="true" config-file="junit.xml">JUnit</depends>
|
||||
<depends optional="true" config-file="gradle.xml">com.intellij.gradle</depends>
|
||||
<depends optional="true" config-file="gradle-java.xml">org.jetbrains.plugins.gradle</depends>
|
||||
<depends optional="true" config-file="kotlin-gradle-testing.xml">org.jetbrains.plugins.gradle</depends>
|
||||
<depends optional="true" config-file="gradle-groovy.xml">org.intellij.groovy</depends>
|
||||
<depends optional="true" config-file="maven.xml">org.jetbrains.idea.maven</depends>
|
||||
<depends optional="true" config-file="testng-j.xml">TestNG-J</depends>
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import groovy.xml.MarkupBuilder
|
||||
import org.gradle.api.internal.tasks.testing.TestDescriptorInternal
|
||||
|
||||
class KotlinMppTestLogger {
|
||||
static def configureTestEventLogging(def task) {
|
||||
task.beforeSuite { descriptor -> logTestEvent("beforeSuite", descriptor, null, null) }
|
||||
task.afterSuite { descriptor, result -> logTestEvent("afterSuite", descriptor, null, result) }
|
||||
|
||||
task.beforeTest { descriptor -> logTestEvent("beforeTest", descriptor, null, null) }
|
||||
task.onOutput { descriptor, event -> logTestEvent("onOutput", descriptor, event, null) }
|
||||
task.afterTest { descriptor, result -> logTestEvent("afterTest", descriptor, null, result) }
|
||||
}
|
||||
|
||||
static def logTestEvent(testEventType, TestDescriptorInternal testDescriptor, testEvent, testResult) {
|
||||
def writer = new StringWriter()
|
||||
def xml = new MarkupBuilder(writer)
|
||||
xml.event(type: testEventType) {
|
||||
test(id: testDescriptor.id, parentId: testDescriptor.parent?.id ?: '') {
|
||||
if (testDescriptor) {
|
||||
descriptor(name: testDescriptor.displayName ?: '', className: testDescriptor.classDisplayName ?: '')
|
||||
}
|
||||
if (testEvent) {
|
||||
def message = escapeCdata(testEvent.message)
|
||||
event(destination: testEvent.destination) {
|
||||
xml.mkp.yieldUnescaped("$message")
|
||||
}
|
||||
}
|
||||
if (testResult) {
|
||||
def errorMsg = escapeCdata(testResult.exception?.message ?: '')
|
||||
def stackTrace = escapeCdata(getStackTrace(testResult.exception))
|
||||
result(resultType: testResult.resultType ?: '', startTime: testResult.startTime, endTime: testResult.endTime) {
|
||||
def exception = testResult.exception
|
||||
if (exception?.message?.trim()) xml.mkp.yieldUnescaped("<errorMsg>$errorMsg</errorMsg>")
|
||||
if (exception) xml.mkp.yieldUnescaped("<stackTrace>$stackTrace</stackTrace>")
|
||||
|
||||
if ('kotlin.AssertionError'.equals(exception?.class?.name) || exception instanceof AssertionError) {
|
||||
failureType('assertionFailed')
|
||||
return
|
||||
}
|
||||
|
||||
failureType('error')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeLog(writer.toString())
|
||||
}
|
||||
|
||||
static String escapeCdata(String s) {
|
||||
return "<![CDATA[" + s?.getBytes("UTF-8")?.encodeBase64()?.toString() + "]]>";
|
||||
}
|
||||
|
||||
static def wrap(String s) {
|
||||
if(!s) return s;
|
||||
s.replaceAll("\r\n|\n\r|\n|\r","<ijLogEol/>\n")
|
||||
}
|
||||
|
||||
static def writeLog(s) {
|
||||
println String.format("\n<ijLog>%s</ijLog>", wrap(s))
|
||||
}
|
||||
|
||||
static def logTestReportLocation(def report) {
|
||||
if(!report) return
|
||||
def writer = new StringWriter()
|
||||
def xml = new MarkupBuilder(writer)
|
||||
xml.event(type: 'reportLocation', testReport: report)
|
||||
writeLog(writer.toString());
|
||||
}
|
||||
|
||||
static def logConfigurationError(aTitle, aMessage, boolean openSettings) {
|
||||
def writer = new StringWriter()
|
||||
def xml = new MarkupBuilder(writer)
|
||||
xml.event(type: 'configurationError', openSettings: openSettings) {
|
||||
title(aTitle)
|
||||
message(aMessage)
|
||||
}
|
||||
writeLog(writer.toString());
|
||||
}
|
||||
|
||||
static def getStackTrace(Throwable t) {
|
||||
if(!t) return ''
|
||||
StringWriter sw = new StringWriter()
|
||||
t.printStackTrace(new PrintWriter(sw))
|
||||
sw.toString()
|
||||
}
|
||||
}
|
||||
|
||||
gradle.taskGraph.beforeTask { task ->
|
||||
def taskSuperClass = task.class
|
||||
while (taskSuperClass != Object.class) {
|
||||
if (taskSuperClass.canonicalName == "org.jetbrains.kotlin.gradle.tasks.KotlinTest") {
|
||||
try {
|
||||
KotlinMppTestLogger.logTestReportLocation(task.reports?.html?.entryPoint?.path)
|
||||
KotlinMppTestLogger.configureTestEventLogging(task)
|
||||
task.testLogging.showStandardStreams = false
|
||||
}
|
||||
catch (all) {
|
||||
logger.error("", all)
|
||||
}
|
||||
return
|
||||
} else {
|
||||
taskSuperClass = taskSuperClass.superclass
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user