Skip to content

Commit

Permalink
feat: add Copy Code action to copy test element as code
Browse files Browse the repository at this point in the history
It produces a draft code, so it is easier to start using DSL
by generating code from the UI.
  • Loading branch information
vlsi committed Jun 15, 2023
1 parent 1398670 commit 7a7e946
Show file tree
Hide file tree
Showing 10 changed files with 482 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public final class ActionNames {
public static final String COLLAPSE_ALL = "collapse all"; // $NON-NLS-1$
public static final String COMPILE_JSR223 = "compile_jsr223"; // $NON-NLS-1$
public static final String COPY = "Copy"; // $NON-NLS-1$
public static final String COPY_CODE = "copy_code"; // $NON-NLS-1$
public static final String CUT = "Cut"; // $NON-NLS-1$
public static final String DEBUG_ON = "debug_on"; // $NON-NLS-1$
public static final String DEBUG_OFF = "debug_off"; // $NON-NLS-1$
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,9 @@ public static void addFileMenu(JPopupMenu menu, boolean addSaveTestFragmentMenu)
ActionNames.SAVE_AS_TEST_FRAGMENT));
}
addSeparator(menu);
JMenuItem saveKotlinDsl = makeMenuItemRes("copy_code", // $NON-NLS-1$
ActionNames.COPY_CODE);
menu.add(saveKotlinDsl);
JMenuItem savePicture = makeMenuItemRes("save_as_image",// $NON-NLS-1$
ActionNames.SAVE_GRAPHICS,
KeyStrokes.SAVE_GRAPHICS);
Expand Down
238 changes: 238 additions & 0 deletions src/core/src/main/kotlin/org/apache/jmeter/dsl/DslPrinterTraverser.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.jmeter.dsl

import org.apache.jmeter.gui.tree.JMeterTreeNode
import org.apache.jmeter.testelement.TestElement
import org.apache.jmeter.testelement.TestElementSchema
import org.apache.jmeter.testelement.property.BooleanProperty
import org.apache.jmeter.testelement.property.CollectionProperty
import org.apache.jmeter.testelement.property.DoubleProperty
import org.apache.jmeter.testelement.property.FloatProperty
import org.apache.jmeter.testelement.property.IntegerProperty
import org.apache.jmeter.testelement.property.JMeterProperty
import org.apache.jmeter.testelement.property.LongProperty
import org.apache.jmeter.testelement.property.MultiProperty
import org.apache.jmeter.testelement.property.StringProperty
import org.apache.jmeter.testelement.property.TestElementProperty
import org.apache.jmeter.testelement.schema.PropertyDescriptor
import org.apache.jorphan.collections.HashTree
import org.apache.jorphan.collections.HashTreeTraverser
import org.apiguardian.api.API
import java.io.StringWriter

/**
* Prints [HashTree] or [TestElement] as JMeter DSL.
* @since 5.6
*/
@API(status = API.Status.EXPERIMENTAL, since = "5.6")
public class DslPrinterTraverser(
private val detailLevel: DetailLevel = DetailLevel.NON_DEFAULT
) : HashTreeTraverser {
public enum class DetailLevel {
ALL, NON_DEFAULT
}
private companion object {
val SPECIAL_CHARS = Regex("[\"\n\r\t\b\\\\$]")
}

private val sw = StringWriter()
private var level = 0
private var plusPosition = -1

override fun toString(): String = sw.toString()

public fun append(testElement: TestElement): DslPrinterTraverser {
addNode(testElement, HashTree())
subtractNode()
return this
}

override fun addNode(node: Any, subTree: HashTree) {
if (sw.buffer.isNotEmpty()) {
append('\n')
}
when (node) {
is TestElement -> appendElement(node)
is JMeterTreeNode -> appendElement(node.testElement)
}
}

private fun appendElement(te: TestElement) {
indent()
plusPosition = sw.buffer.length
append(te::class.java.name).append("::class {\n")
level += 1
appendProperties(te, true)
}

override fun subtractNode() {
level -= 1
// Omit empty braces in case the element did not have properties
// Note: if all the properties are default, we do not print them,
// so we don't use "properties.isEmpty" condition
if (sw.buffer.endsWith(" {\n")) {
sw.buffer.setLength(sw.buffer.length - " {\n".length)
sw.buffer.insert(plusPosition, '+')
append("\n")
} else {
indent().append("}\n")
}
}

override fun processPath() {
}

private fun appendProperties(te: TestElement, canSkipTestClass: Boolean) {
val emptyTe = te::class.java.getDeclaredConstructor().newInstance()

val schema = te.schema
val schemaProps = mutableMapOf<String, JMeterProperty>()
val otherProps = mutableListOf<JMeterProperty>()
for (property in te.propertyIterator()) {
if (emptyTe.getPropertyOrNull(property.name) == property && detailLevel != DetailLevel.ALL) {
// If the property is the same as in newly created element, avoid printing it in the DSL
continue
}
val prop = schema.properties[property.name]
if (detailLevel != DetailLevel.ALL) {
val stringValue = property.stringValue
if (prop == TestElementSchema.testClass && stringValue == te::class.java.name && canSkipTestClass) {
continue
}
if ((property is StringProperty && stringValue.isNullOrEmpty()) ||
stringValue == prop?.defaultValueAsString
) {
continue
}
}

if (prop == null) {
otherProps += property
continue
}

schemaProps[prop.shortName] = property
}
if (schemaProps.isNotEmpty()) {
indent().append("props {\n")
level += 1
for ((name, value) in schemaProps) {
indent().append("it[").append(name).append("] = ")
appendPropertyValue(value)
append('\n')
}
level -= 1
indent().append("}\n")
}

for (property in otherProps) {
if (property is StringProperty && property.stringValue == "") {
continue
}
indent()
.append("setProperty(")
// For TestElementProperty we have to use setProperty(Property) method
// which lacks property name argument, so we generate property name argument
// only for simple properties
val usePropertyConstructor = property is TestElementProperty || property is MultiProperty
if (usePropertyConstructor) {
append(property::class.java.simpleName).append('(')
}
appendLiteral(property.name).append(", ")
appendPropertyValue(property)
if (usePropertyConstructor) {
append(')')
}
append(")\n")
}
}

private fun appendPropertyValue(property: JMeterProperty): DslPrinterTraverser = apply {
when (property) {
is BooleanProperty, is IntegerProperty -> append(property.stringValue)
is DoubleProperty -> append(property.stringValue)
is FloatProperty -> append(property.stringValue).append('f')
is LongProperty -> append(property.stringValue).append('d')
is StringProperty -> appendLiteral(property.stringValue)

is TestElementProperty -> {
val element = property.element
append(element::class.java.name).append("()").append(".apply {\n")
level += 1
appendProperties(element, canSkipTestClass = false)
level -= 1
if (sw.buffer.endsWith(".apply {\n")) {
sw.buffer.setLength(sw.buffer.length - ".apply {\n".length)
} else {
indent().append("}")
}
}

is CollectionProperty -> {
append("listOf(\n")
level += 1
for (property1 in property.iterator()) {
indent()
appendPropertyValue(property1)
append(",\n")
}
level -= 1
if (sw.buffer.endsWith("(\n")) {
sw.buffer.setLength(sw.buffer.length - 1)
append(")")
} else {
indent().append(")")
}
}

else -> append("/* unsupported property type ${property::class.java.simpleName}*/").appendLiteral(property.stringValue)
}
}

private fun append(value: String) = apply { sw.append(value) }

private fun append(value: Char) = apply { sw.append(value) }

private fun appendLiteral(literal: String): DslPrinterTraverser {
return apply {
append('"')
append(
literal.replace(SPECIAL_CHARS) {
when (it.value) {
"\"" -> "\\\""
"\n" -> "\\n"
"\r" -> "\\r"
"\t" -> "\\t"
"\b" -> "\\b"
"\\" -> "\\\\"
"$" -> "\\$"
else -> "\\${it.value}"
}
}
)
append('"')
}
}

private fun indent(): DslPrinterTraverser = apply {
repeat(level) {
sw.append(" ")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.jmeter.gui.action

import com.google.auto.service.AutoService
import org.apache.jmeter.dsl.DslPrinterTraverser
import org.apache.jmeter.gui.GuiPackage
import org.apache.jorphan.gui.GuiUtils
import org.slf4j.LoggerFactory
import java.awt.event.ActionEvent

@AutoService(Command::class)
public class CopyCodeAction : AbstractAction() {
private companion object {
private val log = LoggerFactory.getLogger(CopyCodeAction::class.java)
}
private val actionNames = setOf(ActionNames.COPY_CODE)

override fun getActionNames(): Set<String> = actionNames

override fun doAction(e: ActionEvent) {
val gui = GuiPackage.getInstance()
val selectedNodes = gui?.treeListener?.selectedNodes ?: return
val dslWriter = DslPrinterTraverser()
for (node in selectedNodes) {
val tree = gui.treeModel.getCurrentSubTree(node)
try {
tree.traverse(dslWriter)
} catch (e: Throwable) {
log.warn("Unable to copy DSL for node {}", node, e)
return
}
}
GuiUtils.copyTextToClipboard(dslWriter.toString())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package org.apache.jmeter.treebuilder

import org.apache.jmeter.testelement.TestElement
import org.apache.jmeter.testelement.TestElementSchema
import org.apache.jorphan.collections.ListedHashTree
import org.apiguardian.api.API
import kotlin.reflect.KClass
Expand All @@ -27,7 +28,7 @@ import kotlin.reflect.KClass
*
* Sample Kotlin:
*
* treeBuilder {
* testTree {
* TestPlan::class {
* OpenModelThreadGroup::class {
* name = "Thread Group"
Expand All @@ -39,7 +40,7 @@ import kotlin.reflect.KClass
*
* Sample Java:
*
* treeBuilder(b -> {
* testTree(b -> {
* b.add(TestPlan.class, tp -> {
* b.add(OpenModelThreadGroup.class, tg ->{
* name = "Thread Group"
Expand Down Expand Up @@ -150,6 +151,9 @@ public class TreeBuilder {
}

private fun runConfigurations(testElement: TestElement) {
if (testElement.getPropertyOrNull(TestElementSchema.testClass) == null) {
testElement[TestElementSchema.testClass] = testElement::class.java
}
for (actions in actionsStack) {
actions?.forEach { it(testElement) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ cookie_options=Options
cookies_stored=User-Defined Cookies
cookie_clear_controlled_by_threadgroup=Use Thread Group configuration to control cookie clearing
copy=Copy
copy_code=Copy Code
counter_config_title=Counter
counter_per_user=Track counter independently for each user
counter_reset_per_tg_iteration=Reset counter on each Thread Group Iteration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ cookie_options=Options
cookies_stored=Cookies stockés
cookie_clear_controlled_by_threadgroup=Utiliser thread group pour contrôler l'effacement des cookies
copy=Copier
copy_code=Copier Code
counter_config_title=Compteur
counter_per_user=Suivre le compteur indépendamment pour chaque unité de test
counter_reset_per_tg_iteration=Réinitialiser le compteur à chaque itération du groupe d'unités
Expand Down
Loading

0 comments on commit 7a7e946

Please sign in to comment.