feat(接口测试): JMeter格式导入支持插件
--story=1015890 --user=陈建星 接口定义-导入(Postman、JMX、HAR、MeterSphere)& 导出(MeterSphere) https://www.tapd.cn/55049933/s/1578307
This commit is contained in:
parent
3f8c1da5e1
commit
a8833729e5
|
@ -8,12 +8,35 @@ import org.pf4j.ExtensionPoint;
|
|||
/**
|
||||
* @author jianxing
|
||||
* @createTime 2021-10-30 10:07
|
||||
* 将 MsTestElement 具体实现类转换为 HashTree
|
||||
* 将 HashTree 转换为 MsTestElement
|
||||
* 接口导入 jmx 格式时,解析扩展的 HashTree
|
||||
* 要支持导入,插件需要做以下修改:
|
||||
* 1. 编写 com.thoughtworks.xstream.converters.Converter 的实现类,以实现 xstream 的 xml 反序列化解析
|
||||
* 实现类示例:
|
||||
* public class TCPXStreamConverter extends TestElementConverter {
|
||||
* public TCPXStreamConverter(Mapper mapper) {
|
||||
* super(mapper);
|
||||
* }
|
||||
* @Override
|
||||
* public boolean canConvert(Class clazz) {
|
||||
* return TCPSampler.class.isAssignableFrom(clazz);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* 2. 在插件的 resource 下新建 META-INF/services/com.thoughtworks.xstream.converters.Converter 文件
|
||||
* 并添加实现类的全限定名,已便于SPI的服务发现
|
||||
*
|
||||
* 3. 在插件的 resource 下新建 jmeter_element_alias.properties 配置文件,用于配置元素的别名
|
||||
* 文件内容示例(如果是jmeter官方支持的组件,可以省略这步):
|
||||
* TCPSampler=org.apache.jmeter.protocol.tcp.sampler.TCPSampler
|
||||
* TCPSamplerGui=org.apache.jmeter.protocol.tcp.control.gui.TCPSamplerGui
|
||||
*
|
||||
* 4. 编写 MsElementConverter 的实现类,将 HashTree 转换为 MsTestElement
|
||||
*/
|
||||
public interface MsElementConverter<T extends TestElement> extends ExtensionPoint {
|
||||
|
||||
/**
|
||||
* 将 MsTestElement 具体实现类转换为 HashTree
|
||||
* 将 HashTree 转换为 MsTestElement
|
||||
*/
|
||||
void toMsElement(AbstractMsTestElement parent, T element, HashTree hashTree);
|
||||
}
|
||||
|
|
|
@ -11,7 +11,10 @@ public class MsPluginManager extends DefaultPluginManager {
|
|||
@Override
|
||||
protected ExtensionFinder createExtensionFinder() {
|
||||
DefaultExtensionFinder extensionFinder = (DefaultExtensionFinder) super.createExtensionFinder();
|
||||
// 添加 jdbc 驱动支持
|
||||
extensionFinder.add(new JdbcDriverServiceProviderExtensionFinder(this));
|
||||
// 添加 SPI 支持
|
||||
extensionFinder.addServiceProviderExtensionFinder();
|
||||
return extensionFinder;
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import io.metersphere.api.dto.definition.ApiTestCaseDTO;
|
|||
import io.metersphere.api.dto.request.ImportRequest;
|
||||
import io.metersphere.api.dto.request.http.MsHTTPElement;
|
||||
import io.metersphere.api.parser.ApiDefinitionImportParser;
|
||||
import io.metersphere.api.parser.jmeter.xstream.MsSaveService;
|
||||
import io.metersphere.api.parser.ms.MsTestElementParser;
|
||||
import io.metersphere.api.utils.ApiDefinitionImportUtils;
|
||||
import io.metersphere.plugin.api.spi.AbstractMsProtocolTestElement;
|
||||
|
@ -21,7 +22,6 @@ import io.metersphere.system.service.ApiPluginService;
|
|||
import io.metersphere.system.uid.IDGenerator;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.jmeter.save.SaveService;
|
||||
import org.apache.jorphan.collections.HashTree;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
@ -35,7 +35,7 @@ public class JmeterParserApiDefinition implements ApiDefinitionImportParser<ApiI
|
|||
@Override
|
||||
public ApiImportFileParseResult parse(InputStream inputSource, ImportRequest request) throws Exception {
|
||||
try {
|
||||
Object scriptWrapper = SaveService.loadElement(inputSource);
|
||||
Object scriptWrapper = MsSaveService.loadElement(inputSource);
|
||||
HashTree hashTree = this.getHashTree(scriptWrapper);
|
||||
MsTestElementParser parser = new MsTestElementParser();
|
||||
AbstractMsTestElement msTestElement = parser.parse(hashTree);
|
||||
|
|
|
@ -63,6 +63,15 @@ public class JmeterElementConverterRegister {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销 MsTestElement 对应的转换器
|
||||
*
|
||||
* @param elementConverterClass 转换器的类
|
||||
*/
|
||||
public static void unRegister(Class<? extends AbstractJmeterElementConverter<? extends MsTestElement>> elementConverterClass) {
|
||||
parserMap.remove(elementConverterClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对应组件的转换器
|
||||
*
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
package io.metersphere.api.parser.jmeter.xstream;
|
||||
|
||||
import com.thoughtworks.xstream.converters.ConversionException;
|
||||
import com.thoughtworks.xstream.converters.Converter;
|
||||
import com.thoughtworks.xstream.converters.ConverterLookup;
|
||||
import com.thoughtworks.xstream.converters.ConverterRegistry;
|
||||
import com.thoughtworks.xstream.core.Caching;
|
||||
import com.thoughtworks.xstream.core.util.Cloneables;
|
||||
import com.thoughtworks.xstream.mapper.Mapper;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author jianxing
|
||||
* 代码参考 {@link com.thoughtworks.xstream.core.DefaultConverterLookup}
|
||||
* 增加了 removeConverter 实现动态移除转换器
|
||||
*/
|
||||
public class MsJmeterConverterLookup implements ConverterLookup, ConverterRegistry, Caching {
|
||||
|
||||
private MsPrioritizedList converters = new MsPrioritizedList();
|
||||
private transient Map typeToConverterMap;
|
||||
private Map serializationMap = null;
|
||||
|
||||
public MsJmeterConverterLookup() {
|
||||
this(new HashMap());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a DefaultConverterLookup with a provided map.
|
||||
*
|
||||
* @param map the map to use
|
||||
* @throws NullPointerException if map is null
|
||||
* @since 1.4.11
|
||||
*/
|
||||
public MsJmeterConverterLookup(Map map) {
|
||||
typeToConverterMap = map;
|
||||
typeToConverterMap.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated As of 1.3, use {@link #MsJmeterConverterLookup()}
|
||||
*/
|
||||
public MsJmeterConverterLookup(Mapper mapper) {
|
||||
this();
|
||||
}
|
||||
|
||||
public Converter lookupConverterForType(Class type) {
|
||||
Converter cachedConverter = type != null ? (Converter)typeToConverterMap.get(type.getName()) : null;
|
||||
if (cachedConverter != null) {
|
||||
return cachedConverter;
|
||||
}
|
||||
|
||||
final Map errors = new LinkedHashMap();
|
||||
Iterator iterator = converters.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Converter converter = (Converter)iterator.next();
|
||||
try {
|
||||
if (converter.canConvert(type)) {
|
||||
if (type != null) {
|
||||
typeToConverterMap.put(type.getName(), converter);
|
||||
}
|
||||
return converter;
|
||||
}
|
||||
} catch (final RuntimeException e) {
|
||||
errors.put(converter.getClass().getName(), e.getMessage());
|
||||
} catch (final LinkageError e) {
|
||||
errors.put(converter.getClass().getName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
final ConversionException exception = new ConversionException(errors.isEmpty()
|
||||
? "No converter specified"
|
||||
: "No converter available");
|
||||
exception.add("type", type != null ? type.getName() : "null");
|
||||
iterator = errors.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
final Map.Entry entry = (Map.Entry)iterator.next();
|
||||
exception.add("converter", entry.getKey().toString());
|
||||
exception.add("message", entry.getValue().toString());
|
||||
}
|
||||
throw exception;
|
||||
}
|
||||
|
||||
public void registerConverter(Converter converter, int priority) {
|
||||
typeToConverterMap.clear();
|
||||
converters.add(converter, priority);
|
||||
}
|
||||
|
||||
public void removeConverter(Class<? extends Converter> converterClass) {
|
||||
flushCache();
|
||||
Iterator iterator = this.converters.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Object next = iterator.next();
|
||||
if (converterClass.equals(next.getClass())) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void flushCache() {
|
||||
typeToConverterMap.clear();
|
||||
Iterator iterator = converters.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Converter converter = (Converter)iterator.next();
|
||||
if (converter instanceof Caching) {
|
||||
((Caching)converter).flushCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Object writeReplace() {
|
||||
serializationMap = (Map)Cloneables.cloneIfPossible(typeToConverterMap);
|
||||
serializationMap.clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
private Object readResolve() {
|
||||
typeToConverterMap = serializationMap == null ? new HashMap() : serializationMap;
|
||||
serializationMap = null;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package io.metersphere.api.parser.jmeter.xstream;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* @Author: jianxing
|
||||
* @CreateTime: 2024-09-13 14:18
|
||||
* 参考 {@link com.thoughtworks.xstream.core.util.PrioritizedList}
|
||||
* 主要修改 PrioritizedItemIterator remove 方式,支持移除转换器
|
||||
*/
|
||||
public class MsPrioritizedList {
|
||||
private final Set set = new TreeSet();
|
||||
private int lowestPriority = Integer.MAX_VALUE;
|
||||
private int lastId = 0;
|
||||
|
||||
public void add(Object item, int priority) {
|
||||
if (this.lowestPriority > priority) {
|
||||
this.lowestPriority = priority;
|
||||
}
|
||||
|
||||
this.set.add(new MsPrioritizedList.PrioritizedItem(item, priority, ++this.lastId));
|
||||
}
|
||||
|
||||
public Iterator iterator() {
|
||||
return new MsPrioritizedList.PrioritizedItemIterator(this.set.iterator());
|
||||
}
|
||||
|
||||
private static class PrioritizedItemIterator implements Iterator {
|
||||
private Iterator iterator;
|
||||
|
||||
public PrioritizedItemIterator(Iterator iterator) {
|
||||
this.iterator = iterator;
|
||||
}
|
||||
|
||||
public void remove() {
|
||||
this.iterator.remove();
|
||||
}
|
||||
|
||||
public boolean hasNext() {
|
||||
return this.iterator.hasNext();
|
||||
}
|
||||
|
||||
public Object next() {
|
||||
return ((MsPrioritizedList.PrioritizedItem)this.iterator.next()).value;
|
||||
}
|
||||
}
|
||||
|
||||
private static class PrioritizedItem implements Comparable {
|
||||
final Object value;
|
||||
final int priority;
|
||||
final int id;
|
||||
|
||||
public PrioritizedItem(Object value, int priority, int id) {
|
||||
this.value = value;
|
||||
this.priority = priority;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int compareTo(Object o) {
|
||||
MsPrioritizedList.PrioritizedItem other = (MsPrioritizedList.PrioritizedItem)o;
|
||||
return this.priority != other.priority ? other.priority - this.priority : other.id - this.id;
|
||||
}
|
||||
|
||||
public boolean equals(Object obj) {
|
||||
return this.id == ((MsPrioritizedList.PrioritizedItem)obj).id;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,600 @@
|
|||
package io.metersphere.api.parser.jmeter.xstream;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.*;
|
||||
|
||||
import com.thoughtworks.xstream.converters.*;
|
||||
import com.thoughtworks.xstream.core.ClassLoaderReference;
|
||||
import com.thoughtworks.xstream.core.util.CompositeClassLoader;
|
||||
import com.thoughtworks.xstream.mapper.CachingMapper;
|
||||
import io.metersphere.sdk.exception.MSException;
|
||||
import io.metersphere.sdk.util.CommonBeanFactory;
|
||||
import io.metersphere.sdk.util.LogUtils;
|
||||
import io.metersphere.system.service.PluginLoadService;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.apache.jmeter.reporters.ResultCollectorHelper;
|
||||
import org.apache.jmeter.samplers.SampleEvent;
|
||||
import org.apache.jmeter.samplers.SampleResult;
|
||||
import org.apache.jmeter.util.JMeterUtils;
|
||||
import org.apache.jmeter.util.NameUpdater;
|
||||
import org.apache.jorphan.collections.HashTree;
|
||||
import org.apache.jorphan.util.JMeterError;
|
||||
import org.apache.jorphan.util.JOrphanUtils;
|
||||
import org.pf4j.PluginWrapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.thoughtworks.xstream.XStream;
|
||||
import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider;
|
||||
import com.thoughtworks.xstream.converters.reflection.ReflectionProvider;
|
||||
import com.thoughtworks.xstream.io.xml.XppDriver;
|
||||
import com.thoughtworks.xstream.mapper.CannotResolveClassException;
|
||||
import com.thoughtworks.xstream.mapper.Mapper;
|
||||
import com.thoughtworks.xstream.mapper.MapperWrapper;
|
||||
|
||||
/**
|
||||
* @Author: jianxing
|
||||
* 代码参考 {@link org.apache.jmeter.save.SaveService}
|
||||
* 主要为了接口导入支持动态插件,将 jmx 文件转换成 HashTree
|
||||
* 修改点:
|
||||
* 1. 因为 ScriptWrapper 不是 public ,所以替换成 MsScriptWrapper
|
||||
* 2. StreamWrapper 替换为 MsXStreamWrapper,使用 MsJmeterConverterLookup,实现动态维护 converter
|
||||
* 3. 增加 registerConverter 和 removeConverter 方法,动态维护 converter
|
||||
* 4. 增加 pluginAliasMap 记录插件中 jmeter 元素的别名
|
||||
*/
|
||||
public class MsSaveService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(MsSaveService.class);
|
||||
|
||||
// Names of DataHolder entries for JTL processing
|
||||
public static final String SAMPLE_EVENT_OBJECT = "SampleEvent"; // $NON-NLS-1$
|
||||
public static final String RESULTCOLLECTOR_HELPER_OBJECT = "ResultCollectorHelper"; // $NON-NLS-1$
|
||||
|
||||
// Names of DataHolder entries for JMX processing
|
||||
public static final String TEST_CLASS_NAME = "TestClassName"; // $NON-NLS-1$
|
||||
|
||||
public static final MsJmeterConverterLookup JMETER_CONVERTER_LOOKUP = new MsJmeterConverterLookup();
|
||||
|
||||
private static final class MsXStreamWrapper extends XStream {
|
||||
private MsXStreamWrapper(ReflectionProvider reflectionProvider) {
|
||||
super(reflectionProvider, new XppDriver(), new ClassLoaderReference(new CompositeClassLoader()), null,
|
||||
type -> JMETER_CONVERTER_LOOKUP.lookupConverterForType(type),
|
||||
(converter, priority) -> JMETER_CONVERTER_LOOKUP.registerConverter(converter, priority));
|
||||
}
|
||||
|
||||
// Override wrapMapper in order to insert the Wrapper in the chain
|
||||
@Override
|
||||
protected MapperWrapper wrapMapper(MapperWrapper next) {
|
||||
// Provide our own aliasing using strings rather than classes
|
||||
return new MapperWrapper(next) {
|
||||
// Translate alias to classname and then delegate to wrapped class
|
||||
@Override
|
||||
public Class<?> realClass(String alias) {
|
||||
String fullName = aliasToClass(alias);
|
||||
if (fullName != null) {
|
||||
fullName = NameUpdater.getCurrentName(fullName);
|
||||
}
|
||||
if (fullName == null) {
|
||||
fullName = pluginAliasMap.get(alias);
|
||||
}
|
||||
try {
|
||||
return super.realClass(fullName == null ? alias : fullName);
|
||||
} catch (CannotResolveClassException e) {
|
||||
PluginLoadService pluginLoadService = CommonBeanFactory.getBean(PluginLoadService.class);
|
||||
LogUtils.info(e.getMessage());
|
||||
for (PluginWrapper plugin : pluginLoadService.getMsPluginManager().getPlugins()) {
|
||||
try {
|
||||
Class<?> aClass = plugin.getPluginClassLoader().loadClass(fullName);
|
||||
if (aClass != null) {
|
||||
return aClass;
|
||||
}
|
||||
} catch (ClassNotFoundException ex) {
|
||||
LogUtils.info(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new MSException("无法解析:" + alias);
|
||||
}
|
||||
|
||||
// Translate to alias and then delegate to wrapped class
|
||||
@Override
|
||||
public String serializedClass(@SuppressWarnings("rawtypes") // superclass does not use types
|
||||
Class type) {
|
||||
if (type == null) {
|
||||
return super.serializedClass(null); // was type, but that caused FindBugs warning
|
||||
}
|
||||
String alias = classToAlias(type.getName());
|
||||
return alias == null ? super.serializedClass(type) : alias;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static final XStream JMXSAVER = new MsXStreamWrapper(new PureJavaReflectionProvider());
|
||||
private static final XStream JTLSAVER = new MsXStreamWrapper(new PureJavaReflectionProvider());
|
||||
|
||||
static {
|
||||
JTLSAVER.setMode(XStream.NO_REFERENCES); // This is needed to stop XStream keeping copies of each class
|
||||
JMeterUtils.setupXStreamSecurityPolicy(JMXSAVER);
|
||||
JMeterUtils.setupXStreamSecurityPolicy(JTLSAVER);
|
||||
}
|
||||
|
||||
// The XML header, with placeholder for encoding, since that is controlled by property
|
||||
private static final String XML_HEADER = "<?xml version=\"1.0\" encoding=\"<ph>\"?>"; // $NON-NLS-1$
|
||||
|
||||
// Default file name
|
||||
private static final String SAVESERVICE_PROPERTIES_FILE = "saveservice.properties"; // $NON-NLS-1$
|
||||
|
||||
// Property name used to define file name
|
||||
private static final String SAVESERVICE_PROPERTIES = "saveservice_properties"; // $NON-NLS-1$
|
||||
|
||||
private static final String JMETER_ELEMENT_ALIAS_PROPERTIES = "jmeter_element_alias.properties"; // $NON-NLS-1$
|
||||
private static final Map<String, String> pluginAliasMap = new HashMap<>();
|
||||
|
||||
// Define file format versions
|
||||
private static final String VERSION_2_2 = "2.2"; // $NON-NLS-1$
|
||||
|
||||
// Holds the mappings from the saveservice properties file
|
||||
// Key: alias Entry: full class name
|
||||
// There may be multiple aliases which map to the same class
|
||||
private static final Properties aliasToClass = new Properties();
|
||||
|
||||
// Holds the reverse mappings
|
||||
// Key: full class name Entry: primary alias
|
||||
private static final Properties classToAlias = new Properties();
|
||||
|
||||
// Version information for test plan header
|
||||
// This is written to JMX files by ScriptWrapperConverter
|
||||
// Also to JTL files by ResultCollector
|
||||
private static final String VERSION = "1.2"; // $NON-NLS-1$
|
||||
|
||||
// This is written to JMX files by ScriptWrapperConverter
|
||||
private static String propertiesVersion = "";// read from properties file; written to JMX files
|
||||
|
||||
// Must match _version property value in saveservice.properties
|
||||
// used to ensure saveservice.properties and SaveService are updated simultaneously
|
||||
static final String PROPVERSION = "5.0";// Expected version $NON-NLS-1$
|
||||
|
||||
// Internal information only
|
||||
private static String fileVersion = ""; // computed from saveservice.properties file// $NON-NLS-1$
|
||||
|
||||
private static String fileEncoding = ""; // read from properties file// $NON-NLS-1$
|
||||
|
||||
static {
|
||||
log.info("Testplan (JMX) version: {}. Testlog (JTL) version: {}", VERSION_2_2, VERSION_2_2);
|
||||
initProps();
|
||||
checkVersions();
|
||||
}
|
||||
|
||||
public static Converter registerConverter(Class<? extends Converter> converterClass) {
|
||||
try {
|
||||
Converter converter = converterClass.getConstructor(Mapper.class).newInstance(getJMXMapper());
|
||||
JMETER_CONVERTER_LOOKUP.registerConverter(converter, 0);
|
||||
return converter;
|
||||
} catch (Exception e) {
|
||||
LogUtils.error(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void unRegisterConverter(Class<? extends Converter> converterClass) {
|
||||
JMETER_CONVERTER_LOOKUP.removeConverter(converterClass);
|
||||
if (getJMXMapper() instanceof CachingMapper cachingMapper) {
|
||||
// 有插件卸载,则清空缓存,否则,插件重新上传无效
|
||||
cachingMapper.flushCache();
|
||||
}
|
||||
if (getJTLMapper() instanceof CachingMapper cachingMapper) {
|
||||
// 有插件卸载,则清空缓存,否则,插件重新上传无效
|
||||
cachingMapper.flushCache();
|
||||
}
|
||||
}
|
||||
|
||||
public static Mapper getJMXMapper() {
|
||||
return JMXSAVER.getMapper();
|
||||
}
|
||||
|
||||
public static Mapper getJTLMapper() {
|
||||
return JTLSAVER.getMapper();
|
||||
}
|
||||
|
||||
// Helper method to simplify alias creation from properties
|
||||
private static void makeAlias(String aliasList, String clazz) {
|
||||
String[] aliases = aliasList.split(","); // Can have multiple aliases for same target classname
|
||||
String alias = aliases[0];
|
||||
for (String a : aliases) {
|
||||
Object old = aliasToClass.setProperty(a, clazz);
|
||||
if (old != null) {
|
||||
log.error("Duplicate class detected for {}: {} & {}", alias, clazz, old);
|
||||
}
|
||||
}
|
||||
Object oldval = classToAlias.setProperty(clazz, alias);
|
||||
if (oldval != null) {
|
||||
log.error("Duplicate alias detected for {}: {} & {}", clazz, alias, oldval);
|
||||
}
|
||||
}
|
||||
|
||||
private static File getSaveServiceFile() {
|
||||
String saveServiceProps = JMeterUtils.getPropDefault(SAVESERVICE_PROPERTIES, SAVESERVICE_PROPERTIES_FILE); //$NON-NLS-1$
|
||||
if (saveServiceProps.length() > 0) { //$NON-NLS-1$
|
||||
return JMeterUtils.findFile(saveServiceProps);
|
||||
}
|
||||
throw new IllegalStateException("Could not find file configured in saveservice_properties property set to:" + saveServiceProps);
|
||||
}
|
||||
|
||||
public static Properties loadProperties() throws IOException {
|
||||
Properties nameMap = new Properties();
|
||||
File saveServiceFile = getSaveServiceFile();
|
||||
if (saveServiceFile.canRead()) {
|
||||
try (FileInputStream fis = new FileInputStream(saveServiceFile)) {
|
||||
nameMap.load(fis);
|
||||
}
|
||||
}
|
||||
return nameMap;
|
||||
}
|
||||
|
||||
public static Properties loadPluginAliasProperties(ClassLoader classLoader) throws IOException {
|
||||
Properties nameMap = new Properties();
|
||||
InputStream resourceAsStream = classLoader.getResourceAsStream(JMETER_ELEMENT_ALIAS_PROPERTIES);
|
||||
nameMap.load(resourceAsStream);
|
||||
nameMap.forEach((k, v) -> pluginAliasMap.put((String) k, (String) v));
|
||||
return nameMap;
|
||||
}
|
||||
|
||||
private static String checksum(Properties nameMap) throws NoSuchAlgorithmException {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
||||
// This checksums the actual entries, and it ignores comments and blank lines
|
||||
nameMap.entrySet().stream().sorted(
|
||||
Comparator.comparing((Map.Entry<Object, Object> e) -> e.getKey().toString())
|
||||
.thenComparing(e -> e.getValue().toString())
|
||||
).forEachOrdered(e -> {
|
||||
md.update(e.getKey().toString().getBytes(StandardCharsets.UTF_8));
|
||||
md.update(e.getValue().toString().getBytes(StandardCharsets.UTF_8));
|
||||
});
|
||||
return JOrphanUtils.baToHexString(md.digest());
|
||||
}
|
||||
|
||||
private static void initProps() {
|
||||
// Load the alias properties
|
||||
try {
|
||||
Properties nameMap = loadProperties();
|
||||
try {
|
||||
fileVersion = checksum(nameMap);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
log.error("Can't compute checksum for saveservice properties file", e);
|
||||
throw new JMeterError("JMeter requires the checksum of saveservice properties file to continue", e);
|
||||
}
|
||||
// now create the aliases
|
||||
for (Map.Entry<Object, Object> me : nameMap.entrySet()) {
|
||||
String key = (String) me.getKey();
|
||||
String val = (String) me.getValue();
|
||||
if (!key.startsWith("_")) { // $NON-NLS-1$
|
||||
makeAlias(key, val);
|
||||
} else {
|
||||
// process special keys
|
||||
if (key.equalsIgnoreCase("_version")) { // $NON-NLS-1$
|
||||
propertiesVersion = val;
|
||||
log.info("Using SaveService properties version {}", propertiesVersion);
|
||||
} else if (key.equalsIgnoreCase("_file_version")) { // $NON-NLS-1$
|
||||
log.info("SaveService properties file version is now computed by a checksum,"
|
||||
+ "the property _file_version is not used anymore and can be removed.");
|
||||
} else if (key.equalsIgnoreCase("_file_encoding")) { // $NON-NLS-1$
|
||||
fileEncoding = val;
|
||||
log.info("Using SaveService properties file encoding {}", fileEncoding);
|
||||
} else {
|
||||
key = key.substring(1);// Remove the leading "_"
|
||||
registerConverter(key, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Bad saveservice properties file", e);
|
||||
throw new JMeterError("JMeter requires the saveservice properties file to continue");
|
||||
}
|
||||
}
|
||||
|
||||
private static void registerConverter(String key, String val) {
|
||||
try {
|
||||
final String trimmedValue = val.trim();
|
||||
boolean useMapper = "collection".equals(trimmedValue) || "mapping".equals(trimmedValue); // $NON-NLS-1$ $NON-NLS-2$
|
||||
registerConverter(key, JMXSAVER, useMapper);
|
||||
registerConverter(key, JTLSAVER, useMapper);
|
||||
} catch (IllegalAccessException | InstantiationException | ClassNotFoundException | IllegalArgumentException |
|
||||
SecurityException | InvocationTargetException | NoSuchMethodException e1) {
|
||||
log.warn("Can't register a converter: {}", key, e1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register converter.
|
||||
*
|
||||
* @param key
|
||||
* @param jmxsaver
|
||||
* @param useMapper
|
||||
* @throws InstantiationException
|
||||
* @throws IllegalAccessException
|
||||
* @throws InvocationTargetException
|
||||
* @throws NoSuchMethodException
|
||||
* @throws ClassNotFoundException
|
||||
*/
|
||||
private static void registerConverter(String key, XStream jmxsaver, boolean useMapper)
|
||||
throws InstantiationException, IllegalAccessException,
|
||||
InvocationTargetException, NoSuchMethodException,
|
||||
ClassNotFoundException {
|
||||
final Class<? extends Converter> clazz = Class.forName(key).asSubclass(Converter.class);
|
||||
if (useMapper) {
|
||||
jmxsaver.registerConverter(clazz.getConstructor(Mapper.class).newInstance(jmxsaver.getMapper()));
|
||||
} else {
|
||||
jmxsaver.registerConverter(clazz.getDeclaredConstructor().newInstance());
|
||||
}
|
||||
}
|
||||
|
||||
// For converters to use
|
||||
public static String aliasToClass(String s) {
|
||||
String r = aliasToClass.getProperty(s);
|
||||
return r == null ? s : r;
|
||||
}
|
||||
|
||||
// For converters to use
|
||||
public static String classToAlias(String s) {
|
||||
String r = classToAlias.getProperty(s);
|
||||
return r == null ? s : r;
|
||||
}
|
||||
|
||||
// Called by Save function
|
||||
public static void saveTree(HashTree tree, OutputStream out) throws IOException {
|
||||
// Get the OutputWriter to use
|
||||
OutputStreamWriter outputStreamWriter = getOutputStreamWriter(out);
|
||||
writeXmlHeader(outputStreamWriter);
|
||||
// Use deprecated method, to avoid duplicating code
|
||||
MsScriptWrapper wrapper = new MsScriptWrapper();
|
||||
wrapper.testPlan = tree;
|
||||
JMXSAVER.toXML(wrapper, outputStreamWriter);
|
||||
outputStreamWriter.write('\n');// Ensure terminated properly
|
||||
outputStreamWriter.close();
|
||||
}
|
||||
|
||||
// Used by Test code
|
||||
public static void saveElement(Object el, OutputStream out) throws IOException {
|
||||
// Get the OutputWriter to use
|
||||
OutputStreamWriter outputStreamWriter = getOutputStreamWriter(out);
|
||||
writeXmlHeader(outputStreamWriter);
|
||||
// Use deprecated method, to avoid duplicating code
|
||||
JMXSAVER.toXML(el, outputStreamWriter);
|
||||
outputStreamWriter.close();
|
||||
}
|
||||
|
||||
// Used by Test code
|
||||
public static Object loadElement(InputStream in) throws IOException {
|
||||
// Get the InputReader to use
|
||||
InputStreamReader inputStreamReader = getInputStreamReader(in);
|
||||
// Use deprecated method, to avoid duplicating code
|
||||
Object element = JMXSAVER.fromXML(inputStreamReader);
|
||||
inputStreamReader.close();
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a sampleResult to an XML output file using XStream.
|
||||
*
|
||||
* @param evt sampleResult wrapped in a sampleEvent
|
||||
* @param writer output stream which must be created using {@link #getFileEncoding(String)}
|
||||
* @throws IOException when writing data to output fails
|
||||
*/
|
||||
// Used by ResultCollector.sampleOccurred(SampleEvent event)
|
||||
public static synchronized void saveSampleResult(SampleEvent evt, Writer writer) throws IOException {
|
||||
DataHolder dh = JTLSAVER.newDataHolder();
|
||||
dh.put(SAMPLE_EVENT_OBJECT, evt);
|
||||
// This is effectively the same as saver.toXML(Object, Writer) except we get to provide the DataHolder
|
||||
// Don't know why there is no method for this in the XStream class
|
||||
try {
|
||||
JTLSAVER.marshal(evt.getResult(), new XppDriver().createWriter(writer), dh);
|
||||
} catch (RuntimeException e) {
|
||||
throw new IllegalArgumentException("Failed marshalling:" + (evt.getResult() != null ? showDebuggingInfo(evt.getResult()) : "null"), e);
|
||||
}
|
||||
writer.write('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param result SampleResult
|
||||
* @return String debugging information
|
||||
*/
|
||||
private static String showDebuggingInfo(SampleResult result) {
|
||||
try {
|
||||
return "class:" + result.getClass() + ",content:" + ToStringBuilder.reflectionToString(result);
|
||||
} catch (Exception e) {
|
||||
return "Exception occurred creating debug from event, message:" + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// Routines for TestSaveService
|
||||
static String getPropertyVersion() {
|
||||
return MsSaveService.propertiesVersion;
|
||||
}
|
||||
|
||||
static String getFileVersion() {
|
||||
return MsSaveService.fileVersion;
|
||||
}
|
||||
|
||||
// Allow test code to check for spurious class references
|
||||
static List<String> checkClasses() {
|
||||
final ClassLoader classLoader = MsSaveService.class.getClassLoader();
|
||||
List<String> missingClasses = new ArrayList<>();
|
||||
for (Object clazz : classToAlias.keySet()) {
|
||||
String name = (String) clazz;
|
||||
if (!NameUpdater.isMapped(name)) {// don't bother checking class is present if it is to be updated
|
||||
try {
|
||||
Class.forName(name, false, classLoader);
|
||||
} catch (ClassNotFoundException e) {
|
||||
log.error("Unexpected entry in saveservice.properties; class does not exist and is not upgraded: {}", name);
|
||||
missingClasses.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
return missingClasses;
|
||||
}
|
||||
|
||||
private static void checkVersions() {
|
||||
if (!PROPVERSION.equalsIgnoreCase(propertiesVersion)) {
|
||||
log.warn("Bad _version - expected {}, found {}.", PROPVERSION, propertiesVersion);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read results from JTL file.
|
||||
*
|
||||
* @param reader of the file
|
||||
* @param resultCollectorHelper helper class to enable TestResultWrapperConverter to deliver the samples
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
public static void loadTestResults(InputStream reader, ResultCollectorHelper resultCollectorHelper) throws IOException {
|
||||
// Get the InputReader to use
|
||||
InputStreamReader inputStreamReader = getInputStreamReader(reader);
|
||||
DataHolder dh = JTLSAVER.newDataHolder();
|
||||
dh.put(RESULTCOLLECTOR_HELPER_OBJECT, resultCollectorHelper); // Allow TestResultWrapper to feed back the samples
|
||||
// This is effectively the same as saver.fromXML(InputStream) except we get to provide the DataHolder
|
||||
// Don't know why there is no method for this in the XStream class
|
||||
JTLSAVER.unmarshal(new XppDriver().createReader(reader), null, dh);
|
||||
inputStreamReader.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a Test tree (JMX file)
|
||||
*
|
||||
* @param file the JMX file
|
||||
* @return the loaded tree
|
||||
* @throws IOException if there is a problem reading the file or processing it
|
||||
*/
|
||||
public static HashTree loadTree(File file) throws IOException {
|
||||
log.info("Loading file: {}", file);
|
||||
try (InputStream inputStream = new FileInputStream(file);
|
||||
BufferedInputStream bufferedInputStream =
|
||||
new BufferedInputStream(inputStream)) {
|
||||
return readTree(bufferedInputStream, file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param inputStream {@link InputStream}
|
||||
* @param file the JMX file used only for debug, can be null
|
||||
* @return the loaded tree
|
||||
* @throws IOException if there is a problem reading the file or processing it
|
||||
*/
|
||||
private static HashTree readTree(InputStream inputStream, File file)
|
||||
throws IOException {
|
||||
MsScriptWrapper wrapper = null;
|
||||
try {
|
||||
// Get the InputReader to use
|
||||
InputStreamReader inputStreamReader = getInputStreamReader(inputStream);
|
||||
wrapper = (MsScriptWrapper) JMXSAVER.fromXML(inputStreamReader);
|
||||
inputStreamReader.close();
|
||||
if (wrapper == null) {
|
||||
log.error("Problem loading XML: see above.");
|
||||
return null;
|
||||
}
|
||||
return wrapper.testPlan;
|
||||
} catch (CannotResolveClassException | ConversionException | NoClassDefFoundError e) {
|
||||
if (file != null) {
|
||||
throw new IllegalArgumentException("Problem loading XML from:'" + file.getAbsolutePath() + "'. \nCause:\n" +
|
||||
ExceptionUtils.getRootCauseMessage(e) + "\n\n Detail:" + e, e);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Problem loading XML. \nCause:\n" +
|
||||
ExceptionUtils.getRootCauseMessage(e) + "\n\n Detail:" + e, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static InputStreamReader getInputStreamReader(InputStream inStream) {
|
||||
// Check if we have a encoding to use from properties
|
||||
Charset charset = getFileEncodingCharset();
|
||||
return new InputStreamReader(inStream, charset);
|
||||
}
|
||||
|
||||
private static OutputStreamWriter getOutputStreamWriter(OutputStream outStream) {
|
||||
// Check if we have a encoding to use from properties
|
||||
Charset charset = getFileEncodingCharset();
|
||||
return new OutputStreamWriter(outStream, charset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file Encoding specified in saveservice.properties or the default
|
||||
*
|
||||
* @param dflt value to return if file encoding was not provided
|
||||
* @return file encoding or default
|
||||
*/
|
||||
// Used by ResultCollector when creating output files
|
||||
public static String getFileEncoding(String dflt) {
|
||||
if (fileEncoding != null && fileEncoding.length() > 0) {
|
||||
return fileEncoding;
|
||||
} else {
|
||||
return dflt;
|
||||
}
|
||||
}
|
||||
|
||||
// @NotNull
|
||||
private static Charset getFileEncodingCharset() {
|
||||
// Check if we have a encoding to use from properties
|
||||
if (fileEncoding != null && fileEncoding.length() > 0) {
|
||||
return Charset.forName(fileEncoding);
|
||||
} else {
|
||||
|
||||
// We use the default character set encoding of the JRE
|
||||
log.info("fileEncoding not defined - using JRE default");
|
||||
return Charset.defaultCharset();
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeXmlHeader(OutputStreamWriter writer) throws IOException {
|
||||
// Write XML header if we have the charset to use for encoding
|
||||
Charset charset = getFileEncodingCharset();
|
||||
// We do not use getEncoding method of Writer, since that returns
|
||||
// the historical name
|
||||
String header = XML_HEADER.replaceAll("<ph>", charset.name());
|
||||
writer.write(header);
|
||||
writer.write('\n');
|
||||
}
|
||||
|
||||
// Normal output
|
||||
// ---- Debugging information ----
|
||||
// required-type : org.apache.jorphan.collections.ListedHashTree
|
||||
// cause-message : WebServiceSampler : WebServiceSampler
|
||||
// class : org.apache.jmeter.save.ScriptWrapper
|
||||
// message : WebServiceSampler : WebServiceSampler
|
||||
// line number : 929
|
||||
// path : /jmeterTestPlan/hashTree/hashTree/hashTree[4]/hashTree[5]/WebServiceSampler
|
||||
// cause-exception : com.thoughtworks.xstream.alias.CannotResolveClassException
|
||||
// -------------------------------
|
||||
|
||||
/**
|
||||
* Simplify getMessage() output from XStream ConversionException
|
||||
*
|
||||
* @param ce - ConversionException to analyse
|
||||
* @return string with details of error
|
||||
*/
|
||||
public static String CEtoString(ConversionException ce) {
|
||||
return "XStream ConversionException at line: " + ce.get("line number") + "\n" + ce.get("message")
|
||||
+ "\nPerhaps a missing jar? See log file.";
|
||||
}
|
||||
|
||||
public static String getPropertiesVersion() {
|
||||
return propertiesVersion;
|
||||
}
|
||||
|
||||
public static String getVERSION() {
|
||||
return VERSION;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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 io.metersphere.api.parser.jmeter.xstream;
|
||||
|
||||
import org.apache.jorphan.collections.HashTree;
|
||||
class MsScriptWrapper {
|
||||
// Used by ScriptWrapperConverter
|
||||
String version = "";
|
||||
|
||||
HashTree testPlan;
|
||||
}
|
|
@ -51,8 +51,6 @@ public class MsElementConverterRegister {
|
|||
register(BeanShellPreProcessConverter.class);
|
||||
register(JDBCPreProcessConverter.class);
|
||||
register(JSR223PreProcessConverter.class);
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -71,6 +69,15 @@ public class MsElementConverterRegister {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销 TestElement 对应的转换器
|
||||
*
|
||||
* @param elementConverterClass 转换器的类
|
||||
*/
|
||||
public static void unRegister(Class<? extends AbstractMsElementConverter<? extends TestElement>> elementConverterClass) {
|
||||
parserMap.remove(elementConverterClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对应组件的转换器
|
||||
*
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
package io.metersphere.api.service;
|
||||
|
||||
import com.thoughtworks.xstream.converters.Converter;
|
||||
import io.metersphere.api.parser.jmeter.xstream.MsSaveService;
|
||||
import io.metersphere.api.parser.ms.MsElementConverterRegister;
|
||||
import io.metersphere.api.utils.ApiDataUtils;
|
||||
import io.metersphere.api.parser.jmeter.JmeterElementConverterRegister;
|
||||
import io.metersphere.plugin.api.spi.AbstractApiPlugin;
|
||||
import io.metersphere.plugin.api.spi.AbstractJmeterElementConverter;
|
||||
import io.metersphere.plugin.api.spi.JmeterElementConverter;
|
||||
import io.metersphere.plugin.api.spi.MsTestElement;
|
||||
import io.metersphere.plugin.api.spi.*;
|
||||
import io.metersphere.sdk.plugin.MsPluginManager;
|
||||
import io.metersphere.sdk.util.CommonBeanFactory;
|
||||
import io.metersphere.sdk.util.LogUtils;
|
||||
import io.metersphere.system.service.PluginChangeService;
|
||||
import io.metersphere.system.service.PluginLoadService;
|
||||
import org.apache.jmeter.testelement.TestElement;
|
||||
import org.pf4j.Plugin;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
|
@ -35,12 +36,46 @@ public class ApiPluginChangeService implements PluginChangeService {
|
|||
extensionClasses.forEach(clazzList::add);
|
||||
ApiDataUtils.setResolvers(clazzList);
|
||||
|
||||
// 注册插件元素解析器
|
||||
// 注册插件 JmeterElementConverter 解析器
|
||||
msPluginManager.getExtensionClasses(JmeterElementConverter.class, pluginId)
|
||||
.forEach(item -> JmeterElementConverterRegister.register((Class<? extends AbstractJmeterElementConverter<? extends MsTestElement>>) item));
|
||||
|
||||
// 注册插件 MsElementConverter 解析器
|
||||
msPluginManager.getExtensionClasses(MsElementConverter.class, pluginId)
|
||||
.forEach(item -> MsElementConverterRegister.register((Class<? extends AbstractMsElementConverter<? extends TestElement>>) item));
|
||||
|
||||
// 注册插件 xstream xml 序列化解析器
|
||||
msPluginManager.getExtensionClasses(Converter.class, pluginId)
|
||||
.forEach(MsSaveService::registerConverter);
|
||||
|
||||
// 加载插件 jmeter 元素别名
|
||||
MsSaveService.loadPluginAliasProperties(msPluginManager.getPluginClassLoader(pluginId));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.error("注册接口插件实现类失败:{}", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePluginUnLoad(String pluginId) {
|
||||
MsPluginManager msPluginManager = CommonBeanFactory.getBean(PluginLoadService.class).getMsPluginManager();
|
||||
Plugin plugin = msPluginManager.getPlugin(pluginId).getPlugin();
|
||||
try {
|
||||
if (plugin instanceof AbstractApiPlugin) {
|
||||
// 注销插件 JmeterElementConverter 解析器
|
||||
msPluginManager.getExtensionClasses(JmeterElementConverter.class, pluginId)
|
||||
.forEach(item -> JmeterElementConverterRegister.unRegister((Class<? extends AbstractJmeterElementConverter<? extends MsTestElement>>) item));
|
||||
|
||||
// 注销插件 MsElementConverter 解析器
|
||||
msPluginManager.getExtensionClasses(MsElementConverter.class, pluginId)
|
||||
.forEach(item -> MsElementConverterRegister.unRegister((Class<? extends AbstractMsElementConverter<? extends TestElement>>) item));
|
||||
|
||||
// 注销插件 xstream xml 序列化解析器
|
||||
msPluginManager.getExtensionClasses(Converter.class, pluginId)
|
||||
.forEach(MsSaveService::unRegisterConverter);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.error("注销接口插件实现类失败:{}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
package io.metersphere.api.service;
|
||||
|
||||
import io.metersphere.api.parser.jmeter.xstream.MsSaveService;
|
||||
import io.metersphere.system.base.BaseApiPluginTestService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.jorphan.collections.HashTree;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
/**
|
||||
* @Author: jianxing
|
||||
* @CreateTime: 2024-08-01 14:04
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
@AutoConfigureMockMvc
|
||||
public class MsSaveServiceTests {
|
||||
|
||||
@Resource
|
||||
private BaseApiPluginTestService baseApiPluginTestService;
|
||||
|
||||
@Test
|
||||
public void getBlankJon() throws Exception {
|
||||
baseApiPluginTestService.addPlugin("file/tcp-sampler-xstream.jar");
|
||||
File tcpJmx = new File(
|
||||
this.getClass().getClassLoader().getResource("file/tcp.jmx")
|
||||
.getPath()
|
||||
);
|
||||
Object scriptWrapper = MsSaveService.loadElement(new FileInputStream(tcpJmx));
|
||||
getHashTree(scriptWrapper);
|
||||
}
|
||||
|
||||
private HashTree getHashTree(Object scriptWrapper) throws Exception {
|
||||
Field field = scriptWrapper.getClass().getDeclaredField("testPlan");
|
||||
field.setAccessible(true);
|
||||
return (HashTree) field.get(scriptWrapper);
|
||||
}
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
package io.metersphere.api.utils;
|
||||
|
||||
import io.metersphere.api.parser.jmeter.xstream.MsSaveService;
|
||||
import io.metersphere.api.parser.ms.MsTestElementParser;
|
||||
import io.metersphere.plugin.api.spi.AbstractMsProtocolTestElement;
|
||||
import io.metersphere.plugin.api.spi.AbstractMsTestElement;
|
||||
import org.apache.jmeter.save.SaveService;
|
||||
import org.apache.jorphan.collections.HashTree;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
|
@ -39,7 +39,7 @@ public class MsTestElementParserTest {
|
|||
.getPath()
|
||||
);
|
||||
|
||||
Object scriptWrapper = SaveService.loadElement(new FileInputStream(httpJmx));
|
||||
Object scriptWrapper = MsSaveService.loadElement(new FileInputStream(httpJmx));
|
||||
HashTree hashTree = getHashTree(scriptWrapper);
|
||||
MsTestElementParser parser = new MsTestElementParser();
|
||||
AbstractMsTestElement msTestElement = parser.parse(hashTree);
|
||||
|
@ -51,7 +51,7 @@ public class MsTestElementParserTest {
|
|||
this.getClass().getClassLoader().getResource("file/import/jmeter/single.jmx")
|
||||
.getPath()
|
||||
);
|
||||
scriptWrapper = SaveService.loadElement(new FileInputStream(httpJmx));
|
||||
scriptWrapper = MsSaveService.loadElement(new FileInputStream(httpJmx));
|
||||
hashTree = getHashTree(scriptWrapper);
|
||||
parser = new MsTestElementParser();
|
||||
msTestElement = parser.parse(hashTree);
|
||||
|
|
Binary file not shown.
|
@ -0,0 +1,59 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.6.3">
|
||||
<hashTree>
|
||||
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" enabled="true">
|
||||
<boolProp name="TestPlan.functional_mode">false</boolProp>
|
||||
<boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
|
||||
<boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
|
||||
<elementProp name="TestPlan.user_defined_variables" elementType="Arguments">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
</TestPlan>
|
||||
<hashTree>
|
||||
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="172474118045900001" enabled="true">
|
||||
<intProp name="ThreadGroup.num_threads">1</intProp>
|
||||
<intProp name="ThreadGroup.ramp_time">1</intProp>
|
||||
<longProp name="ThreadGroup.delay">0</longProp>
|
||||
<longProp name="ThreadGroup.duration">0</longProp>
|
||||
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
|
||||
<boolProp name="ThreadGroup.scheduler">false</boolProp>
|
||||
<elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="LoopController" enabled="true">
|
||||
<intProp name="LoopController.loops">1</intProp>
|
||||
<boolProp name="LoopController.continue_forever">false</boolProp>
|
||||
</elementProp>
|
||||
</ThreadGroup>
|
||||
<hashTree>
|
||||
<TCPSampler guiclass="TCPSamplerGui" testclass="TCPSampler" testname="12312312" enabled="true">
|
||||
<stringProp name="TCPSampler.classname">TCPClientImpl</stringProp>
|
||||
<stringProp name="TCPSampler.ctimeout">5000</stringProp>
|
||||
<boolProp name="TCPSampler.reUseConnection">false</boolProp>
|
||||
<boolProp name="TCPSampler.nodelay">false</boolProp>
|
||||
<stringProp name="TCPSampler.closeConnection">false</stringProp>
|
||||
<stringProp name="TCPSampler.timeout">5000</stringProp>
|
||||
<stringProp name="TCPSampler.request">123123</stringProp>
|
||||
<stringProp name="MS_RESOURCE_ID">913573930622976</stringProp>
|
||||
<stringProp name="MS_STEP_ID">913573930622976</stringProp>
|
||||
<stringProp name="MS_REPORT_ID">172474118045900001</stringProp>
|
||||
<stringProp name="PROJECT_ID">718255970852864</stringProp>
|
||||
<stringProp name="MS_CUSTOM_URL">TCP://null:null</stringProp>
|
||||
<stringProp name="xxx">{"reportId":"172474118045900001","parseDisabledElement":false,"enableGlobalCookie":true,"envConfig":{"id":"2160780881534976","projectId":"718255970852864","name":"百度云","config":{"commonParams":{"requestTimeout":60000,"responseTimeout":60000},"commonVariables":[{"key":"","value":"","id":"aa0d9ff3-e0da-4f24-bf0c-51728e37bcd4","paramType":"CONSTANT","enable":true,"description":"","tags":[],"notBlankValue":false,"valid":false}],"httpConfig":[{"id":"4062c143-b73c-4109-bceb-6a60a8067f34","protocol":"https","hostname":"www.baidu.com","url":"https://www.baidu.com","type":"NONE","pathMatchRule":{"condition":"CONTAINS","path":""},"moduleMatchRule":{"modules":[]},"headers":[],"description":"","order":1,"authConfig":{"authType":"NONE","basicAuth":{"userName":"","password":"","valid":false},"digestAuth":{"userName":"","password":"","valid":false},"httpauthValid":false},"moduleMatchRuleOrder":2}],"dataSources":[{"id":"1ab74c5f-eea2-4d35-9bbc-e8e17e96185e","dataSource":"DM","driver":"dm.jdbc.driver.DmDriver","driverId":"dm.jdbc.driver.DmDriver&dm.jdbc.driver.DmDriver","dbUrl":"jdbc:dm://123.56.8.132:5236/SYS","username":"SYSDBA","password":"SYSDBA001","poolMax":1,"timeout":1000}],"hostConfig":{"enable":false,"hosts":[{"ip":null,"domain":null,"description":null}]},"preProcessorConfig":{"apiProcessorConfig":{"planProcessorConfig":{"processors":[]},"scenarioProcessorConfig":{"processors":[]},"requestProcessorConfig":{"processors":[]}}},"postProcessorConfig":{"apiProcessorConfig":{"planProcessorConfig":{"processors":[]},"scenarioProcessorConfig":{"processors":[]},"requestProcessorConfig":{"processors":[]}}},"assertionConfig":{"assertions":[]},"pluginConfigMap":{}},"mock":false,"description":null},"globalParams":null,"retryOnFail":false,"retryConfig":null,"testElementClassPluginIdMap":{"io.metersphere.plugin.sofa.rpc.SofaRpcSamplerModule":"sofa-rpc-sampler","io.metersphere.plugin.sampler.MsSpxTradeSampler":"SpxSampler","io.metersphere.plugin.redis.mode.RedisSamplerModule":"redis-sampler","io.metersphere.plugin.mongo.mode.MongoSamplerModule":"metersphere-plugin-mongo","io.metersphere.plugin.tcp.TCPSamplerModule":"tcp-sampler"},"testElementClassProtocolMap":{"io.metersphere.plugin.sofa.rpc.SofaRpcSamplerModule":"MongoDB","io.metersphere.plugin.sampler.MsSpxTradeSampler":"MongoDB","io.metersphere.plugin.redis.mode.RedisSamplerModule":"MongoDB","io.metersphere.plugin.mongo.mode.MongoSamplerModule":"MongoDB","io.metersphere.plugin.tcp.TCPSamplerModule":"MongoDB"}}</stringProp>
|
||||
</TCPSampler>
|
||||
<hashTree>
|
||||
<JSR223PostProcessor guiclass="TestBeanGUI" testclass="JSR223PostProcessor" testname="脚本名称" enabled="true">
|
||||
<boolProp name="cacheKey">false</boolProp>
|
||||
<stringProp name="PROJECT_ID">718255970852864</stringProp>
|
||||
<stringProp name="script">log.info("=======");</stringProp>
|
||||
<stringProp name="scriptLanguage">beanshell</stringProp>
|
||||
</JSR223PostProcessor>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<DebugSampler guiclass="TestBeanGUI" testclass="DebugSampler" testname="RunningDebugSampler" enabled="true">
|
||||
<boolProp name="displayJMeterProperties">false</boolProp>
|
||||
<boolProp name="displayJMeterVariables">true</boolProp>
|
||||
<boolProp name="displaySystemProperties">false</boolProp>
|
||||
</DebugSampler>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
</jmeterTestPlan>
|
|
@ -24,4 +24,9 @@ public class PluginChangeServiceInvoker implements PluginChangeService {
|
|||
public void handlePluginLoad(String pluginId) {
|
||||
this.pluginChangeServices.forEach(service -> service.handlePluginLoad(pluginId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePluginUnLoad(String pluginId) {
|
||||
this.pluginChangeServices.forEach(service -> service.handlePluginUnLoad(pluginId));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,13 @@ package io.metersphere.system.service;
|
|||
*/
|
||||
public interface PluginChangeService {
|
||||
/**
|
||||
* 插件时调用
|
||||
* 插件加载时调用
|
||||
* @param pluginId 插件ID
|
||||
*/
|
||||
void handlePluginLoad(String pluginId);
|
||||
/**
|
||||
* 插件卸载时调用
|
||||
* @param pluginId 插件ID
|
||||
*/
|
||||
void handlePluginUnLoad(String pluginId);
|
||||
}
|
||||
|
|
|
@ -210,6 +210,7 @@ public class PluginLoadService {
|
|||
*/
|
||||
public synchronized void unloadPlugin(String pluginId) {
|
||||
if (hasPlugin(pluginId)) {
|
||||
pluginChangeServiceInvoker.handlePluginUnLoad(pluginId);
|
||||
msPluginManager.deletePlugin(pluginId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@ import org.springframework.stereotype.Service;
|
|||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import static io.metersphere.sdk.constants.InternalUserRole.ADMIN;
|
||||
|
||||
|
@ -37,19 +39,23 @@ public class BaseApiPluginTestService {
|
|||
if (hasJdbcPlugin()) {
|
||||
return jdbcPlugin;
|
||||
}
|
||||
jdbcPlugin = addPlugin("file/jdbc-sampler-v3.x.jar");
|
||||
return jdbcPlugin;
|
||||
}
|
||||
|
||||
public Plugin addPlugin(String filePath) throws IOException {
|
||||
PluginUpdateRequest request = new PluginUpdateRequest();
|
||||
File jarFile = new File(
|
||||
this.getClass().getClassLoader().getResource("file/jdbc-sampler-v3.x.jar")
|
||||
this.getClass().getClassLoader().getResource(filePath)
|
||||
.getPath()
|
||||
);
|
||||
FileInputStream inputStream = new FileInputStream(jarFile);
|
||||
MockMultipartFile mockMultipartFile = new MockMultipartFile(jarFile.getName(), jarFile.getName(), "jar", inputStream);
|
||||
request.setName("测试jdbc插件");
|
||||
request.setName(UUID.randomUUID().toString());
|
||||
request.setGlobal(true);
|
||||
request.setEnable(true);
|
||||
request.setCreateUser(ADMIN.name());
|
||||
jdbcPlugin = pluginService.add(request, mockMultipartFile);
|
||||
return jdbcPlugin;
|
||||
return pluginService.add(request, mockMultipartFile);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -187,7 +187,7 @@ public class PluginControllerTests extends BaseTest {
|
|||
);
|
||||
this.requestMultipartWithOkAndReturn(DEFAULT_ADD,
|
||||
getDefaultMultiPartParam(request, myDriver));
|
||||
Assertions.assertEquals(jdbcDriverPluginService.getJdbcDriverClass(DEFAULT_ORGANIZATION_ID), Arrays.asList("io.jianxing.MyDriver", "com.mysql.cj.jdbc.Driver"));
|
||||
Assertions.assertEquals(jdbcDriverPluginService.getJdbcDriverClass(DEFAULT_ORGANIZATION_ID), Arrays.asList("io.jianxing.MyDriver", "io.jianxing.MyDriver", "com.mysql.cj.jdbc.Driver"));
|
||||
|
||||
// 校验QUOTA动上传成功
|
||||
request.setName("cloud-quota-plugin");
|
||||
|
|
Loading…
Reference in New Issue