blob: 8fae00c2d37037f71d792ee349b13038c94af8de [file] [log] [blame]
/*
* 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.junit;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import java.awt.Component;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.swing.SwingUtilities;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.lang3.StringUtils;
import org.apache.jmeter.config.gui.ObsoleteGui;
import org.apache.jmeter.control.gui.TestFragmentControllerGui;
import org.apache.jmeter.dsl.DslPrinterTraverser;
import org.apache.jmeter.gui.GuiComponentHolder;
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.protocol.http.control.gui.GraphQLHTTPSamplerGui;
import org.apache.jmeter.protocol.http.sampler.HTTPSamplerBaseSchema;
import org.apache.jmeter.protocol.java.config.gui.JavaConfigGui;
import org.apache.jmeter.protocol.java.control.gui.JUnitTestSamplerGui;
import org.apache.jmeter.protocol.java.control.gui.JavaTestSamplerGui;
import org.apache.jmeter.save.SaveService;
import org.apache.jmeter.testbeans.TestBean;
import org.apache.jmeter.testbeans.gui.TestBeanGUI;
import org.apache.jmeter.testelement.TestElement;
import org.apache.jmeter.testelement.property.JMeterProperty;
import org.apache.jmeter.testelement.property.PropertyIterator;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jmeter.visualizers.backend.BackendListenerGui;
import org.apache.jorphan.reflect.ClassFinder;
import org.apache.jorphan.util.JOrphanUtils;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.parallel.Isolated;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
@Isolated("changes default locale")
@TestInstance(TestInstance.Lifecycle.PER_CLASS) // maps must persist across test method executions
public class JMeterTest extends JMeterTestCase {
private static final Logger log = LoggerFactory.getLogger(JMeterTest.class);
private static Map<String, Boolean> guiTitles;
private static Map<String, Boolean> guiTags;
private static Properties nameMap;
private static final Locale TEST_LOCALE = Locale.ENGLISH;
private static final Locale DEFAULT_LOCALE = Locale.getDefault();
private static volatile boolean classPathShown = false;// Only show classpath once
@BeforeAll
public static void setLocale() {
JMeterUtils.setLocale(TEST_LOCALE);
Locale.setDefault(TEST_LOCALE);
}
// Restore the original Locale
@AfterAll
public static void resetLocale() {
JMeterUtils.setLocale(DEFAULT_LOCALE);
Locale.setDefault(DEFAULT_LOCALE);
}
/*
* Extract titles from component_reference.xml
*/
@BeforeAll
public static void createTitleSet() throws Exception {
guiTitles = new HashMap<>(90);
String compref = "../xdocs/usermanual/component_reference.xml";
try (InputStream stream = new FileInputStream(findTestFile(compref))) {
org.w3c.dom.Element body = getBodyFromXMLDocument(stream);
NodeList sections = body.getElementsByTagName("section");
for (int i = 0; i < sections.getLength(); i++) {
org.w3c.dom.Element section = (org.w3c.dom.Element) sections.item(i);
NodeList components = section.getElementsByTagName("component");
for (int j = 0; j < components.getLength(); j++) {
org.w3c.dom.Element comp = (org.w3c.dom.Element)
components.item(j);
String nm = comp.getAttribute("name");
if (!nm.equals("SSL Manager")) {// Not a true GUI component
guiTitles.put(nm.replace(' ', '_'), Boolean.FALSE);
}
}
}
}
// Add titles that don't need to be documented
guiTitles.put("Example Sampler", Boolean.FALSE);
}
/**
* @return first element named {@code body}
* @throws ParserConfigurationException when stream contains invalid XML
* @throws IOException when stream can not be read
* @throws SAXException in case of XML parsing error
*/
private static org.w3c.dom.Element getBodyFromXMLDocument(InputStream stream)
throws ParserConfigurationException, SAXException, IOException {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setIgnoringElementContentWhitespace(true);
dbf.setIgnoringComments(true);
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(new InputSource(stream));
org.w3c.dom.Element root = doc.getDocumentElement();
org.w3c.dom.Element body = (org.w3c.dom.Element) root.getElementsByTagName("body").item(0);
return body;
}
/*
* Extract titles from component_reference.xml
*/
@BeforeAll
public static void createTagSet() throws Exception {
guiTags = new HashMap<>(90);
String compref = "../xdocs/usermanual/component_reference.xml";
try (InputStream stream = new FileInputStream(findTestFile(compref))) {
org.w3c.dom.Element body = getBodyFromXMLDocument(stream);
NodeList sections = body.getElementsByTagName("section");
for (int i = 0; i < sections.getLength(); i++) {
org.w3c.dom.Element section = (org.w3c.dom.Element) sections.item(i);
NodeList components = section.getElementsByTagName("component");
for (int j = 0; j < components.getLength(); j++) {
org.w3c.dom.Element comp = (org.w3c.dom.Element)
components.item(j);
String tag = comp.getAttribute("tag");
if (!StringUtils.isEmpty(tag)){
guiTags.put(tag, Boolean.FALSE);
}
}
}
}
}
public static<T> List<T> keysWithFalseValues(Map<? extends T, Boolean> map) {
return map.entrySet().stream()
.filter(e -> !e.getValue().equals(Boolean.TRUE))
.map(Map.Entry::getKey)
.sorted()
.collect(Collectors.toList());
}
@AfterAll
public static void checkGuiSet() {
guiTitles.remove("Example Sampler");// We don't mind if this is left over
guiTitles.remove("Sample_Result_Save_Configuration");// Ditto, not a sampler
assertEquals(
"[]",
keysWithFalseValues(guiTitles).toString(),
() -> "Should not have any names left over in guiTitles map, check name of components in EN (default) Locale, "
+ "which must match name attribute of component, check java.awt.HeadlessException errors before,"
+ " we are running with '-Djava.awt.headless="
+ System.getProperty("java.awt.headless") + "'");
}
/*
* Test GUI elements - create the suite of tests
*/
static Collection<GuiComponentHolder> guiComponents() throws Throwable {
List<GuiComponentHolder> components = new ArrayList<>();
for (Object o : getObjects(JMeterGUIComponent.class)) {
JMeterGUIComponent item = (JMeterGUIComponent) o;
if (item.getClass() == TestBeanGUI.class) {
continue;
}
if (item instanceof JMeterTreeNode) {
System.out.println("o.a.j.junit.JMeterTest INFO: JMeterGUIComponent: skipping all tests " + item.getClass().getName());
continue;
}
if (item instanceof ObsoleteGui) {
continue;
}
components.add(new GuiComponentHolder(item));
}
for (Object o : getObjects(TestBean.class)) {
Class<?> c = o.getClass();
JMeterGUIComponent item = new TestBeanGUI(c);
components.add(new GuiComponentHolder(item));
}
return components;
}
/*
* Test GUI elements - run the test
*/
@ParameterizedTest
@MethodSource("guiComponents")
public void runGUITitle(GuiComponentHolder componentHolder) throws Exception {
JMeterGUIComponent guiItem = componentHolder.getComponent();
if (!guiTitles.isEmpty()) {
String title = guiItem.getDocAnchor();
boolean ct = guiTitles.containsKey(title);
if (ct) {
guiTitles.put(title, Boolean.TRUE);// So we can detect extra entries
}
String name = guiItem.getClass().getName();
if (// Is this a work in progress or an internal GUI component?
title != null && !title.isEmpty() // Will be "" for internal components
&& !title.toUpperCase(Locale.ENGLISH).contains("(ALPHA")
&& !title.toUpperCase(Locale.ENGLISH).contains("(BETA")
&& !title.toUpperCase(Locale.ENGLISH).contains("(DEPRECATED")
&& !title.matches("Example\\d+") // Skip the example samplers ...
&& !name.startsWith("org.apache.jmeter.examples."))
{// No, not a work in progress ...
String s = "component_reference.xml needs '" + title + "' anchor for " + name;
if (!ct) {
log.warn(s); // Record in log as well
}
assertTrue(ct, s);
}
}
}
/*
* Test GUI elements - run for all components
*/
@ParameterizedTest
@MethodSource("guiComponents")
public void GUIComponents1(GuiComponentHolder componentHolder) throws Exception {
JMeterGUIComponent guiItem = componentHolder.getComponent();
String name = componentHolder.toString();
if (guiItem.getClass().getName().startsWith("org.apache.jmeter.examples.")){
return;
}
if (guiItem.getClass() != TestBeanGUI.class) {
try {
String label = guiItem.getLabelResource();
assertNotNull(label, () -> "Label should not be null for " + name);
assertFalse(label.isEmpty(), () -> "Label should not be empty for " + name);
assertFalse(JMeterUtils.getResString(
label).startsWith(JMeterUtils.RES_KEY_PFX), () -> "'" + label + "' should be in resource file for " + name);
} catch (UnsupportedOperationException uoe) {
log.warn("Class has not yet implemented getLabelResource {}", name);
}
}
checkElementAlias(guiItem);
}
/*
* Test GUI elements - not run for TestBeanGui items
*/
@ParameterizedTest
@MethodSource("guiComponents")
public void GUIComponents2(GuiComponentHolder componentHolder) throws Exception {
JMeterGUIComponent guiItem = componentHolder.getComponent();
String name = guiItem.getClass().getName();
// TODO these assertions should be separate tests
TestElement el = guiItem.createTestElement();
assertNotNull(el, name + ".createTestElement should be non-null ");
assertEquals(name, el.getPropertyAsString(TestElement.GUI_CLASS), "GUI-CLASS: Failed on " + name);
assertEquals(guiItem.getName(), el.getName(), () -> "NAME: Failed on " + name);
if (StringUtils.isEmpty(el.getName())) {
fail("Name of the element must not be blank. Gui class " + name + ", element class " + el.getClass().getName());
}
assertEquals(el.getClass().getName(), el
.getPropertyAsString(TestElement.TEST_CLASS), "TEST-CLASS: Failed on " + name);
if (guiItem.getClass() != TestFragmentControllerGui.class) {
assertTrue(el.isEnabled(), "Should be enabled by default: " + name);
}
TestElement el2 = guiItem.createTestElement();
el.setName("hey, new name!:");
el.setProperty("NOT", "Shouldn't be here");
if (!(guiItem instanceof UnsharedComponent)) {
assertEquals("", el2.getPropertyAsString("NOT"), () -> "SHARED: Failed on " + name);
}
log.debug("Saving element: {}", el.getClass());
ByteArrayOutputStream bos = new ByteArrayOutputStream();
SaveService.saveElement(el, bos);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
bos.close();
el = (TestElement) SaveService.loadElement(bis);
bis.close();
assertNotNull(el, "Load element failed on: "+name);
guiItem.configure(el);
assertEquals(el.getName(), guiItem.getName(), () -> "CONFIGURE-TEST: Failed on " + name);
guiItem.modifyTestElement(el2);
assertEquals("hey, new name!:", el2.getName(), () -> "Modify Test: Failed on " + name);
}
@ParameterizedTest
@MethodSource("guiComponents")
public void propertiesShouldNotBeInitializedToNullValues(GuiComponentHolder componentHolder) {
JMeterGUIComponent guiItem = componentHolder.getComponent();
TestElement el = guiItem.createTestElement();
assertFalse(
StringUtils.isEmpty(el.getName()),
() -> "Name should be non-blank for element " + componentHolder);
PropertyIterator it = el.propertyIterator();
while (it.hasNext()) {
JMeterProperty property = it.next();
if (property.getObjectValue() == null) {
fail("Property " + property.getName() + " is initialized with NULL OBJECT value in " +
" test element " + el + " created with " + guiItem + ".createTestElement() " +
"Please refrain from that since null properties consume memory, and they will be " +
"removed when saving and loading the plan anyway");
}
if (property.getStringValue() == null) {
fail("Property " + property.getName() + " is initialized with NULL STRING value in " +
" test element " + el + " created with " + guiItem + ".createTestElement() " +
"Please refrain from that since null properties consume memory, and they will be " +
"removed when saving and loading the plan anyway");
}
}
}
@ParameterizedTest
@MethodSource("guiComponents")
public void elementShouldNotBeModifiedWithConfigureModify(GuiComponentHolder componentHolder) {
JMeterGUIComponent guiItem = componentHolder.getComponent();
TestElement expected = guiItem.createTestElement();
TestElement actual = guiItem.createTestElement();
guiItem.configure(actual);
if (!Objects.equals(expected, actual)) {
boolean breakpointForDebugging = Objects.equals(expected, actual);
String expectedStr = new DslPrinterTraverser(DslPrinterTraverser.DetailLevel.ALL).append(expected).toString();
String actualStr = new DslPrinterTraverser(DslPrinterTraverser.DetailLevel.ALL).append(actual).toString();
assertEquals(
expectedStr,
actualStr,
() -> "TestElement should not be modified by " + guiItem.getClass().getName() + ".configure(element)"
);
}
guiItem.modifyTestElement(actual);
if (guiItem.getClass() == GraphQLHTTPSamplerGui.class) {
// GraphQL sampler computes its arguments, so we don't compare them
// See org.apache.jmeter.protocol.http.config.gui.GraphQLUrlConfigGui.modifyTestElement
expected.removeProperty(HTTPSamplerBaseSchema.INSTANCE.getArguments());
actual.removeProperty(HTTPSamplerBaseSchema.INSTANCE.getArguments());
}
if (!Objects.equals(expected, actual)) {
if (guiItem.getClass() == JavaConfigGui.class || guiItem.getClass() == JavaTestSamplerGui.class) {
// TODO: JavaConfigGui modifies UI when classname combobox changes, and it causes inconsistency between the
// element state and the UI state. We ignore the discrepancy for now
return;
}
if (guiItem.getClass() == JUnitTestSamplerGui.class) {
// TODO: fix org.apache.jmeter.protocol.java.control.gui.JUnitTestSamplerGui.configure to use placeholders
return;
}
if (guiItem.getClass() == BackendListenerGui.class) {
// TODO: fix handling of default arguments in org.apache.jmeter.visualizers.backend.BackendListenerGui.actionPerformed
return;
}
boolean breakpointForDebugging = Objects.equals(expected, actual);
String expectedStr = new DslPrinterTraverser(DslPrinterTraverser.DetailLevel.ALL).append(expected).toString();
String actualStr = new DslPrinterTraverser(DslPrinterTraverser.DetailLevel.ALL).append(actual).toString();
assertEquals(
expectedStr,
actualStr,
() -> "TestElement should not be modified by " + guiItem.getClass().getName() + ".configure(element); gui.modifyTestElement(element)"
);
}
}
@ParameterizedTest
@MethodSource("guiComponents")
public void saveLoadShouldKeepElementIntact(GuiComponentHolder componentHolder) throws IOException {
JMeterGUIComponent guiItem = componentHolder.getComponent();
TestElement expected = guiItem.createTestElement();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
SaveService.saveElement(expected, bos);
byte[] serializedBytes = bos.toByteArray();
TestElement actual = (TestElement) SaveService.loadElement(new ByteArrayInputStream(serializedBytes));
compareAllProperties(expected, actual, serializedBytes);
}
private static void compareAllProperties(TestElement expected, TestElement actual, byte[] serializedBytes) {
expected.traverse(IsEnabledNormalizer.INSTANCE);
actual.traverse(IsEnabledNormalizer.INSTANCE);
String expectedStr = new DslPrinterTraverser(DslPrinterTraverser.DetailLevel.ALL).append(expected).toString();
if (!Objects.equals(expected, actual)) {
boolean breakpointForDebugging = Objects.equals(expected, actual);
assertEquals(
expectedStr,
new DslPrinterTraverser(DslPrinterTraverser.DetailLevel.ALL).append(actual).toString(),
"TestElement after 'save+load' should match the one created in GUI\n" +
"JMX is " + new String(serializedBytes, StandardCharsets.UTF_8));
fail("TestElement after 'save+load' should match the one created in GUI. " +
"DSL representation is the same, however TestElement#equals says the elements are different. " +
"DSL is " + expectedStr + "\n" +
"JMX is " + new String(serializedBytes, StandardCharsets.UTF_8));
}
assertEquals(expected.hashCode(), actual.hashCode(), "TestElement.hashCode after 'save+load' should match the one created in GUI. " +
"DSL representation is the same, however TestElement#hashCode says the elements are different. " +
"DSL is " + expectedStr + "\n" +
"JMX is " + new String(serializedBytes, StandardCharsets.UTF_8));
}
static Stream<Serializable> serializableObjects() throws Throwable {
return getObjects(Serializable.class)
.stream()
.map(Serializable.class::cast)
.filter(o -> !o.getClass().getName().endsWith("_Stub"));
}
/*
* Test serializable elements - test the object
*/
@ParameterizedTest
@MethodSource("serializableObjects")
public void runSerialTest(Serializable serObj) throws Exception {
if (!(serObj instanceof Component)) {//
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bytes);
out.writeObject(serObj);
out.close();
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes.toByteArray()));
Object readObject = in.readObject();
in.close();
assertEquals(
serObj.getClass(),
readObject.getClass(),
() -> "deserializing class: " + serObj.getClass().getName());
} catch (Exception e) {
fail("serialization of " + serObj.getClass().getName() + " failed: " + e);
}
}
}
@BeforeAll
public static void readAliases() throws Exception {
nameMap = SaveService.loadProperties();
assertNotNull(nameMap, "SaveService nameMap (saveservice.properties) should not be null");
}
private void checkElementAlias(Object item) {
String name=item.getClass().getName();
boolean contains = nameMap.values().contains(name);
if (!contains){
fail("SaveService nameMap (saveservice.properties) should contain "+name);
}
}
public static Collection<Object> getObjects(Class<?> extendsClass) throws Throwable {
String exName = extendsClass.getName();
@SuppressWarnings("deprecation")
Iterator<String> classes = ClassFinder
.findClassesThatExtend(JMeterUtils.getSearchPaths(), new Class[] { extendsClass }).iterator();
List<Object> objects = new ArrayList<>();
while (classes.hasNext()) {
String className = classes.next();
// TODO - improve this check
if (className.equals("org.apache.jmeter.gui.menu.StaticJMeterGUIComponent")) {
continue;
}
if (className.endsWith("RemoteJMeterEngineImpl")) {
continue; // Don't try to instantiate remote server
}
if (className.endsWith("RemoteSampleListenerImpl")) {
// TODO: Cannot start. travis-job-e984b3d5-f93f-4b0f-b6c0-50988a5ece9d is a loopback address.
continue;
}
try {
// Construct classes in the AWT thread, as we may have found classes, that
// assume to be constructed in the AWT thread.
SwingUtilities.invokeAndWait(() -> {
Object object = instantiateClass(className);
if (object != null) {
objects.add(object);
}
});
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof IllegalStateException && cause.getMessage().startsWith("Unable to instantiate ")) {
cause = cause.getCause();
}
if (extendsClass.equals(Serializable.class)) {
// TODO: ignore only well-known classes
System.err.println("Unable to instantiate " + className + " for service " + extendsClass + ", " + cause.toString());
} else {
throw new IllegalStateException("Unable to instantiate " + className + " for service " + extendsClass, cause);
}
}
}
if (objects.isEmpty()) {
System.out.println("No classes found that extend " + exName + ". Check the following:");
System.out.println("Search paths are:");
String[] ss = JMeterUtils.getSearchPaths();
for (String s : ss) {
System.out.println(s);
}
if (!classPathShown) {// Only dump it once
System.out.println("Class path is:");
String cp = System.getProperty("java.class.path");
String[] classPathElements = JOrphanUtils.split(cp, java.io.File.pathSeparator);
for (String classPathElement : classPathElements) {
System.out.println(classPathElement);
}
classPathShown = true;
}
}
return objects;
}
private static Object instantiateClass(String className) {
try {
Class<?> aClass = Class.forName(className);
if (aClass.isEnum() || Modifier.isAbstract(aClass.getModifiers()) || aClass.isInterface()) {
return null;
}
return aClass
.getDeclaredConstructor()
.newInstance();
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException |
ClassNotFoundException e) {
throw new IllegalStateException("Unable to instantiate " + className, e instanceof InvocationTargetException ? e.getCause() : e);
}
}
}