This is an automated email from the ASF dual-hosted git repository.
vladimirsitnikov pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/jmeter.git
The following commit(s) were added to refs/heads/master by this push:
new c1a2a0c8fd fix: treat null or blank values as empty for saved
properties
c1a2a0c8fd is described below
commit c1a2a0c8fd7840165f7c1d5f48bcd224024713dc
Author: Vladimir Sitnikov <[email protected]>
AuthorDate: Fri Jun 30 16:12:23 2023 +0300
fix: treat null or blank values as empty for saved properties
It turned out it was impossible to modify element while iterating over its
properties since ReentrantReadWriteLock is not upgradable in Java.
So this commit adds upgrading locks for writeLock paths
Fixes https://github.com/apache/jmeter/issues/6026
See
https://www.javaspecialists.eu/archive/Issue279-Upgrading-ReadWriteLock.html>Issue279-Upgrading-ReadWriteLock
---
.../jmeter/testelement/AbstractTestElement.java | 148 +++++++++------------
.../org/apache/jmeter/testelement/TestElement.kt | 59 ++++----
.../schema/BooleanPropertyDescriptor.kt | 4 +-
.../java/org/apache/jmeter/junit/JMeterTest.java | 13 +-
.../apache/jmeter/loadsave/IsEnabledNormalizer.kt | 47 +++++++
.../protocol/http/config/gui/HttpDefaultsGui.java | 18 ++-
.../protocol/http/config/gui/UrlConfigGui.java | 11 +-
.../http/sampler/HttpSamplerPrintDslTest.kt | 37 ++++++
8 files changed, 198 insertions(+), 139 deletions(-)
diff --git
a/src/core/src/main/java/org/apache/jmeter/testelement/AbstractTestElement.java
b/src/core/src/main/java/org/apache/jmeter/testelement/AbstractTestElement.java
index 0486721475..95c65cc2ab 100644
---
a/src/core/src/main/java/org/apache/jmeter/testelement/AbstractTestElement.java
+++
b/src/core/src/main/java/org/apache/jmeter/testelement/AbstractTestElement.java
@@ -27,7 +27,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.jmeter.engine.util.NoThreadClone;
@@ -66,7 +65,7 @@ public abstract class AbstractTestElement implements
TestElement, Serializable,
* Note: acquiring locks (read or write) allocates memory, and accesses
{@link ThreadLocal}, so it can be expensive
* in both CPU and allocation terms.
*/
- private transient final ReadWriteLock lock =
+ private transient final ReentrantReadWriteLock lock =
this instanceof NoThreadClone
// Note: thread groups are cloned for every thread,
however, JMeterContext contains a reference
// to a non-cloned ThreadGroup instance.
@@ -76,6 +75,39 @@ public abstract class AbstractTestElement implements
TestElement, Serializable,
? new ReentrantReadWriteLock()
: null;
+ private transient final ResourceLock readUnlock = lock == null ? null :
lock.readLock()::unlock;
+
+ /**
+ * Downgrades input lock from write to read on close.
+ * It enables using {@link #writeLock()} in try-with-resources to upgrade
the lock.
+ */
+ interface ResourceLock extends AutoCloseable {
+ @Override
+ void close();
+
+ ResourceLock NO_LOCK = () -> { };
+
+ /**
+ * Upgrades lock to write if needed, and returns a callback for
releasing the write lock and restoring read locks.
+ * @see <a
href="https://www.javaspecialists.eu/archive/Issue279-Upgrading-ReadWriteLock.html>Issue279-Upgrading-ReadWriteLock</a>
+ */
+ static ResourceLock releaseWrite(ReentrantReadWriteLock lock) {
+ int readHoldCount = lock.getWriteHoldCount() == 0 ?
lock.getReadHoldCount() : 0;
+ // We need to unlock read locks since ReentrantReadWriteLock does
not support lock upgrade
+ for (int i = 0; i < readHoldCount; i++) {
+ lock.readLock().unlock();
+ }
+ lock.writeLock().lock();
+ // Restore locks on close
+ return () -> {
+ for (int i = 0; i < readHoldCount; i++) {
+ lock.readLock().lock();
+ }
+ lock.writeLock().unlock();
+ };
+ }
+ }
+
/**
* When the element is shared between threads, then {@link #lock} protects
the access,
* however, when element in not shared, then adds overhead as every lock
and unlock allocates memory.
@@ -146,40 +178,33 @@ public abstract class AbstractTestElement implements
TestElement, Serializable,
*/
@Override
public void clear() {
- writeLock();
- try {
+ try (ResourceLock ignored = writeLock()) {
propMap.clear();
Map<String, JMeterProperty> propMapConcurrent =
this.propMapConcurrent;
if (propMapConcurrent != null) {
propMapConcurrent.clear();
}
- } finally {
- writeUnlock();
- }
- }
-
- private void writeLock() {
- if (lock != null) {
- lock.writeLock().lock();
}
}
- private void writeUnlock() {
- if (lock != null) {
- lock.writeLock().unlock();
- }
- }
-
- private void readLock() {
- if (lock != null) {
- lock.readLock().lock();
+ /**
+ * Acquires write lock, and upgrades the current read lock to write lock
if needed.
+ * Note: this should be used in try-with-resources for automatic cleanup.
+ * @return ResourceLock to close when the write lock is no longer needed
+ */
+ private ResourceLock writeLock() {
+ if (lock == null) {
+ return ResourceLock.NO_LOCK;
}
+ return ResourceLock.releaseWrite(lock);
}
- private void readUnlock() {
- if (lock != null) {
- lock.readLock().unlock();
+ private ResourceLock readLock() {
+ if (lock == null) {
+ return ResourceLock.NO_LOCK;
}
+ lock.readLock().lock();
+ return readUnlock;
}
/**
@@ -197,15 +222,12 @@ public abstract class AbstractTestElement implements
TestElement, Serializable,
*/
@Override
public void removeProperty(String key) {
- writeLock();
- try {
+ try (ResourceLock ignored = writeLock()) {
propMap.remove(key);
Map<String, JMeterProperty> propMapConcurrent =
this.propMapConcurrent;
if (propMapConcurrent != null) {
propMapConcurrent.remove(key);
}
- } finally {
- writeUnlock();
}
}
@@ -215,11 +237,8 @@ public abstract class AbstractTestElement implements
TestElement, Serializable,
@Override
public boolean equals(Object o) {
if (o instanceof AbstractTestElement) {
- readLock();
- try {
+ try (ResourceLock ignored = readLock()) {
return ((AbstractTestElement) o).propMap.equals(propMap);
- } finally {
- readUnlock();
}
} else {
return false;
@@ -307,16 +326,13 @@ public abstract class AbstractTestElement implements
TestElement, Serializable,
@Override
public void traverse(TestElementTraverser traverser) {
- readLock();
- try {
+ try (ResourceLock ignored = readLock()) {
PropertyIterator iter = propertyIterator();
traverser.startTestElement(this);
while (iter.hasNext()) {
traverseProperty(traverser, iter.next());
}
traverser.endTestElement(this);
- } finally {
- readUnlock();
}
}
@@ -417,15 +433,12 @@ public abstract class AbstractTestElement implements
TestElement, Serializable,
JMeterProperty prop = getProperty(property.getName());
if (prop instanceof NullProperty || (prop instanceof StringProperty &&
prop.getStringValue().isEmpty())) {
- writeLock();
- try {
+ try (ResourceLock ignored = writeLock()) {
propMap.put(property.getName(), propertyToPut);
Map<String, JMeterProperty> propMapConcurrent =
this.propMapConcurrent;
if (propMapConcurrent != null) {
propMapConcurrent.put(property.getName(), propertyToPut);
}
- } finally {
- writeUnlock();
}
} else {
prop.mergeIn(propertyToPut);
@@ -445,13 +458,10 @@ public abstract class AbstractTestElement implements
TestElement, Serializable,
* @param property {@link JMeterProperty}
*/
protected void clearTemporary(JMeterProperty property) {
- writeLock();
- try {
+ try (ResourceLock ignored = writeLock()) {
if (temporaryProperties != null) {
temporaryProperties.remove(property);
}
- } finally {
- writeUnlock();
}
}
@@ -480,8 +490,7 @@ public abstract class AbstractTestElement implements
TestElement, Serializable,
getProperty(property.getName()).setObjectValue(property.getObjectValue());
}
} else {
- writeLock();
- try {
+ try (ResourceLock ignored = writeLock()) {
if (property instanceof StringProperty &&
property.getStringValue() == null) {
// Avoid storing properties with null values since they
will be skipped anyway when saving
// the test plan
@@ -493,8 +502,6 @@ public abstract class AbstractTestElement implements
TestElement, Serializable,
propMapConcurrent.put(property.getName(), property);
}
}
- } finally {
- writeUnlock();
}
}
}
@@ -602,15 +609,12 @@ public abstract class AbstractTestElement implements
TestElement, Serializable,
@Override
public PropertyIterator propertyIterator() {
// Note: can't use ConcurrentMap here as it would return elements in
unpredictable order
- readLock();
- try {
+ try (ResourceLock ignored = readLock()) {
if (propMap.isEmpty()) {
return PropertyIteratorImpl.EMPTY_ITERATOR;
}
// TODO: copy the contents of the iterator to avoid
ConcurrentModificationException?
return new PropertyIteratorImpl(this, propMap.values());
- } finally {
- readUnlock();
}
}
@@ -619,15 +623,12 @@ public abstract class AbstractTestElement implements
TestElement, Serializable,
* @param element {@link TestElement}
*/
protected void mergeIn(TestElement element) {
- writeLock();
- try {
+ try (ResourceLock ignored = writeLock()) {
PropertyIterator iter = element.propertyIterator();
while (iter.hasNext()) {
JMeterProperty prop = iter.next();
addProperty(prop, false);
}
- } finally {
- writeUnlock();
}
}
@@ -647,8 +648,7 @@ public abstract class AbstractTestElement implements
TestElement, Serializable,
*/
@Override
public void setRunningVersion(boolean runningVersion) {
- writeLock();
- try {
+ try (ResourceLock ignored = writeLock()) {
this.runningVersion = runningVersion;
PropertyIterator iter = propertyIterator();
Map<String, JMeterProperty> propMapConcurrent =
this.propMapConcurrent;
@@ -659,8 +659,6 @@ public abstract class AbstractTestElement implements
TestElement, Serializable,
propMapConcurrent.put(property.getName(), property);
}
}
- } finally {
- writeUnlock();
}
}
@@ -674,8 +672,7 @@ public abstract class AbstractTestElement implements
TestElement, Serializable,
// See https://github.com/apache/jmeter/issues/5875
return;
}
- writeLock();
- try {
+ try (ResourceLock ignored = writeLock()) {
Iterator<Map.Entry<String, JMeterProperty>> iter =
propMap.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<String, JMeterProperty> entry = iter.next();
@@ -688,8 +685,6 @@ public abstract class AbstractTestElement implements
TestElement, Serializable,
}
}
emptyTemporary();
- } finally {
- writeUnlock();
}
}
@@ -697,13 +692,10 @@ public abstract class AbstractTestElement implements
TestElement, Serializable,
* Clears temporaryProperties
*/
protected void emptyTemporary() {
- writeLock();
- try {
+ try (ResourceLock ignored = writeLock()) {
if (temporaryProperties != null) {
temporaryProperties.clear();
}
- } finally {
- writeUnlock();
}
}
@@ -712,11 +704,8 @@ public abstract class AbstractTestElement implements
TestElement, Serializable,
*/
@Override
public boolean isTemporary(JMeterProperty property) {
- readLock();
- try {
+ try (ResourceLock ignored = readLock()) {
return temporaryProperties != null &&
temporaryProperties.contains(property);
- } finally {
- readUnlock();
}
}
@@ -725,8 +714,7 @@ public abstract class AbstractTestElement implements
TestElement, Serializable,
*/
@Override
public void setTemporary(JMeterProperty property) {
- writeLock();
- try {
+ try (ResourceLock ignored = writeLock()) {
if (temporaryProperties == null) {
LinkedHashSet<JMeterProperty> set = new LinkedHashSet<>();
temporaryProperties = lock != null ? set :
Collections.synchronizedSet(set) ;
@@ -737,8 +725,6 @@ public abstract class AbstractTestElement implements
TestElement, Serializable,
setTemporary(jMeterProperty);
}
}
- } finally {
- writeUnlock();
}
}
@@ -833,16 +819,13 @@ public abstract class AbstractTestElement implements
TestElement, Serializable,
@Override
public List<String> getSearchableTokens() {
List<String> result = new ArrayList<>(25);
- readLock();
- try {
+ try (ResourceLock ignored = readLock()) {
PropertyIterator iterator = propertyIterator();
while(iterator.hasNext()) {
JMeterProperty jMeterProperty = iterator.next();
result.add(jMeterProperty.getName());
result.add(jMeterProperty.getStringValue());
}
- } finally {
- readUnlock();
}
return result;
}
@@ -853,8 +836,7 @@ public abstract class AbstractTestElement implements
TestElement, Serializable,
* @param propertyNames Set of names of properties to extract
*/
protected final void addPropertiesValues(List<? super String> result,
Set<String> propertyNames) {
- readLock();
- try {
+ try (ResourceLock ignored = readLock()) {
PropertyIterator iterator = propertyIterator();
while (iterator.hasNext()) {
JMeterProperty jMeterProperty = iterator.next();
@@ -862,8 +844,6 @@ public abstract class AbstractTestElement implements
TestElement, Serializable,
result.add(jMeterProperty.getStringValue());
}
}
- } finally {
- readUnlock();
}
}
}
diff --git
a/src/core/src/main/kotlin/org/apache/jmeter/testelement/TestElement.kt
b/src/core/src/main/kotlin/org/apache/jmeter/testelement/TestElement.kt
index 9de8162814..d6cb9ec99f 100644
--- a/src/core/src/main/kotlin/org/apache/jmeter/testelement/TestElement.kt
+++ b/src/core/src/main/kotlin/org/apache/jmeter/testelement/TestElement.kt
@@ -21,9 +21,7 @@ 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.NullProperty
import org.apache.jmeter.testelement.property.PropertyIterator
import org.apache.jmeter.testelement.property.StringProperty
@@ -269,7 +267,7 @@ public interface TestElement : Cloneable {
getPropertyOrNull(property.name)
/**
- * Retrieve property name as string.
+ * Retrieve property as string, or default value, or empty string if the
property is unset.
* @since 5.6
*/
@JMeterPropertySchemaUnchecked
@@ -278,7 +276,7 @@ public interface TestElement : Cloneable {
getPropertyOrNull(property)?.stringValue ?: property.defaultValue ?: ""
/**
- * Read property value as string, and return default value if the property
is unset.
+ * Read property value as string, or default value, or empty string if the
property is unset.
* @since 5.6
*/
@JMeterPropertySchemaUnchecked
@@ -287,19 +285,19 @@ public interface TestElement : Cloneable {
getPropertyOrNull(property)?.stringValue ?:
property.defaultValueAsString ?: ""
/**
- * Set property as string, or remove it if the given value matches the
default one for the property.
+ * Set property as string, or remove it if the given value is `null` or
empty.
* @since 5.6
*/
@JMeterPropertySchemaUnchecked
@API(status = API.Status.EXPERIMENTAL, since = "5.6")
public operator fun set(property: PropertyDescriptor<*, *>, value:
String?) {
- removeOrSet(value.isNullOrEmpty() || property.defaultValueAsString ==
value, property.name) {
+ removeOrSet(value.isNullOrEmpty(), property.name) {
StringProperty(it, value)
}
}
/**
- * Retrieve boolean property value.
+ * Retrieve boolean property value, or default value, or `false` if the
property is unset.
* @since 5.6
*/
@JMeterPropertySchemaUnchecked
@@ -308,14 +306,14 @@ public interface TestElement : Cloneable {
getPropertyOrNull(property)?.booleanValue ?: property.defaultValue ?:
false
/**
- * Set property as boolean, or remove it if the given value matches the
default one for the property.
+ * Set property as boolean, or remove it if the given value is null.
* @since 5.6
*/
@JMeterPropertySchemaUnchecked
@API(status = API.Status.EXPERIMENTAL, since = "5.6")
- public operator fun set(property: BooleanPropertyDescriptor<*>, value:
Boolean) {
- removeOrSet(property.defaultValue == value, property.name) {
- BooleanProperty(it, value)
+ public operator fun set(property: BooleanPropertyDescriptor<*>, value:
Boolean?) {
+ removeOrSet(value == null, property.name) {
+ BooleanProperty(it, value!!)
}
}
@@ -329,19 +327,17 @@ public interface TestElement : Cloneable {
getPropertyOrNull(property)?.intValue ?: property.defaultValue ?: 0
/**
- * Set property as integer, or remove it if the given value matches the
default one for the property.
+ * Set property as integer.
* @since 5.6
*/
@JMeterPropertySchemaUnchecked
@API(status = API.Status.EXPERIMENTAL, since = "5.6")
public operator fun set(property: IntegerPropertyDescriptor<*>, value:
Int) {
- removeOrSet(property.defaultValue == value, property.name) {
- IntegerProperty(it, value)
- }
+ setProperty(property.name, value)
}
/**
- * Retrieve long property value.
+ * Retrieve long property value, or default value, or `0` if the property
is unset.
* @since 5.6
*/
@JMeterPropertySchemaUnchecked
@@ -350,19 +346,17 @@ public interface TestElement : Cloneable {
getPropertyOrNull(property)?.longValue ?: property.defaultValue ?: 0
/**
- * Set property as long, or remove it if the given value matches the
default one for the property.
+ * Set property as long.
* @since 5.6
*/
@JMeterPropertySchemaUnchecked
@API(status = API.Status.EXPERIMENTAL, since = "5.6")
public operator fun set(property: LongPropertyDescriptor<*>, value: Long) {
- removeOrSet(property.defaultValue == value, property.name) {
- LongProperty(it, value)
- }
+ setProperty(property.name, value)
}
/**
- * Retrieve float property value.
+ * Retrieve float property value, or default value, or `0.0f` if the
property is unset.
* @since 5.6
*/
@JMeterPropertySchemaUnchecked
@@ -371,19 +365,17 @@ public interface TestElement : Cloneable {
getPropertyOrNull(property)?.floatValue ?: property.defaultValue ?:
0.0f
/**
- * Set property as float, or remove it if the given value matches the
default one for the property.
+ * Set property as float.
* @since 5.6
*/
@JMeterPropertySchemaUnchecked
@API(status = API.Status.EXPERIMENTAL, since = "5.6")
public operator fun set(property: FloatPropertyDescriptor<*>, value:
Float) {
- removeOrSet(property.defaultValue == value, property.name) {
- FloatProperty(it, value)
- }
+ setProperty(FloatProperty(property.name, value))
}
/**
- * Retrieve double property value.
+ * Retrieve double property value, or default value, or `0.0` if the
property is unset.
* @since 5.6
*/
@JMeterPropertySchemaUnchecked
@@ -392,19 +384,17 @@ public interface TestElement : Cloneable {
getPropertyOrNull(property)?.doubleValue ?: property.defaultValue ?:
0.0
/**
- * Set property as double, or remove it if the given value matches the
default one for the property.
+ * Set property as double.
* @since 5.6
*/
@JMeterPropertySchemaUnchecked
@API(status = API.Status.EXPERIMENTAL, since = "5.6")
public operator fun set(property: DoublePropertyDescriptor<*>, value:
Double) {
- removeOrSet(property.defaultValue == value, property.name) {
- DoubleProperty(it, value)
- }
+ setProperty(DoubleProperty(property.name, value))
}
/**
- * Retrieve [Class] property value, or throw [NoSuchElementException] in
case the property is unset.
+ * Retrieve [Class] property value, or default value, or throw
[NoSuchElementException] in case the property is unset.
* @throws NoSuchElementException if the property is unset
* @since 5.6
*/
@@ -414,7 +404,7 @@ public interface TestElement : Cloneable {
getOrNull(property) ?: throw NoSuchElementException("Property
${property.name} is unset for element $this")
/**
- * Retrieve [Class] property value, or return `null` in case the property
is unset.
+ * Retrieve [Class] property value, or default value, or `null` in case
the property is unset.
* @since 5.6
*/
@JMeterPropertySchemaUnchecked
@@ -426,14 +416,14 @@ public interface TestElement : Cloneable {
} ?: property.defaultValue
/**
- * Set property as [Class], or remove it if the given value matches the
default one for the property.
+ * Set property as [Class], or remove it if the given value is `null`.
* The value is set as [StringProperty] with the name of the given class.
* @since 5.6
*/
@JMeterPropertySchemaUnchecked
@API(status = API.Status.EXPERIMENTAL, since = "5.6")
public operator fun <ValueClass : Any> set(property:
ClassPropertyDescriptor<*, ValueClass>, value: Class<out ValueClass>?) {
- removeOrSet(property.defaultValue == value, property.name) {
+ removeOrSet(value == null, property.name) {
StringProperty(it, value!!.name)
}
}
@@ -583,7 +573,6 @@ public interface TestElement : Cloneable {
imports = ["org.apache.jmeter.threads.JMeterContextService"]
)
)
- @get:API(status = API.Status.DEPRECATED, since = "5.6")
@set:API(status = API.Status.DEPRECATED, since = "5.6")
public var threadName: String
diff --git
a/src/core/src/main/kotlin/org/apache/jmeter/testelement/schema/BooleanPropertyDescriptor.kt
b/src/core/src/main/kotlin/org/apache/jmeter/testelement/schema/BooleanPropertyDescriptor.kt
index d21566d141..0553e20d66 100644
---
a/src/core/src/main/kotlin/org/apache/jmeter/testelement/schema/BooleanPropertyDescriptor.kt
+++
b/src/core/src/main/kotlin/org/apache/jmeter/testelement/schema/BooleanPropertyDescriptor.kt
@@ -40,7 +40,7 @@ public data class BooleanPropertyDescriptor<in Schema :
BaseTestElementSchema>(
public operator fun get(target: TestElement): Boolean =
target[this]
- public operator fun set(target: TestElement, value: Boolean) {
+ public operator fun set(target: TestElement, value: Boolean?) {
target[this] = value
}
@@ -52,7 +52,7 @@ public data class BooleanPropertyDescriptor<in Schema :
BaseTestElementSchema>(
@JvmSynthetic
@Suppress("NOTHING_TO_INLINE")
- public inline operator fun setValue(testElement: TestElement, property:
KProperty<*>, value: Boolean) {
+ public inline operator fun setValue(testElement: TestElement, property:
KProperty<*>, value: Boolean?) {
testElement[this] = value
}
}
diff --git
a/src/dist-check/src/test/java/org/apache/jmeter/junit/JMeterTest.java
b/src/dist-check/src/test/java/org/apache/jmeter/junit/JMeterTest.java
index f7463cbcd9..3ed11e88c1 100644
--- a/src/dist-check/src/test/java/org/apache/jmeter/junit/JMeterTest.java
+++ b/src/dist-check/src/test/java/org/apache/jmeter/junit/JMeterTest.java
@@ -51,6 +51,7 @@ import org.apache.jmeter.dsl.DslPrinterTraverser;
import org.apache.jmeter.gui.JMeterGUIComponent;
import org.apache.jmeter.gui.UnsharedComponent;
import org.apache.jmeter.gui.tree.JMeterTreeNode;
+import org.apache.jmeter.loadsave.IsEnabledNormalizer;
import org.apache.jmeter.save.SaveService;
import org.apache.jmeter.testbeans.TestBean;
import org.apache.jmeter.testbeans.gui.TestBeanGUI;
@@ -429,16 +430,8 @@ public class JMeterTest extends JMeterTestCaseJUnit
implements Describable {
}
private static void compareAllProperties(TestElement expected, TestElement
actual, byte[] serializedBytes) {
- // JMeter restores "enabled" as StringProperty, see
- //
org.apache.jmeter.save.converters.ConversionHelp.restoreSpecialProperties
- // So we normalize it back to BooleanProperty
- JMeterProperty expEnabled =
expected.getPropertyOrNull(expected.getSchema().getEnabled());
- if (expEnabled != null && (expEnabled.getStringValue().equals("true")
|| expEnabled.getStringValue().equals("false"))) {
- JMeterProperty actEnabled =
actual.getPropertyOrNull(actual.getSchema().getEnabled());
- if (actEnabled != null &&
actEnabled.getStringValue().equals(expEnabled.getStringValue())) {
- actual.setProperty(expEnabled);
- }
- }
+ expected.traverse(IsEnabledNormalizer.INSTANCE);
+ actual.traverse(IsEnabledNormalizer.INSTANCE);
String expectedStr = new
DslPrinterTraverser(DslPrinterTraverser.DetailLevel.ALL).append(expected).toString();
if (!Objects.equals(expected, actual)) {
diff --git
a/src/dist-check/src/test/kotlin/org/apache/jmeter/loadsave/IsEnabledNormalizer.kt
b/src/dist-check/src/test/kotlin/org/apache/jmeter/loadsave/IsEnabledNormalizer.kt
new file mode 100644
index 0000000000..3521a6fa81
--- /dev/null
+++
b/src/dist-check/src/test/kotlin/org/apache/jmeter/loadsave/IsEnabledNormalizer.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.loadsave
+
+import org.apache.jmeter.testelement.TestElement
+import org.apache.jmeter.testelement.TestElementTraverser
+import org.apache.jmeter.testelement.property.JMeterProperty
+import org.apache.jmeter.testelement.property.StringProperty
+
+/**
+ * Normalize "enabled" property to boolean if the property is set.
+ * JMeter loads "enabled" as [StringProperty], see
+ * [org.apache.jmeter.save.converters.ConversionHelp.restoreSpecialProperties],
+ * so we need to normalize it back if we want comparing elements
+ */
+object IsEnabledNormalizer : TestElementTraverser {
+ override fun startTestElement(el: TestElement) {
+ el.getPropertyOrNull(el.schema.enabled)
+ ?.stringValue
+ ?.toBooleanStrictOrNull()
+ ?.let { el.isEnabled = it }
+ }
+
+ override fun endTestElement(el: TestElement) {
+ }
+
+ override fun startProperty(key: JMeterProperty) {
+ }
+
+ override fun endProperty(key: JMeterProperty) {
+ }
+}
diff --git
a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/HttpDefaultsGui.java
b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/HttpDefaultsGui.java
index e0672815ce..8d8dd64b32 100644
---
a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/HttpDefaultsGui.java
+++
b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/HttpDefaultsGui.java
@@ -94,6 +94,13 @@ public class HttpDefaultsGui extends AbstractConfigGui {
return config;
}
+ /**
+ * Treat unset checkbox as empty, so "unset" checkboxes do not override
values in HTTP Request Samplers.
+ */
+ private static Boolean nullIfUnset(JCheckBox checkBox) {
+ return checkBox.isSelected() ? true : null;
+ }
+
/**
* Modifies a given TestElement to mirror the data in the gui components.
*
@@ -107,15 +114,16 @@ public class HttpDefaultsGui extends AbstractConfigGui {
cfg.addConfigElement(el);
super.configureTestElement(config);
HTTPSamplerBaseSchema.INSTANCE httpSchema =
HTTPSamplerBaseSchema.INSTANCE;
- config.set(httpSchema.getRetrieveEmbeddedResources(),
retrieveEmbeddedResources.isSelected());
+ config.set(httpSchema.getRetrieveEmbeddedResources(),
nullIfUnset(retrieveEmbeddedResources));
enableConcurrentDwn(retrieveEmbeddedResources.isSelected());
- config.set(httpSchema.getConcurrentDownload(),
retrieveEmbeddedResources.isSelected());
- if (!StringUtils.isEmpty(concurrentPool.getText())) {
+ if (concurrentDwn.isSelected()) {
+ config.set(httpSchema.getConcurrentDownload(), true);
config.set(httpSchema.getConcurrentDownloadPoolSize(),
concurrentPool.getText());
} else {
+ config.removeProperty(httpSchema.getConcurrentDownload());
config.removeProperty(httpSchema.getConcurrentDownloadPoolSize());
}
- config.set(httpSchema.getStoreAsMD5(), useMD5.isSelected());
+ config.set(httpSchema.getStoreAsMD5(), nullIfUnset(useMD5));
config.set(httpSchema.getEmbeddedUrlAllowRegex(),
embeddedAllowRE.getText());
config.set(httpSchema.getEmbeddedUrlExcludeRegex(),
embeddedExcludeRE.getText());
@@ -171,7 +179,7 @@ public class HttpDefaultsGui extends AbstractConfigGui {
HTTPSamplerBaseSchema httpSchema = HTTPSamplerBaseSchema.INSTANCE;
retrieveEmbeddedResources.setSelected(samplerBase.get(httpSchema.getRetrieveEmbeddedResources()));
concurrentDwn.setSelected(samplerBase.get(httpSchema.getConcurrentDownload()));
-
concurrentPool.setText(samplerBase.getString(httpSchema.getConcurrentDownloadPoolSize()));
+
concurrentPool.setText(samplerBase.getPropertyAsString(httpSchema.getConcurrentDownloadPoolSize().getName(),
""));
useMD5.setSelected(samplerBase.get(httpSchema.getStoreAsMD5()));
embeddedAllowRE.setText(samplerBase.get(httpSchema.getEmbeddedUrlAllowRegex()));
embeddedExcludeRE.setText(samplerBase.get(httpSchema.getEmbeddedUrlExcludeRegex()));
diff --git
a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/UrlConfigGui.java
b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/UrlConfigGui.java
index ba451fc911..39062621fa 100644
---
a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/UrlConfigGui.java
+++
b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/UrlConfigGui.java
@@ -214,7 +214,9 @@ public class UrlConfigGui extends JPanel implements
ChangeListener {
filesPanel.modifyTestElement(element);
}
HTTPSamplerBaseSchema.INSTANCE httpSchema =
HTTPSamplerBaseSchema.INSTANCE;
- element.set(httpSchema.getPostBodyRaw(), useRaw);
+ // Treat "unset" checkbox as "property removal" for HTTP Request
Defaults component
+ // Regular sampler should save both true and false values
+ element.set(httpSchema.getPostBodyRaw(), useRaw ? Boolean.TRUE :
(notConfigOnly ? false : null));
element.set(httpSchema.getArguments(), args);
element.set(httpSchema.getDomain(), domain.getText());
element.set(httpSchema.getPort(), port.getText());
@@ -300,8 +302,11 @@ public class UrlConfigGui extends JPanel implements
ChangeListener {
} else {
port.setText(portString);
}
- protocol.setText(el.getString(httpSchema.getProtocol()));
- contentEncoding.setText(el.getString(httpSchema.getContentEncoding()));
+ // We explicitly
+ String protocol =
el.getPropertyAsString(httpSchema.getProtocol().getName(), "");
+ this.protocol.setText(protocol);
+ String encoding =
el.getPropertyAsString(httpSchema.getContentEncoding().getName(), "");
+ contentEncoding.setText(encoding);
path.setText(el.getString(httpSchema.getPath()));
if (notConfigOnly){
method.setText(el.getString(httpSchema.getMethod()));
diff --git
a/src/protocol/http/src/test/kotlin/org/apache/jmeter/protocol/http/sampler/HttpSamplerPrintDslTest.kt
b/src/protocol/http/src/test/kotlin/org/apache/jmeter/protocol/http/sampler/HttpSamplerPrintDslTest.kt
index 2baf053e04..d9348448f9 100644
---
a/src/protocol/http/src/test/kotlin/org/apache/jmeter/protocol/http/sampler/HttpSamplerPrintDslTest.kt
+++
b/src/protocol/http/src/test/kotlin/org/apache/jmeter/protocol/http/sampler/HttpSamplerPrintDslTest.kt
@@ -21,6 +21,8 @@ import org.apache.jmeter.dsl.DslPrinterTraverser
import org.apache.jmeter.junit.JMeterTestCase
import org.apache.jmeter.protocol.http.control.gui.HttpTestSampleGui
import org.apache.jmeter.testelement.TestElement
+import org.apache.jmeter.testelement.TestElementTraverser
+import org.apache.jmeter.testelement.property.JMeterProperty
import org.apache.jmeter.treebuilder.dsl.testTree
import org.apache.jmeter.util.JMeterUtils
import org.junit.jupiter.api.AfterAll
@@ -50,6 +52,38 @@ class HttpSamplerPrintDslTest : JMeterTestCase() {
}
}
+ object RemoveDefaultValues : TestElementTraverser {
+ private val elements = mutableListOf<TestElement>()
+ private val defaultProps = mutableListOf<MutableList<String>>()
+
+ override fun startTestElement(el: TestElement) {
+ elements += el
+ defaultProps += mutableListOf<String>()
+ }
+
+ override fun endTestElement(el: TestElement) {
+ val element = elements.removeLast()
+ for (propertyName in defaultProps.removeLast()) {
+ element.removeProperty(propertyName)
+ }
+ }
+
+ override fun startProperty(key: JMeterProperty) {
+ val element = elements.last()
+ val defaultProp = defaultProps.last()
+ element.schema.properties[key.name]?.let {
+ if (key.stringValue == it.defaultValueAsString) {
+ // We can't remove property here as it would break ongoing
iterator
+ // So we remove properties in endTestElement instead
+ defaultProp += key.name
+ }
+ }
+ }
+
+ override fun endProperty(key: JMeterProperty) {
+ }
+ }
+
@Test
fun `http sampler created with UI`() {
val ui = HttpTestSampleGui()
@@ -106,10 +140,13 @@ class HttpSamplerPrintDslTest : JMeterTestCase() {
}
}.keys.first() as TestElement
+ createdWithUi.traverse(RemoveDefaultValues)
+
// We compare elements manually, and call assertEquals(toString,
toString) so
// the test output looks better (diff in IDE) in case of the failure
// If we use just assertEquals(createdWithUi, createdWithDsl), then
there will be no "diff in IDE"
if (createdWithUi != createdWithDsl) {
+ val abc = createdWithDsl.equals(createdWithDsl)
assertEquals(
DslPrinterTraverser(DslPrinterTraverser.DetailLevel.ALL).append(createdWithUi).toString(),
DslPrinterTraverser(DslPrinterTraverser.DetailLevel.ALL).append(createdWithDsl).toString()