diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml new file mode 100644 index 0000000..2a07cb3 --- /dev/null +++ b/.github/workflows/build-push.yml @@ -0,0 +1,58 @@ +name: Build Docker Image and Push + +on: + push: + branches: + - master + - v1* + pull_request: + branches: + - master + - v1* + + workflow_dispatch: + +jobs: + build_push: + runs-on: ubuntu-latest + name: Build Docker Image and Push + steps: + - uses: actions/checkout@v2 + - name: Inject slug/short variables + uses: rlespinasse/github-slug-action@v3.x + - name: Cache node modules + uses: actions/cache@v2 + env: + cache-name: cache-node-modules + with: + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + - name: Cache local Maven repository + uses: actions/cache@v2 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + server-id: github # Value of the distributionManagement/repository/id field of the pom.xml + settings-path: ${{ github.workspace }} # location for the settings.xml file + + - name: Build with Maven + run: mvn -B package --file pom.xml + + - name: Push to Docker Hub + uses: docker/build-push-action@v1 + with: + username: metersphere + password: ${{ secrets.DOCKER_HUB_TOKEN }} + repository: metersphere/node-controller + tags: ${{ env.GITHUB_REF_SLUG }} + build_args: MS_VERSION=${{ env.GITHUB_REF_SLUG }}-${{ env.GITHUB_SHA_SHORT }} diff --git a/.gitignore b/.gitignore index 179b07f..b9d234f 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ target/ *.iml *.ipr src/test/ +*.jar \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 644a0c6..4abaf45 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,28 @@ -FROM registry.fit2cloud.com/fit2cloud2/fabric8-java-alpine-openjdk8-jre +FROM openjdk:8-jdk-alpine as build +WORKDIR /workspace/app -MAINTAINER FIT2CLOUD +COPY target/*.jar . -RUN mkdir -p /opt/apps +RUN mkdir -p dependency && (cd dependency; jar -xf ../*.jar) -ADD target/node-controller-1.0.jar /opt/apps +FROM metersphere/fabric8-java-alpine-openjdk8-jre -ENV JAVA_APP_JAR=/opt/apps/node-controller-1.0.jar +LABEL maintainer="FIT2CLOUD " -ENV AB_OFF=true +ARG MS_VERSION=dev +ARG DEPENDENCY=/workspace/app/dependency + +COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib +COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF +COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app +RUN mv /app/jmeter /opt/ +RUN mkdir -p /opt/jmeter/lib/junit + +ENV JAVA_CLASSPATH=/app:/app/lib/* +ENV JAVA_MAIN_CLASS=io.metersphere.Application +ENV AB_OFF=true +ENV MS_VERSION=${MS_VERSION} ENV JAVA_OPTIONS=-Dfile.encoding=utf-8 CMD ["/deployments/run-java.sh"] diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..1106dc0 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,43 @@ +pipeline { + agent { + node { + label 'metersphere' + } + } + options { quietPeriod(600) } + environment { + IMAGE_NAME = 'ms-node-controller' + IMAGE_PREFIX = 'registry.cn-qingdao.aliyuncs.com/metersphere' + } + stages { + stage('Build/Test') { + steps { + configFileProvider([configFile(fileId: 'metersphere-maven', targetLocation: 'settings.xml')]) { + sh "mvn clean package --settings ./settings.xml" + } + } + } + stage('Docker build & push') { + steps { + sh "docker build --build-arg MS_VERSION=\${TAG_NAME:-\$BRANCH_NAME}-\${GIT_COMMIT:0:8} -t ${IMAGE_NAME}:\${TAG_NAME:-\$BRANCH_NAME} ." + sh "docker tag ${IMAGE_NAME}:\${TAG_NAME:-\$BRANCH_NAME} ${IMAGE_PREFIX}/${IMAGE_NAME}:\${TAG_NAME:-\$BRANCH_NAME}" + sh "docker push ${IMAGE_PREFIX}/${IMAGE_NAME}:\${TAG_NAME:-\$BRANCH_NAME}" + } + } + stage('Notification') { + steps { + withCredentials([string(credentialsId: 'wechat-bot-webhook', variable: 'WEBHOOK')]) { + qyWechatNotification failSend: true, mentionedId: '', mentionedMobile: '', webhookUrl: '${WEBHOOK}' + } + } + } + } + post('Notification') { + always { + sh "echo \$WEBHOOK\n" + withCredentials([string(credentialsId: 'wechat-bot-webhook', variable: 'WEBHOOK')]) { + qyWechatNotification failSend: true, mentionedId: '', mentionedMobile: '', webhookUrl: "$WEBHOOK" + } + } + } +} diff --git a/pom.xml b/pom.xml index eaabcaf..283a442 100644 --- a/pom.xml +++ b/pom.xml @@ -5,20 +5,29 @@ org.springframework.boot spring-boot-starter-parent - 2.2.6.RELEASE + 2.5.4 io.metersphere node-controller - 1.0 + 1.12 node-controller node-controller 1.8 + 5.4.1 + 1.1.3 + 2.7.8 + 3.0.8 + 1.4 + + org.springframework.boot + spring-boot-configuration-processor + org.springframework.boot spring-boot-starter-web @@ -37,13 +46,16 @@ com.github.docker-java docker-java - 3.2.0 + 3.2.5 org.projectlombok lombok - + + org.apache.commons + commons-lang3 + org.springframework.boot spring-boot-starter-test @@ -55,6 +67,346 @@ + + + + org.flywaydb + flyway-core + + + mysql + mysql-connector-java + runtime + + + com.github.pagehelper + pagehelper + 5.0.3 + + + + org.apache.commons + commons-collections4 + 4.1 + + + org.apache.commons + commons-text + 1.8 + + + commons-codec + commons-codec + + + + com.alibaba + fastjson + 1.2.72 + + + + + org.springdoc + springdoc-openapi-ui + 1.5.6 + + + + org.apache.jmeter + ApacheJMeter_http + ${jmeter.version} + + + org.apache.logging.log4j + log4j-slf4j-impl + + + xstream + com.thoughtworks.xstream + + + + + + com.thoughtworks.xstream + xstream + 1.4.17 + + + + org.python + jython-standalone + 2.7.0 + + + + org.apache.jmeter + ApacheJMeter_functions + ${jmeter.version} + + + + org.apache.jmeter + ApacheJMeter_jdbc + ${jmeter.version} + + + + org.apache.jmeter + ApacheJMeter_tcp + ${jmeter.version} + + + org.apache.jmeter + ApacheJMeter_java + ${jmeter.version} + + + + com.microsoft.sqlserver + mssql-jdbc + 7.4.1.jre8 + + + + org.postgresql + postgresql + 42.2.14 + + + + com.oracle.database.jdbc + ojdbc8 + 19.7.0.0 + + + + + org.apache.dubbo + dubbo + ${dubbo.version} + + + spring-context + org.springframework + + + + + org.apache.zookeeper + zookeeper + 3.4.14 + + + slf4j-log4j12 + org.slf4j + + + + + org.apache.curator + curator-framework + 4.0.1 + + + org.apache.curator + curator-recipes + 4.0.1 + + + + + org.apache.dubbo + dubbo-registry-nacos + ${dubbo.version} + + + com.alibaba.nacos + nacos-api + ${nacos.version} + + + com.alibaba.nacos + nacos-client + ${nacos.version} + + + + com.alibaba + easyexcel + 2.1.7 + + + + io.metersphere + jmeter-plugins-dubbo + 2.7.12 + + + + + org.springframework.boot + spring-boot-starter-data-ldap + + + + + io.swagger.parser.v3 + swagger-parser + 2.0.24 + + + + + org.codehaus.groovy + groovy-jsr223 + + + + + org.mozilla + rhino-engine + 1.7.13 + + + + org.jsoup + jsoup + 1.10.3 + + + + com.atlassian.commonmark + commonmark + 0.15.2 + + + + org.apache.commons + commons-compress + 1.20 + + + + org.dom4j + dom4j + 2.1.3 + + + + jaxen + jaxen + 1.2.0 + + + net.sourceforge.htmlcleaner + htmlcleaner + 2.24 + + + + org.json + json + 20171018 + + + + com.aliyun + alibaba-dingtalk-service-sdk + 1.0.1 + + + org.apache.httpcomponents + httpclient + + + + net.oneandone.reflections8 + reflections8 + 0.11.7 + + + + io.fabric8 + kubernetes-client + 4.13.0 + + + com.github.fge + json-schema-validator + 2.2.6 + + + + org.apache.jmeter + ApacheJMeter_bolt + ${jmeter.version} + + + org.apache.jmeter + ApacheJMeter_jms + ${jmeter.version} + + + org.apache.jmeter + ApacheJMeter_ftp + ${jmeter.version} + + + org.apache.jmeter + ApacheJMeter_junit + ${jmeter.version} + + + org.apache.jmeter + ApacheJMeter_ldap + ${jmeter.version} + + + org.apache.jmeter + ApacheJMeter_mail + ${jmeter.version} + + + org.apache.jmeter + ApacheJMeter_components + ${jmeter.version} + + + org.apache.jmeter + ApacheJMeter_native + ${jmeter.version} + + + io.metersphere + metersphere-jmeter-functions + ${metersphere-jmeter-functions.version} + + + + + com.jayway.jsonpath + json-path + 2.4.0 + + + org.aspectj + aspectjweaver + 1.9.6 + + + commons-collections + commons-collections + 3.2.2 + + + org.springframework.kafka + spring-kafka + + + + @@ -63,6 +415,53 @@ org.springframework.boot spring-boot-maven-plugin + + org.apache.maven.plugins + maven-dependency-plugin + + + copy + generate-resources + + copy + + + + + + + org.apache.jmeter + ApacheJMeter_functions + ${jmeter.version} + jar + true + src/main/resources/jmeter/lib/ext + ApacheJMeter_functions.jar + + + io.metersphere + metersphere-jmeter-functions + ${metersphere-jmeter-functions.version} + jar + true + src/main/resources/jmeter/lib/ext + metersphere-jmeter-functions.jar + + + org.python + jython-standalone + 2.7.0 + jar + true + src/main/resources/jmeter/lib/ext + jython-standalone.jar + + + ${project.build.directory}/wars + false + true + + diff --git a/src/main/java/io/metersphere/Application.java b/src/main/java/io/metersphere/Application.java index 010e94c..5212b82 100644 --- a/src/main/java/io/metersphere/Application.java +++ b/src/main/java/io/metersphere/Application.java @@ -1,13 +1,28 @@ package io.metersphere; - +import io.metersphere.api.jmeter.utils.CommonBeanFactory; +import io.metersphere.api.jmeter.utils.JmeterProperties; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.ServletComponentScan; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.PropertySource; +@ServletComponentScan +@EnableConfigurationProperties({ + JmeterProperties.class, +}) +@PropertySource(value = {"file:/opt/metersphere/conf/metersphere.properties"}, encoding = "UTF-8", ignoreResourceNotFound = true) @SpringBootApplication public class Application { - public static void main(String[] args) { SpringApplication.run(Application.class, args); } + @Bean + @ConditionalOnMissingBean + public CommonBeanFactory commonBeanFactory() { + return new CommonBeanFactory(); + } } diff --git a/src/main/java/io/metersphere/api/config/AppStartListener.java b/src/main/java/io/metersphere/api/config/AppStartListener.java new file mode 100644 index 0000000..6d2e6db --- /dev/null +++ b/src/main/java/io/metersphere/api/config/AppStartListener.java @@ -0,0 +1,65 @@ +package io.metersphere.api.config; + +import io.metersphere.api.jmeter.JMeterService; +import io.metersphere.api.jmeter.utils.FileUtils; +import io.metersphere.api.jmeter.utils.MSException; +import io.metersphere.node.util.LogUtil; +import org.apache.jmeter.NewDriver; +import org.python.core.Options; +import org.python.util.PythonInterpreter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.net.MalformedURLException; + +@Component +public class AppStartListener implements ApplicationListener { + + @Resource + private JMeterService jMeterService; + + @Value("${jmeter.home}") + private String jmeterHome; + + @Override + public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) { + System.out.println("================= NODE 应用启动 START ================="); + System.setProperty("jmeter.home", jmeterHome); + loadJars(); + initPythonEnv(); + } + + /** + * 解决接口测试-无法导入内置python包 + */ + private void initPythonEnv() { + //解决无法加载 PyScriptEngineFactory + Options.importSite = false; + try { + PythonInterpreter interp = new PythonInterpreter(); + String path = jMeterService.getJmeterHome(); + System.out.println("sys.path: " + path); + path += "/lib/ext/jython-standalone.jar/Lib"; + interp.exec("import sys"); + interp.exec("sys.path.append(\"" + path + "\")"); + } catch (Exception e) { + e.printStackTrace(); + LogUtil.error(e.getMessage(), e); + } + } + + /** + * 加载jar包 + */ + private void loadJars() { + try { + NewDriver.addPath(FileUtils.JAR_FILE_DIR); + } catch (MalformedURLException e) { + LogUtil.error(e.getMessage(), e); + MSException.throwException(e.getMessage()); + } + } +} diff --git a/src/main/java/io/metersphere/api/config/KafkaConfig.java b/src/main/java/io/metersphere/api/config/KafkaConfig.java new file mode 100644 index 0000000..3dac81f --- /dev/null +++ b/src/main/java/io/metersphere/api/config/KafkaConfig.java @@ -0,0 +1,8 @@ +package io.metersphere.api.config; +public class KafkaConfig { + // 执行内容监听 + public final static String EXEC_TOPIC = "ms-automation-exec-topic"; + //执行结果回传 + public static final String TOPICS = "ms-api-exec-topic"; + +} diff --git a/src/main/java/io/metersphere/api/controller/ConsumerController.java b/src/main/java/io/metersphere/api/controller/ConsumerController.java new file mode 100644 index 0000000..6cf2981 --- /dev/null +++ b/src/main/java/io/metersphere/api/controller/ConsumerController.java @@ -0,0 +1,77 @@ +package io.metersphere.api.controller; + +import io.metersphere.api.service.MsKafkaListener; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Scope; +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; +import org.springframework.kafka.config.KafkaListenerEndpointRegistry; +import org.springframework.kafka.core.DefaultKafkaConsumerFactory; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.Map; + +@RestController +@RequestMapping("consumer") +public class ConsumerController { + /** + * 应用程序上行文 + */ + @Resource + ApplicationContext context; + /** + * 监听器容器工厂 + */ + @Resource + ConcurrentKafkaListenerContainerFactory containerFactory; + /** + * 所有@KafkaListener这个注解所标注的方法都会被注册在这里面中 + */ + @Resource + KafkaListenerEndpointRegistry registry; + + private MsKafkaListener listener; + + /** + * 创建消费者分组 + */ + @PostMapping("/create") + public void create(@RequestBody Map consumerProps) { + try { + if (consumerProps != null && consumerProps.size() > 0 && listener == null) { + consumerProps.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false); + consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, + "org.apache.kafka.common.serialization.StringDeserializer"); + consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, + "org.apache.kafka.common.serialization.StringDeserializer"); + // 设置监听器容器工厂 + containerFactory.setConsumerFactory(new DefaultKafkaConsumerFactory<>(consumerProps)); + // 获取监听类实例 + listener = context.getBean(MsKafkaListener.class); + } + } catch (Exception e) { + } + } + + /** + * 停止所有消费监听 + */ + @GetMapping("/stop") + public void stop() { + registry.getListenerContainers().forEach(container -> { + container.stop(); + }); + } + + /** + * 获取监听类实例 + */ + @Bean + @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) + public MsKafkaListener listener() { + return new MsKafkaListener(); + } +} diff --git a/src/main/java/io/metersphere/api/controller/JmeterExecuteController.java b/src/main/java/io/metersphere/api/controller/JmeterExecuteController.java new file mode 100644 index 0000000..ffcdab3 --- /dev/null +++ b/src/main/java/io/metersphere/api/controller/JmeterExecuteController.java @@ -0,0 +1,57 @@ +package io.metersphere.api.controller; + +import com.alibaba.fastjson.JSON; +import io.metersphere.api.controller.request.RunRequest; +import io.metersphere.api.jmeter.LocalRunner; +import io.metersphere.api.module.JvmInfo; +import io.metersphere.api.service.JmeterExecuteService; +import io.metersphere.api.service.JvmService; +import io.metersphere.node.util.LogUtil; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import java.util.List; + +@RestController +@RequestMapping("/jmeter") +public class JmeterExecuteController { + + @Resource + private JmeterExecuteService jmeterExecuteService; + + @PostMapping(value = "/api/run", consumes = {"multipart/form-data"}) + public String apiRun(@RequestParam(value = "files") MultipartFile[] bodyFiles, @RequestParam(value = "jarFiles") MultipartFile[] jarFiles, String request) { + LogUtil.info("接收到测试请求 start "); + RunRequest runRequest = JSON.parseObject(request, RunRequest.class); + return jmeterExecuteService.run(runRequest, bodyFiles, jarFiles); + } + + @PostMapping(value = "/api/start") + public String apiStartRun(@RequestBody RunRequest runRequest) { + System.out.println("接收到测试请求: " + JSON.toJSONString(runRequest)); + return jmeterExecuteService.runStart(runRequest); + } + + @GetMapping("/status") + public String getStatus() { + return "OK"; + } + + + @GetMapping("/getJvmInfo") + public JvmInfo getJvmInfo() { + return JvmService.jvmInfo(); + } + + @GetMapping("/getRunning/{key}") + public Integer getRunning(@PathVariable String key) { + return jmeterExecuteService.getRunningTasks(key); + } + + @GetMapping("/stop") + public void stop(@RequestBody List keys) { + new LocalRunner().stop(keys); + } + +} diff --git a/src/main/java/io/metersphere/api/controller/ProducerController.java b/src/main/java/io/metersphere/api/controller/ProducerController.java new file mode 100644 index 0000000..3d88387 --- /dev/null +++ b/src/main/java/io/metersphere/api/controller/ProducerController.java @@ -0,0 +1,20 @@ +package io.metersphere.api.controller; + +import io.metersphere.api.service.ProducerService; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.Map; + +@RestController +@RequestMapping("producer") +public class ProducerController { + @Resource + private ProducerService producerService; + + @PostMapping("/create") + public String create(@RequestBody Map producerProps) { + return producerService.init(producerProps); + } +} + diff --git a/src/main/java/io/metersphere/api/controller/request/RunRequest.java b/src/main/java/io/metersphere/api/controller/request/RunRequest.java new file mode 100644 index 0000000..6efb04c --- /dev/null +++ b/src/main/java/io/metersphere/api/controller/request/RunRequest.java @@ -0,0 +1,19 @@ +package io.metersphere.api.controller.request; + +import io.metersphere.api.jmeter.utils.RunModeConfig; +import lombok.Data; + +@Data +public class RunRequest { + private String testId; + // api / case 或有这个属性值 + private String reportId; + private String url; + private String userId; + private boolean isDebug; + private String runMode; + private String jmx; + // 集成报告ID + private String amassReport; + private RunModeConfig config; +} diff --git a/src/main/java/io/metersphere/api/jmeter/APIBackendListenerClient.java b/src/main/java/io/metersphere/api/jmeter/APIBackendListenerClient.java new file mode 100644 index 0000000..46d400b --- /dev/null +++ b/src/main/java/io/metersphere/api/jmeter/APIBackendListenerClient.java @@ -0,0 +1,269 @@ +package io.metersphere.api.jmeter; + +import com.alibaba.fastjson.JSON; +import io.metersphere.api.jmeter.constants.ApiRunMode; +import io.metersphere.api.jmeter.constants.RequestType; +import io.metersphere.api.jmeter.utils.CommonBeanFactory; +import io.metersphere.api.jmeter.utils.MessageCache; +import io.metersphere.api.module.*; +import io.metersphere.api.service.JmeterExecuteService; +import io.metersphere.api.service.ProducerService; +import io.metersphere.node.util.LogUtil; +import org.apache.commons.lang3.StringUtils; +import org.apache.dubbo.common.utils.CollectionUtils; +import org.apache.jmeter.assertions.AssertionResult; +import org.apache.jmeter.protocol.http.sampler.HTTPSampleResult; +import org.apache.jmeter.samplers.SampleResult; +import org.apache.jmeter.visualizers.backend.AbstractBackendListenerClient; +import org.apache.jmeter.visualizers.backend.BackendListenerContext; +import org.springframework.http.HttpMethod; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.io.Serializable; +import java.util.*; + +/** + * JMeter BackendListener扩展, jmx脚本中使用 + */ +public class APIBackendListenerClient extends AbstractBackendListenerClient implements Serializable { + + public final static String TEST_ID = "ms.test.id"; + + public final static String TEST_REPORT_ID = "ms.test.report.name"; + + public final static String AMASS_REPORT = "ms.test.amass.report.id"; + + private final static String THREAD_SPLIT = " "; + + private final static String ID_SPLIT = "-"; + + private final List queue = new ArrayList<>(); + + private String runMode = ApiRunMode.RUN.name(); + + private String userId; + + private boolean isDebug; + + private JmeterExecuteService jmeterExecuteService; + private ProducerService producerServer; + /** + * 测试ID + */ + private String testId; + + /** + * 只有合并报告是这个有值 + */ + private String setReportId; + + private String amassReport; + /** + * 获得控制台内容 + */ + private PrintStream oldPrintStream = System.out; + private ByteArrayOutputStream bos = new ByteArrayOutputStream(); + + private void setConsole() { + // 设置新的out + System.setOut(new PrintStream(bos)); + } + + private String getConsole() { + System.setOut(oldPrintStream); + return bos.toString(); + } + + @Override + public void setupTest(BackendListenerContext context) throws Exception { + setConsole(); + setParam(context); + super.setupTest(context); + } + + + @Override + public void handleSampleResults(List sampleResults, BackendListenerContext context) { + queue.addAll(sampleResults); + } + + @Override + public void teardownTest(BackendListenerContext context) throws Exception { + TestResult testResult = new TestResult(); + testResult.setTestId(testId); + testResult.setTotal(queue.size()); + testResult.setSetReportId(this.setReportId); + testResult.setDebug(this.isDebug); + testResult.setUserId(this.userId); + testResult.setConsole(getConsole()); + jmeterExecuteService = CommonBeanFactory.getBean(JmeterExecuteService.class); + producerServer = CommonBeanFactory.getBean(ProducerService.class); + try { + if (StringUtils.isNotEmpty(testId) && !MessageCache.runningEngine.isEmpty()) { + MessageCache.runningEngine.remove(testId); + } + if (StringUtils.isNotEmpty(setReportId) && !MessageCache.runningEngine.isEmpty()) { + MessageCache.runningEngine.remove(setReportId); + } + // 一个脚本里可能包含多个场景(ThreadGroup),所以要区分开,key: 场景Id + final Map scenarios = new LinkedHashMap<>(); + queue.forEach(result -> { + // 线程名称: <场景名> <场景Index>-<请求Index>, 例如:Scenario 2-1 + if (StringUtils.equals(result.getSampleLabel(), "RunningDebugSampler")) { + testResult.setRunningDebugSampler(result.getResponseDataAsString()); + } else { + String scenarioName = StringUtils.substringBeforeLast(result.getThreadName(), THREAD_SPLIT); + String index = StringUtils.substringAfterLast(result.getThreadName(), THREAD_SPLIT); + String scenarioId = StringUtils.substringBefore(index, ID_SPLIT); + ScenarioResult scenarioResult; + if (!scenarios.containsKey(scenarioId)) { + scenarioResult = new ScenarioResult(); + try { + scenarioResult.setId(Integer.parseInt(scenarioId)); + } catch (Exception e) { + scenarioResult.setId(0); + LogUtil.error("场景ID转换异常: " + e.getMessage()); + } + scenarioResult.setName(scenarioName); + scenarios.put(scenarioId, scenarioResult); + } else { + scenarioResult = scenarios.get(scenarioId); + } + + if (result.isSuccessful()) { + scenarioResult.addSuccess(); + testResult.addSuccess(); + } else { + scenarioResult.addError(result.getErrorCount()); + testResult.addError(result.getErrorCount()); + } + + RequestResult requestResult = getRequestResult(result); + scenarioResult.getRequestResults().add(requestResult); + scenarioResult.addResponseTime(result.getTime()); + + testResult.addPassAssertions(requestResult.getPassAssertions()); + testResult.addTotalAssertions(requestResult.getTotalAssertions()); + + scenarioResult.addPassAssertions(requestResult.getPassAssertions()); + scenarioResult.addTotalAssertions(requestResult.getTotalAssertions()); + } + }); + testResult.getScenarios().addAll(scenarios.values()); + testResult.getScenarios().sort(Comparator.comparing(ScenarioResult::getId)); + testResult.setRunMode(this.runMode); + } catch (Exception e) { + LogUtil.error("处理执行数据异常:" + e.getMessage()); + } + // 推送执行结果 + producerServer.send(JSON.toJSONString(testResult)); + jmeterExecuteService.remove(amassReport, testId); + queue.clear(); + super.teardownTest(context); + } + + private RequestResult getRequestResult(SampleResult result) { + RequestResult requestResult = new RequestResult(); + requestResult.setId(result.getSamplerId()); + requestResult.setResourceId(result.getResourceId()); + requestResult.setName(result.getSampleLabel()); + requestResult.setUrl(result.getUrlAsString()); + requestResult.setMethod(getMethod(result)); + requestResult.setBody(result.getSamplerData()); + requestResult.setHeaders(result.getRequestHeaders()); + requestResult.setRequestSize(result.getSentBytes()); + requestResult.setStartTime(result.getStartTime()); + requestResult.setEndTime(result.getEndTime()); + requestResult.setTotalAssertions(result.getAssertionResults().length); + requestResult.setSuccess(result.isSuccessful()); + requestResult.setError(result.getErrorCount()); + requestResult.setScenario(result.getScenario()); + if (result instanceof HTTPSampleResult) { + HTTPSampleResult res = (HTTPSampleResult) result; + requestResult.setCookies(res.getCookies()); + } + + for (SampleResult subResult : result.getSubResults()) { + requestResult.getSubRequestResults().add(getRequestResult(subResult)); + } + ResponseResult responseResult = requestResult.getResponseResult(); + responseResult.setBody(result.getResponseDataAsString()); + responseResult.setHeaders(result.getResponseHeaders()); + responseResult.setLatency(result.getLatency()); + responseResult.setResponseCode(result.getResponseCode()); + responseResult.setResponseSize(result.getResponseData().length); + responseResult.setResponseTime(result.getTime()); + responseResult.setResponseMessage(result.getResponseMessage()); + if (JMeterVars.get(result.hashCode()) != null && CollectionUtils.isNotEmpty(JMeterVars.get(result.hashCode()).entrySet())) { + StringBuilder builder = new StringBuilder(); + for (Map.Entry entry : JMeterVars.get(result.hashCode()).entrySet()) { + builder.append(entry.getKey()).append(":").append(entry.getValue()).append("\n"); + } + if (StringUtils.isNotEmpty(builder)) { + responseResult.setVars(builder.toString()); + } + JMeterVars.remove(result.hashCode()); + } + for (AssertionResult assertionResult : result.getAssertionResults()) { + ResponseAssertionResult responseAssertionResult = getResponseAssertionResult(assertionResult); + if (responseAssertionResult.isPass()) { + requestResult.addPassAssertions(); + } + //xpath 提取错误会添加断言错误 + if (StringUtils.isBlank(responseAssertionResult.getMessage()) || + (StringUtils.isNotBlank(responseAssertionResult.getName()) && !responseAssertionResult.getName().endsWith("XPath2Extractor"))) { + responseResult.getAssertions().add(responseAssertionResult); + } + } + return requestResult; + } + + private String getMethod(SampleResult result) { + String body = result.getSamplerData(); + // Dubbo Protocol + String start = "RPC Protocol: "; + String end = "://"; + if (StringUtils.contains(body, start)) { + String protocol = StringUtils.substringBetween(body, start, end); + if (StringUtils.isNotEmpty(protocol)) { + return protocol.toUpperCase(); + } + return RequestType.DUBBO; + } else if (StringUtils.contains(result.getResponseHeaders(), "url:jdbc")) { + return "SQL"; + } else { + // Http Method + String method = StringUtils.substringBefore(body, " "); + for (HttpMethod value : HttpMethod.values()) { + if (StringUtils.equals(method, value.name())) { + return method; + } + } + return "Request"; + } + } + + private void setParam(BackendListenerContext context) { + this.testId = context.getParameter(TEST_ID); + this.setReportId = context.getParameter(TEST_REPORT_ID); + this.amassReport = context.getParameter(AMASS_REPORT); + this.runMode = context.getParameter("runMode"); + this.isDebug = StringUtils.equals(context.getParameter("DEBUG"), "DEBUG") ? true : false; + this.userId = context.getParameter("USER_ID"); + if (StringUtils.isBlank(this.runMode)) { + this.runMode = ApiRunMode.RUN.name(); + } + } + + private ResponseAssertionResult getResponseAssertionResult(AssertionResult assertionResult) { + ResponseAssertionResult responseAssertionResult = new ResponseAssertionResult(); + responseAssertionResult.setName(assertionResult.getName()); + responseAssertionResult.setPass(!assertionResult.isFailure() && !assertionResult.isError()); + if (!responseAssertionResult.isPass()) { + responseAssertionResult.setMessage(assertionResult.getFailureMessage()); + } + return responseAssertionResult; + } + +} diff --git a/src/main/java/io/metersphere/api/jmeter/JMeterService.java b/src/main/java/io/metersphere/api/jmeter/JMeterService.java new file mode 100644 index 0000000..72d376b --- /dev/null +++ b/src/main/java/io/metersphere/api/jmeter/JMeterService.java @@ -0,0 +1,98 @@ +package io.metersphere.api.jmeter; + +import io.metersphere.api.controller.request.RunRequest; +import io.metersphere.api.jmeter.constants.ApiRunMode; +import io.metersphere.api.jmeter.utils.JmeterProperties; +import io.metersphere.api.jmeter.utils.MSException; +import io.metersphere.node.util.LogUtil; +import org.apache.commons.lang3.StringUtils; +import org.apache.jmeter.config.Arguments; +import org.apache.jmeter.util.JMeterUtils; +import org.apache.jmeter.visualizers.backend.BackendListener; +import org.apache.jorphan.collections.HashTree; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.io.File; +import java.lang.reflect.Field; + +@Service +public class JMeterService { + + @Resource + private JmeterProperties jmeterProperties; + + @PostConstruct + public void init() { + String JMETER_HOME = getJmeterHome(); + + String JMETER_PROPERTIES = JMETER_HOME + "/bin/jmeter.properties"; + JMeterUtils.loadJMeterProperties(JMETER_PROPERTIES); + JMeterUtils.setJMeterHome(JMETER_HOME); + JMeterUtils.setLocale(LocaleContextHolder.getLocale()); + } + + public String getJmeterHome() { + String home = getClass().getResource("/").getPath() + "jmeter"; + try { + File file = new File(home); + if (file.exists()) { + return home; + } else { + return jmeterProperties.getHome(); + } + } catch (Exception e) { + return jmeterProperties.getHome(); + } + } + + public static HashTree getHashTree(Object scriptWrapper) throws Exception { + Field field = scriptWrapper.getClass().getDeclaredField("testPlan"); + field.setAccessible(true); + return (HashTree) field.get(scriptWrapper); + } + + + private void addBackendListener(HashTree testPlan, RunRequest request) { + BackendListener backendListener = new BackendListener(); + if (StringUtils.isNotEmpty(request.getReportId())) { + backendListener.setName(request.getReportId()); + } else { + backendListener.setName(request.getTestId()); + } + Arguments arguments = new Arguments(); + if (StringUtils.isNotEmpty(request.getAmassReport())) { + arguments.addArgument(APIBackendListenerClient.AMASS_REPORT, request.getAmassReport()); + } + if (request.getConfig() != null && request.getConfig().getMode().equals("serial") && request.getConfig().getReportType().equals("setReport")) { + arguments.addArgument(APIBackendListenerClient.TEST_REPORT_ID, request.getConfig().getReportName()); + } + if (StringUtils.isNotEmpty(request.getReportId()) && ApiRunMode.API_PLAN.name().equals(request.getRunMode())) { + arguments.addArgument(APIBackendListenerClient.TEST_ID, request.getReportId()); + } else { + arguments.addArgument(APIBackendListenerClient.TEST_ID, request.getTestId()); + } + if (StringUtils.isNotBlank(request.getRunMode())) { + arguments.addArgument("runMode", request.getRunMode()); + } + arguments.addArgument("DEBUG", request.isDebug() ? "DEBUG" : "RUN"); + arguments.addArgument("USER_ID", request.getUserId()); + backendListener.setArguments(arguments); + backendListener.setClassname(APIBackendListenerClient.class.getCanonicalName()); + testPlan.add(testPlan.getArray()[0], backendListener); + } + + public void run(RunRequest request, HashTree testPlan) { + try { + init(); + addBackendListener(testPlan, request); + LocalRunner runner = new LocalRunner(testPlan); + runner.run(request.getReportId()); + } catch (Exception e) { + LogUtil.error(e.getMessage(), e); + MSException.throwException("读取脚本失败"); + } + } +} diff --git a/src/main/java/io/metersphere/api/jmeter/JMeterVars.java b/src/main/java/io/metersphere/api/jmeter/JMeterVars.java new file mode 100644 index 0000000..ae468d3 --- /dev/null +++ b/src/main/java/io/metersphere/api/jmeter/JMeterVars.java @@ -0,0 +1,115 @@ +package io.metersphere.api.jmeter; + +import com.alibaba.fastjson.JSON; +import io.github.ningyu.jmeter.plugin.dubbo.sample.DubboSample; +import org.apache.dubbo.common.utils.CollectionUtils; +import org.apache.jmeter.extractor.JSR223PostProcessor; +import org.apache.jmeter.extractor.RegexExtractor; +import org.apache.jmeter.extractor.XPath2Extractor; +import org.apache.jmeter.extractor.json.jsonpath.JSONPostProcessor; +import org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy; +import org.apache.jmeter.protocol.jdbc.sampler.JDBCSampler; +import org.apache.jmeter.threads.JMeterVariables; +import org.apache.jorphan.collections.HashTree; +import org.springframework.util.StringUtils; + +import java.util.*; + +public class JMeterVars { + + private JMeterVars() { + } + + /** + * 数据和线程变量保持一致 + */ + private static Map variables = new HashMap<>(); + + /** + * 线程执行过程调用提取变量值 + * + * @param testId + * @param vars + * @param extract + */ + public static void addVars(Integer testId, JMeterVariables vars, String extract) { + JMeterVariables vs = variables.get(testId); + if (vs == null) { + vs = new JMeterVariables(); + } + if (!StringUtils.isEmpty(extract) && vars != null) { + List extracts = Arrays.asList(extract.split(";")); + if (CollectionUtils.isNotEmpty(extracts)) { + for (String item : extracts) { + String nrKey = item + "_matchNr"; + Object nr = vars.get(nrKey); + if (nr != null) { + int nrv = 0; + try { + nrv = Integer.valueOf(String.valueOf(nr)); + } catch (Exception e) { + } + if (nrv > 0) { + List data = new ArrayList<>(); + for (int i = 1; i < nrv + 1; i++) { + data.add(vars.get(item + "_" + i)); + } + String array = JSON.toJSONString(data); + vars.put(item, array); + } + } + vs.put(item, vars.get(item) == null ? "" : vars.get(item)); + } + vs.remove("TESTSTART.MS"); // 标示变量移除 + } + } + + variables.put(testId, vs); + } + + /** + * 处理所有请求,有提取变量的请求增加后置脚本提取变量值 + * + * @param tree + */ + public static void addJSR223PostProcessor(HashTree tree) { + for (Object key : tree.keySet()) { + HashTree node = tree.get(key); + if (key instanceof HTTPSamplerProxy || key instanceof DubboSample || key instanceof JDBCSampler) { + StringJoiner extract = new StringJoiner(";"); + for (Object child : node.keySet()) { + if (child instanceof RegexExtractor) { + RegexExtractor regexExtractor = (RegexExtractor) child; + extract.add(regexExtractor.getRefName()); + } else if (child instanceof XPath2Extractor) { + XPath2Extractor regexExtractor = (XPath2Extractor) child; + extract.add(regexExtractor.getRefName()); + } else if (child instanceof JSONPostProcessor) { + JSONPostProcessor regexExtractor = (JSONPostProcessor) child; + extract.add(regexExtractor.getRefNames()); + } + } + + if (Optional.ofNullable(extract).orElse(extract).length() > 0) { + JSR223PostProcessor shell = new JSR223PostProcessor(); + shell.setEnabled(true); + shell.setProperty("script", "io.metersphere.api.jmeter.JMeterVars.addVars(prev.hashCode(),vars," + "\"" + extract.toString() + "\"" + ");"); + node.add(shell); + } + } + + if (node != null) { + addJSR223PostProcessor(node); + } + } + } + + public static JMeterVariables get(Integer key) { + return variables.get(key); + } + + public static void remove(Integer key) { + variables.remove(key); + } + +} diff --git a/src/main/java/io/metersphere/api/jmeter/LocalRunner.java b/src/main/java/io/metersphere/api/jmeter/LocalRunner.java new file mode 100644 index 0000000..60ab90a --- /dev/null +++ b/src/main/java/io/metersphere/api/jmeter/LocalRunner.java @@ -0,0 +1,48 @@ +package io.metersphere.api.jmeter; + +import io.metersphere.api.jmeter.utils.MessageCache; +import io.metersphere.node.util.LogUtil; +import org.apache.commons.collections.CollectionUtils; +import org.apache.jmeter.engine.JMeterEngineException; +import org.apache.jmeter.engine.StandardJMeterEngine; +import org.apache.jorphan.collections.HashTree; + +import java.util.List; + +public class LocalRunner { + private HashTree jmxTree; + + public LocalRunner(HashTree jmxTree) { + this.jmxTree = jmxTree; + } + + public LocalRunner() { + } + + public void run(String report) { + StandardJMeterEngine engine = new StandardJMeterEngine(); + engine.configure(jmxTree); + try { + engine.runTest(); + MessageCache.runningEngine.put(report, engine); + } catch (JMeterEngineException e) { + engine.stopTest(true); + } + } + + public void stop(List reports) { + try { + if (CollectionUtils.isNotEmpty(reports)) { + for (String report : reports) { + StandardJMeterEngine engine = MessageCache.runningEngine.get(report); + if (engine != null) { + engine.stopTest(); + MessageCache.runningEngine.remove(report); + } + } + } + } catch (Exception e) { + LogUtil.error(e); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/metersphere/api/jmeter/constants/ApiRunMode.java b/src/main/java/io/metersphere/api/jmeter/constants/ApiRunMode.java new file mode 100644 index 0000000..2fcccaf --- /dev/null +++ b/src/main/java/io/metersphere/api/jmeter/constants/ApiRunMode.java @@ -0,0 +1,5 @@ +package io.metersphere.api.jmeter.constants; + +public enum ApiRunMode { + RUN, DEBUG, DEFINITION, SCENARIO, API_PLAN, JENKINS_API_PLAN, JENKINS, SCENARIO_PLAN, API, SCHEDULE_API_PLAN, SCHEDULE_SCENARIO,SCHEDULE_SCENARIO_PLAN, SCHEDULE_PERFORMANCE_TEST +} diff --git a/src/main/java/io/metersphere/api/jmeter/constants/RequestType.java b/src/main/java/io/metersphere/api/jmeter/constants/RequestType.java new file mode 100644 index 0000000..377746b --- /dev/null +++ b/src/main/java/io/metersphere/api/jmeter/constants/RequestType.java @@ -0,0 +1,12 @@ +package io.metersphere.api.jmeter.constants; + +public class RequestType { + + public static final String HTTP = "HTTP"; + + public static final String DUBBO = "DUBBO"; + + public static final String SQL = "SQL"; + + public static final String TCP = "TCP"; +} diff --git a/src/main/java/io/metersphere/api/jmeter/utils/CommonBeanFactory.java b/src/main/java/io/metersphere/api/jmeter/utils/CommonBeanFactory.java new file mode 100644 index 0000000..edf156f --- /dev/null +++ b/src/main/java/io/metersphere/api/jmeter/utils/CommonBeanFactory.java @@ -0,0 +1,34 @@ +package io.metersphere.api.jmeter.utils; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +public class CommonBeanFactory implements ApplicationContextAware { + private static ApplicationContext context; + + public CommonBeanFactory() { + } + + public void setApplicationContext(ApplicationContext ctx) throws BeansException { + context = ctx; + } + + public static Object getBean(String beanName) { + try { + return context != null && !StringUtils.isBlank(beanName) ? context.getBean(beanName) : null; + } catch (BeansException e) { + return null; + } + } + + public static T getBean(Class className) { + try { + return context != null && className != null ? context.getBean(className) : null; + } catch (BeansException e) { + return null; + } + } +} + diff --git a/src/main/java/io/metersphere/api/jmeter/utils/FileUtils.java b/src/main/java/io/metersphere/api/jmeter/utils/FileUtils.java new file mode 100644 index 0000000..de94ce0 --- /dev/null +++ b/src/main/java/io/metersphere/api/jmeter/utils/FileUtils.java @@ -0,0 +1,38 @@ +package io.metersphere.api.jmeter.utils; + +import io.metersphere.node.util.LogUtil; +import org.aspectj.util.FileUtil; +import org.springframework.web.multipart.MultipartFile; + +import java.io.*; + +public class FileUtils { + public static final String BODY_FILE_DIR = "/opt/metersphere/data/body"; + public static final String JAR_FILE_DIR = "/opt/metersphere/data/node/jar"; + + public static void createFiles(MultipartFile[] bodyFiles, String path) { + if (bodyFiles != null && bodyFiles.length > 0) { + File testDir = new File(path); + if (!testDir.exists()) { + testDir.mkdirs(); + } + for (int i = 0; i < bodyFiles.length; i++) { + MultipartFile item = bodyFiles[i]; + File file = new File(path + "/" + item.getOriginalFilename()); + try (InputStream in = item.getInputStream(); OutputStream out = new FileOutputStream(file)) { + file.createNewFile(); + FileUtil.copyStream(in, out); + } catch (IOException e) { + LogUtil.error(e); + MSException.throwException("文件处理异常"); + } + } + } + } + public static void deleteFile(String path) { + File file = new File(path); + if (file.exists()) { + file.delete(); + } + } +} diff --git a/src/main/java/io/metersphere/api/jmeter/utils/JmeterProperties.java b/src/main/java/io/metersphere/api/jmeter/utils/JmeterProperties.java new file mode 100644 index 0000000..9fd5eaa --- /dev/null +++ b/src/main/java/io/metersphere/api/jmeter/utils/JmeterProperties.java @@ -0,0 +1,21 @@ +package io.metersphere.api.jmeter.utils; + + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = JmeterProperties.JMETER_PREFIX) +@Setter +@Getter +public class JmeterProperties { + + public static final String JMETER_PREFIX = "jmeter"; + + private String image; + + private String home; + + private String heap = "-Xms1g -Xmx1g -XX:MaxMetaspaceSize=256m"; + private String gcAlgo = "-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:G1ReservePercent=20"; +} diff --git a/src/main/java/io/metersphere/api/jmeter/utils/MSException.java b/src/main/java/io/metersphere/api/jmeter/utils/MSException.java new file mode 100644 index 0000000..688df74 --- /dev/null +++ b/src/main/java/io/metersphere/api/jmeter/utils/MSException.java @@ -0,0 +1,24 @@ +package io.metersphere.api.jmeter.utils; + +public class MSException extends RuntimeException { + + private MSException(String message) { + super(message); + } + + private MSException(Throwable t) { + super(t); + } + + public static void throwException(String message) { + throw new MSException(message); + } + + public static MSException getException(String message) { + throw new MSException(message); + } + + public static void throwException(Throwable t) { + throw new MSException(t); + } +} diff --git a/src/main/java/io/metersphere/api/jmeter/utils/MessageCache.java b/src/main/java/io/metersphere/api/jmeter/utils/MessageCache.java new file mode 100644 index 0000000..ee0cd9d --- /dev/null +++ b/src/main/java/io/metersphere/api/jmeter/utils/MessageCache.java @@ -0,0 +1,10 @@ +package io.metersphere.api.jmeter.utils; + +import org.apache.jmeter.engine.StandardJMeterEngine; + +import java.util.concurrent.ConcurrentHashMap; + +public class MessageCache { + public static ConcurrentHashMap runningEngine = new ConcurrentHashMap<>(); + +} diff --git a/src/main/java/io/metersphere/api/jmeter/utils/RunModeConfig.java b/src/main/java/io/metersphere/api/jmeter/utils/RunModeConfig.java new file mode 100644 index 0000000..8330e8a --- /dev/null +++ b/src/main/java/io/metersphere/api/jmeter/utils/RunModeConfig.java @@ -0,0 +1,11 @@ +package io.metersphere.api.jmeter.utils; + +import lombok.Data; + +@Data +public class RunModeConfig { + private String mode; + private String reportType; + private String reportName; + private boolean onSampleError; +} diff --git a/src/main/java/io/metersphere/api/module/JvmInfo.java b/src/main/java/io/metersphere/api/module/JvmInfo.java new file mode 100644 index 0000000..05c8dab --- /dev/null +++ b/src/main/java/io/metersphere/api/module/JvmInfo.java @@ -0,0 +1,35 @@ +package io.metersphere.api.module; + +import lombok.Data; + +@Data +public class JvmInfo { + /** + * JVM内存的空闲空间为 + */ + private long vmFree; + /** + * JVM内存已用的空间为 + */ + private long vmUse; + + private long vmTotal; + /** + * JVM总内存空间为 + */ + private long vmMax; + + private int totalThread; + + public JvmInfo() { + + } + + public JvmInfo(long vmTotal, long vmFree, long vmMax, long vmUse, int totalThread) { + this.vmFree = vmFree; + this.vmTotal = vmTotal; + this.vmMax = vmMax; + this.vmUse = vmUse; + this.totalThread = totalThread; + } +} diff --git a/src/main/java/io/metersphere/api/module/RequestResult.java b/src/main/java/io/metersphere/api/module/RequestResult.java new file mode 100644 index 0000000..3732394 --- /dev/null +++ b/src/main/java/io/metersphere/api/module/RequestResult.java @@ -0,0 +1,51 @@ +package io.metersphere.api.module; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class RequestResult { + // 请求ID + private String id; + + private String resourceId; + + private String name; + + private String url; + + private String method; + + private String scenario; + + private long requestSize; + + private long startTime; + + private long endTime; + + private int error; + + private boolean success; + + private String headers; + + private String cookies; + + private String body; + + private int totalAssertions = 0; + + private int passAssertions = 0; + + private final List subRequestResults = new ArrayList<>(); + + private final ResponseResult responseResult = new ResponseResult(); + + public void addPassAssertions() { + this.passAssertions++; + } + +} diff --git a/src/main/java/io/metersphere/api/module/ResponseAssertionResult.java b/src/main/java/io/metersphere/api/module/ResponseAssertionResult.java new file mode 100644 index 0000000..f75904c --- /dev/null +++ b/src/main/java/io/metersphere/api/module/ResponseAssertionResult.java @@ -0,0 +1,13 @@ +package io.metersphere.api.module; + +import lombok.Data; + +@Data +public class ResponseAssertionResult { + + private String name; + + private String message; + + private boolean pass; +} diff --git a/src/main/java/io/metersphere/api/module/ResponseResult.java b/src/main/java/io/metersphere/api/module/ResponseResult.java new file mode 100644 index 0000000..f1ecd30 --- /dev/null +++ b/src/main/java/io/metersphere/api/module/ResponseResult.java @@ -0,0 +1,32 @@ +package io.metersphere.api.module; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + + +@Data +public class ResponseResult { + + private String responseCode; + + private String responseMessage; + + private long responseTime; + + private long latency; + + private long responseSize; + + private String headers; + + private String body; + + private String vars; + + private String console; + + private final List assertions = new ArrayList<>(); + +} diff --git a/src/main/java/io/metersphere/api/module/ScenarioResult.java b/src/main/java/io/metersphere/api/module/ScenarioResult.java new file mode 100644 index 0000000..d388e3f --- /dev/null +++ b/src/main/java/io/metersphere/api/module/ScenarioResult.java @@ -0,0 +1,50 @@ +package io.metersphere.api.module; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class ScenarioResult { + + private Integer id; + + private String name; + + private long responseTime; + + private int error = 0; + + private int success = 0; + + private int totalAssertions = 0; + + private int passAssertions = 0; + + private List requestResults = new ArrayList<>(); + + public void addResponseTime(long time) { + this.responseTime += time; + } + + public void addError(int count) { + this.error += count; + } + + public void addSuccess() { + this.success++; + } + + public void addTotalAssertions(int count) { + this.totalAssertions += count; + } + + public void addPassAssertions(int count) { + this.passAssertions += count; + } + + public int getTotal() { + return error + success; + } +} diff --git a/src/main/java/io/metersphere/api/module/TestResult.java b/src/main/java/io/metersphere/api/module/TestResult.java new file mode 100644 index 0000000..c0d48e4 --- /dev/null +++ b/src/main/java/io/metersphere/api/module/TestResult.java @@ -0,0 +1,63 @@ +package io.metersphere.api.module; + +import lombok.Data; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class TestResult { + + private String testId; + + private String setReportId; + + private int scenarioTotal; + + private int scenarioSuccess; + + private int scenarioError; + + private String userId; + + private boolean isDebug; + + private String runMode; + + private int success = 0; + + private int error = 0; + + private int total = 0; + + private int totalAssertions = 0; + + private int passAssertions = 0; + + private String console; + + private String runningDebugSampler; + + private List scenarios = new ArrayList<>(); + + public void addError(int count) { + this.error += count; + } + + public void addSuccess() { + this.success++; + } + + public void addTotalAssertions(int count) { + this.totalAssertions += count; + } + + public void addPassAssertions(int count) { + this.passAssertions += count; + } + + private static final String SEPARATOR = "<->"; + +} diff --git a/src/main/java/io/metersphere/api/service/JmeterExecuteService.java b/src/main/java/io/metersphere/api/service/JmeterExecuteService.java new file mode 100644 index 0000000..8ba4dd4 --- /dev/null +++ b/src/main/java/io/metersphere/api/service/JmeterExecuteService.java @@ -0,0 +1,154 @@ +package io.metersphere.api.service; + +import io.metersphere.api.controller.request.RunRequest; +import io.metersphere.api.jmeter.JMeterService; +import io.metersphere.api.jmeter.utils.FileUtils; +import io.metersphere.api.jmeter.utils.MSException; +import io.metersphere.api.service.utils.ZipSpider; +import io.metersphere.node.util.LogUtil; +import org.apache.commons.lang3.StringUtils; +import org.apache.jmeter.NewDriver; +import org.apache.jmeter.save.SaveService; +import org.apache.jorphan.collections.HashTree; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class JmeterExecuteService { + @Resource + private JMeterService jMeterService; + @Resource + private KafkaTemplate kafkaTemplate; + + private static String url = null; + // 记录所以执行中的请求/场景 + private Map> runningTasks = new HashMap<>(); + + private static InputStream getStrToStream(String sInputString) { + if (StringUtils.isNotEmpty(sInputString)) { + try { + ByteArrayInputStream tInputStringStream = new ByteArrayInputStream(sInputString.getBytes()); + return tInputStringStream; + } catch (Exception ex) { + ex.printStackTrace(); + MSException.throwException("生成脚本异常"); + } + } + return null; + } + + private void loadJar(String path) { + try { + NewDriver.addPath(path); + } catch (MalformedURLException e) { + LogUtil.error(e.getMessage(), e); + MSException.throwException(e.getMessage()); + } + } + + public String run(RunRequest request, MultipartFile[] bodyFiles, MultipartFile[] jarFiles) { + if (request == null || request.getJmx() == null) { + return "执行文件为空,无法执行!"; + } + LogUtil.info(request.getJmx()); + // 生成附件/JAR文件 + FileUtils.createFiles(bodyFiles, FileUtils.BODY_FILE_DIR); + FileUtils.createFiles(jarFiles, FileUtils.JAR_FILE_DIR); + try { + this.loadJar(FileUtils.JAR_FILE_DIR); + // 生成执行脚本 + InputStream inputSource = getStrToStream(request.getJmx()); + Object scriptWrapper = SaveService.loadElement(inputSource); + HashTree testPlan = JMeterService.getHashTree(scriptWrapper); + // 开始执行 + jMeterService.run(request, testPlan); + } catch (Exception e) { + LogUtil.error(e.getMessage()); + return e.getMessage(); + } + return "SUCCESS"; + } + + public String runStart(RunRequest runRequest) { + try { + // 生成附件/JAR文件 + URL urlObject = new URL(runRequest.getUrl()); + String jarUrl = urlObject.getProtocol() + "://" + urlObject.getHost() + (urlObject.getPort() > 0 ? ":" + urlObject.getPort() : "") + "/api/jmeter/download/jar"; + + if (StringUtils.isEmpty(url)) { + LogUtil.info("开始同步上传的第三方JAR:" + jarUrl); + File file = ZipSpider.downloadFile(jarUrl, FileUtils.JAR_FILE_DIR); + if (file != null) { + ZipSpider.unzip(file.getPath(), FileUtils.JAR_FILE_DIR); + this.loadJar(FileUtils.JAR_FILE_DIR); + } + } + url = jarUrl; + LogUtil.info("开始拉取脚本和脚本附件:" + runRequest.getUrl()); + + File bodyFile = ZipSpider.downloadFile(runRequest.getUrl(), FileUtils.BODY_FILE_DIR); + if (bodyFile != null) { + ZipSpider.unzip(bodyFile.getPath(), FileUtils.BODY_FILE_DIR); + File jmxFile = new File(FileUtils.BODY_FILE_DIR + "/" + runRequest.getTestId() + ".jmx"); + // 生成执行脚本 + HashTree testPlan = SaveService.loadTree(jmxFile); + // 开始执行 + jMeterService.run(runRequest, testPlan); + FileUtils.deleteFile(bodyFile.getPath()); + } else { + MSException.throwException("未找到执行的JMX文件"); + } + } catch (Exception e) { + LogUtil.error(e.getMessage()); + return e.getMessage(); + } + return "SUCCESS"; + } + + public void putRunningTasks(String key, String value) { + List list = new ArrayList<>(); + if (this.runningTasks.containsKey(key)) { + list = this.runningTasks.get(key); + } + list.add(value); + this.runningTasks.put(key, list); + } + + public int getRunningTasks(String key) { + if (this.runningTasks.containsKey(key)) { + return this.runningTasks.get(key).size(); + } + return 0; + } + + public void remove(String key, String value) { + if (this.runningTasks.containsKey(key)) { + this.runningTasks.get(key).remove(value); + } + } + + @Scheduled(cron = "0 0/5 * * * ?") + public void execute() { + if (StringUtils.isNotEmpty(url)) { + File file = ZipSpider.downloadFile(url, FileUtils.JAR_FILE_DIR); + if (file != null) { + ZipSpider.unzip(file.getPath(), FileUtils.JAR_FILE_DIR); + this.loadJar(FileUtils.JAR_FILE_DIR); + FileUtils.deleteFile(file.getPath()); + } + } + } +} diff --git a/src/main/java/io/metersphere/api/service/JvmService.java b/src/main/java/io/metersphere/api/service/JvmService.java new file mode 100644 index 0000000..2e470ea --- /dev/null +++ b/src/main/java/io/metersphere/api/service/JvmService.java @@ -0,0 +1,35 @@ +/** + * + */ +package io.metersphere.api.service; + +import io.metersphere.api.module.JvmInfo; + +/** + * @author mr.zhao + */ +public class JvmService { + + // 日志输出 + public static JvmInfo jvmInfo() { + // 虚拟机级内存情况查询 + long vmFree = 0; + long vmUse = 0; + long vmTotal = 0; + long vmMax = 0; + int byteToMb = 1024 * 1024; + Runtime rt = Runtime.getRuntime(); + vmTotal = rt.totalMemory() / byteToMb; + vmFree = rt.freeMemory() / byteToMb; + vmMax = rt.maxMemory() / byteToMb; + vmUse = vmTotal - vmFree; + // 获得线程总数 + ThreadGroup parentThread; + int totalThread = 0; + for (parentThread = Thread.currentThread().getThreadGroup(); parentThread + .getParent() != null; parentThread = parentThread.getParent()) { + totalThread = parentThread.activeCount(); + } + return new JvmInfo(vmTotal, vmFree, vmMax, vmUse, totalThread); + } +} diff --git a/src/main/java/io/metersphere/api/service/MsKafkaListener.java b/src/main/java/io/metersphere/api/service/MsKafkaListener.java new file mode 100644 index 0000000..a945d1c --- /dev/null +++ b/src/main/java/io/metersphere/api/service/MsKafkaListener.java @@ -0,0 +1,39 @@ +package io.metersphere.api.service; +import com.alibaba.fastjson.JSON; +import io.metersphere.api.config.KafkaConfig; +import io.metersphere.api.controller.request.RunRequest; +import io.metersphere.api.jmeter.utils.CommonBeanFactory; +import io.metersphere.node.util.LogUtil; +import org.apache.commons.lang3.StringUtils; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.kafka.support.Acknowledgment; + +public class MsKafkaListener { + + public static final String CONSUME_ID = "ms-api-automation-consume"; + + private JmeterExecuteService jmeterExecuteService; + + @KafkaListener(id = CONSUME_ID, topics = KafkaConfig.EXEC_TOPIC, groupId = "${spring.kafka.consumer.group-id}") + public void consume(ConsumerRecord record, Acknowledgment ack) { + LogUtil.info("接收到执行执行请求开始处理"); + if (jmeterExecuteService == null) { + jmeterExecuteService = CommonBeanFactory.getBean(JmeterExecuteService.class); + } + try { + if (record.value() != null) { + RunRequest request = JSON.parseObject(record.value(), RunRequest.class); + if (StringUtils.isNotEmpty(request.getAmassReport())) { + jmeterExecuteService.putRunningTasks(request.getAmassReport(), request.getTestId()); + } + jmeterExecuteService.runStart(request); + } + } catch (Exception e) { + LogUtil.error(e.getMessage()); + } finally { + ack.acknowledge(); + } + LogUtil.info("执行执行请求处理结束"); + } +} diff --git a/src/main/java/io/metersphere/api/service/ProducerService.java b/src/main/java/io/metersphere/api/service/ProducerService.java new file mode 100644 index 0000000..c19894e --- /dev/null +++ b/src/main/java/io/metersphere/api/service/ProducerService.java @@ -0,0 +1,38 @@ +package io.metersphere.api.service; + +import io.metersphere.api.config.KafkaConfig; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Service; + +import java.util.Map; + +@Service +public class ProducerService { + private KafkaTemplate kafkaTemplate; + + public String init(Map producerProps) { + try { + if (kafkaTemplate == null) { + producerProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, + "org.apache.kafka.common.serialization.StringSerializer"); + producerProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, + "org.apache.kafka.common.serialization.StringSerializer"); + DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(producerProps); + KafkaTemplate kafkaTemplate = new KafkaTemplate(pf, true); + this.kafkaTemplate = kafkaTemplate; + } + return "SUCCESS"; + } catch (Exception e) { + return e.getMessage(); + } + } + + public void send(String message) { + if (this.kafkaTemplate != null) { + this.kafkaTemplate.send(KafkaConfig.TOPICS, message); + this.kafkaTemplate.flush(); + } + } +} diff --git a/src/main/java/io/metersphere/api/service/utils/BodyFile.java b/src/main/java/io/metersphere/api/service/utils/BodyFile.java new file mode 100644 index 0000000..f73d457 --- /dev/null +++ b/src/main/java/io/metersphere/api/service/utils/BodyFile.java @@ -0,0 +1,10 @@ +package io.metersphere.api.service.utils; + +import lombok.Data; + +@Data +public class BodyFile { + + private String id; + private String name; +} diff --git a/src/main/java/io/metersphere/api/service/utils/ZipSpider.java b/src/main/java/io/metersphere/api/service/utils/ZipSpider.java new file mode 100644 index 0000000..89b749f --- /dev/null +++ b/src/main/java/io/metersphere/api/service/utils/ZipSpider.java @@ -0,0 +1,239 @@ +package io.metersphere.api.service.utils; + +import java.io.*; +import java.net.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import com.alibaba.fastjson.JSON; +import io.metersphere.api.jmeter.utils.FileUtils; +import io.metersphere.node.util.LogUtil; +import org.apache.dubbo.common.utils.CollectionUtils; +import org.apache.jmeter.config.CSVDataSet; +import org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy; +import org.apache.jmeter.protocol.http.util.HTTPFileArg; +import org.apache.jorphan.collections.HashTree; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +public class ZipSpider { + + //根据网址返回网页源代码 + public static String getHtmlFromUrl(String url, String encoding) { + StringBuffer html = new StringBuffer(); + InputStreamReader isr = null; + BufferedReader buf = null; + String str = null; + try { + URL urlObj = new URL(url); + URLConnection con = urlObj.openConnection(); + isr = new InputStreamReader(con.getInputStream(), encoding); + buf = new BufferedReader(isr); + while ((str = buf.readLine()) != null) { + html.append(str + "\n"); + } + //sop(html.toString()); + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (isr != null) { + try { + buf.close(); + isr.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return html.toString(); + } + + //根据网址下载网络文件到硬盘,包括图片,Gif图,以及压缩包 + public static void download(String url, String path) { + File file = null; + FileOutputStream fos = null; + String downloadName = url.substring(url.lastIndexOf("/") + 1); + HttpURLConnection httpCon = null; + URLConnection con = null; + URL urlObj = null; + InputStream in = null; + byte[] size = new byte[1024]; + int num = 0; + try { + file = new File(path + downloadName); + fos = new FileOutputStream(file); + if (url.startsWith("http")) { + urlObj = new URL(url); + con = urlObj.openConnection(); + httpCon = (HttpURLConnection) con; + in = httpCon.getInputStream(); + while ((num = in.read(size)) != -1) { + for (int i = 0; i < num; i++) + fos.write(size[i]); + } + } + + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + in.close(); + fos.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + //解压本地文件至目的文件路径 + public static void unzip(String fromFile, String toFile) { + try (ZipInputStream zin = new ZipInputStream(new FileInputStream(fromFile)); BufferedInputStream bin = new BufferedInputStream(zin);) { + String Parent = toFile; + ZipEntry entry; + while ((entry = zin.getNextEntry()) != null && !entry.isDirectory()) { + File fout = new File(Parent, entry.getName()); + if (!fout.exists()) { + (new File(fout.getParent())).mkdirs(); + } + try (FileOutputStream out = new FileOutputStream(fout); + BufferedOutputStream bout = new BufferedOutputStream(out);) { + int b; + while ((b = bin.read()) != -1) { + bout.write(b); + } + bout.close(); + out.close(); + LogUtil.info(fout + "解压成功"); + } catch (Exception e) { + e.printStackTrace(); + } + + } + bin.close(); + zin.close(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + //从总目录下解压文件里所有的压缩包至目的文件路径 + public static void unzipFromLoc(String filePath) throws Exception { + File file = new File(filePath); + File[] list = file.listFiles(); + String from = ""; + String to = "E:\\myDownload\\unzipFileFromWeb\\"; + for (File f : list) { + boolean bool = f.isFile(); + if (bool) { + from = f.getAbsolutePath(); + from = from.replace("\\", "\\\\"); + sop(from); + unzip(from, to); + } + } + } + + public static void sop(Object obj) { + LogUtil.info(obj); + } + + public static void seperate(char c) { + for (int x = 0; x < 100; x++) { + System.out.print(c); + } + sop(""); + } + + public static void getFiles(HashTree tree, List files) { + for (Object key : tree.keySet()) { + HashTree node = tree.get(key); + if (key instanceof HTTPSamplerProxy) { + HTTPSamplerProxy source = (HTTPSamplerProxy) key; + if (source != null && source.getHTTPFiles().length > 0) { + for (HTTPFileArg arg : source.getHTTPFiles()) { + BodyFile file = new BodyFile(); + file.setId(arg.getParamName()); + file.setName(arg.getPath()); + files.add(file); + } + } + } else if (key instanceof CSVDataSet) { + CSVDataSet source = (CSVDataSet) key; + if (source != null && source.getFilename() != null) { + BodyFile file = new BodyFile(); + file.setId(source.getFilename()); + file.setName(source.getFilename()); + files.add(file); + } + } + if (node != null) { + getFiles(node, files); + } + } + } + + @SuppressWarnings("finally") + public static File downloadFile(String urlPath, String downloadDir) { + OutputStream out = null; + BufferedInputStream bin = null; + try { + URL url = new URL(urlPath); + URLConnection urlConnection = url.openConnection(); + HttpURLConnection httpURLConnection = (HttpURLConnection) urlConnection;// http的连接类 + httpURLConnection.setConnectTimeout(1000 * 5);//设置超时 + httpURLConnection.setRequestMethod("GET");//设置请求方式,默认是GET + httpURLConnection.setRequestProperty("Charset", "UTF-8");// 设置字符编码 + httpURLConnection.connect();// 打开连接 + + bin = new BufferedInputStream(httpURLConnection.getInputStream()); + String fileName = httpURLConnection.getHeaderField("Content-Disposition"); + fileName = URLDecoder.decode(fileName.substring(fileName.indexOf("filename") + 10, fileName.length() - 1), "UTF-8"); + String path = downloadDir + File.separatorChar + fileName;// 指定存放位置 + File file = new File(path); + // 校验文件夹目录是否存在,不存在就创建一个目录 + if (file.getParentFile() != null && !file.getParentFile().exists()) { + file.getParentFile().mkdirs(); + } + + out = new FileOutputStream(file); + int size = 0; + + byte[] b = new byte[2048]; + //把输入流的文件读取到字节数据b中,然后输出到指定目录的文件 + while ((size = bin.read(b)) != -1) { + out.write(b, 0, size); + } + // 关闭资源 + bin.close(); + out.close(); + LogUtil.info("文件下载成功!"); + return file; + } catch (MalformedURLException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + LogUtil.info("文件下载失败!"); + } finally { + try { + if (bin != null) { + bin.close(); + } + if (out != null) { + out.close(); + } + } catch (Exception e) { + + } + } + return null; + } +} diff --git a/src/main/java/io/metersphere/controller/request/TestRequest.java b/src/main/java/io/metersphere/controller/request/TestRequest.java deleted file mode 100644 index f790f43..0000000 --- a/src/main/java/io/metersphere/controller/request/TestRequest.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.metersphere.controller.request; - -import lombok.Data; - -import java.util.HashMap; -import java.util.Map; - -@Data -public class TestRequest extends DockerLoginRequest { - - private int size; - private String fileString; - private String testId; - private String image; - private Map testData = new HashMap<>(); - private Map env = new HashMap<>(); -} diff --git a/src/main/java/io/metersphere/controller/DockerClientController.java b/src/main/java/io/metersphere/node/controller/DockerClientController.java similarity index 86% rename from src/main/java/io/metersphere/controller/DockerClientController.java rename to src/main/java/io/metersphere/node/controller/DockerClientController.java index 516f498..7dea07c 100644 --- a/src/main/java/io/metersphere/controller/DockerClientController.java +++ b/src/main/java/io/metersphere/node/controller/DockerClientController.java @@ -1,4 +1,4 @@ -package io.metersphere.controller; +package io.metersphere.node.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; diff --git a/src/main/java/io/metersphere/controller/JmeterOperateController.java b/src/main/java/io/metersphere/node/controller/JmeterOperateController.java similarity index 52% rename from src/main/java/io/metersphere/controller/JmeterOperateController.java rename to src/main/java/io/metersphere/node/controller/JmeterOperateController.java index 7c8d24c..00359a0 100644 --- a/src/main/java/io/metersphere/controller/JmeterOperateController.java +++ b/src/main/java/io/metersphere/node/controller/JmeterOperateController.java @@ -1,13 +1,12 @@ -package io.metersphere.controller; +package io.metersphere.node.controller; import com.github.dockerjava.api.model.Container; -import io.metersphere.controller.request.TestRequest; -import io.metersphere.service.JmeterOperateService; +import io.metersphere.node.controller.request.TestRequest; +import io.metersphere.node.service.JmeterOperateService; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; -import java.io.IOException; import java.util.List; @RestController @@ -20,32 +19,18 @@ public class JmeterOperateController { * 初始化测试任务,根据需求启动若干个 JMeter Engine 容器 */ @PostMapping("/container/start") - public void startContainer(@RequestBody TestRequest testRequest) throws IOException { + public String startContainer(@RequestBody TestRequest testRequest) { jmeterOperateService.startContainer(testRequest); - } - - // 上传测试相关文件,将请求传过来的脚本、测试数据等文件,拷贝到上述容器中 - @PostMapping("/upload/task") - public void uploadFile(String jmxString) { - // 挂载数据到启动的容器 - // FileUtil.saveFile(jmxString, "/User/liyuhao/test", "ceshi2.jmx"); - } - - // 启动测试任务,控制上述容器执行 jmeter 相关命令开始进行测试 - @PostMapping("/exec/task") - public void taskPerform() { - // 执行 jmx - // docker run -it -v /Users/liyuhao/test:/test justb4/jmeter:latest -n -t /test/ceshi3.jmx - // dockerClient.startContainerCmd("18894c7755b1").exec(); - // dockerClient.execCreateCmd("18894c7755b1").withAttachStdout(true).withCmd("jmeter", "-n", "-t", "/test/*.jmx"); + return "OK"; } /** * 停止指定测试任务,控制上述容器停止指定的 JMeter 测试 */ @GetMapping("container/stop/{testId}") - public void stopContainer(@PathVariable String testId) { + public String stopContainer(@PathVariable String testId) { jmeterOperateService.stopContainer(testId); + return "OK"; } /** diff --git a/src/main/java/io/metersphere/node/controller/handler/RestControllerExceptionHandler.java b/src/main/java/io/metersphere/node/controller/handler/RestControllerExceptionHandler.java new file mode 100644 index 0000000..de54f00 --- /dev/null +++ b/src/main/java/io/metersphere/node/controller/handler/RestControllerExceptionHandler.java @@ -0,0 +1,18 @@ +package io.metersphere.node.controller.handler; + + +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + + +@RestControllerAdvice +public class RestControllerExceptionHandler { + + @ExceptionHandler(Exception.class) + public ResultHolder msExceptionHandler(HttpServletRequest request, HttpServletResponse response, Exception e) { + return ResultHolder.error(e.getMessage()); + } +} diff --git a/src/main/java/io/metersphere/node/controller/handler/ResultHolder.java b/src/main/java/io/metersphere/node/controller/handler/ResultHolder.java new file mode 100644 index 0000000..e3c1d2a --- /dev/null +++ b/src/main/java/io/metersphere/node/controller/handler/ResultHolder.java @@ -0,0 +1,73 @@ +package io.metersphere.node.controller.handler; + +import org.apache.commons.lang3.builder.ReflectionToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +public class ResultHolder { + public ResultHolder() { + this.success = true; + } + + private ResultHolder(Object data) { + this.data = data; + this.success = true; + } + + private ResultHolder(boolean success, String msg) { + this.success = success; + this.message = msg; + } + + private ResultHolder(boolean success, String msg, Object data) { + this.success = success; + this.message = msg; + this.data = data; + } + + // 请求是否成功 + private boolean success = false; + // 描述信息 + private String message; + // 返回数据 + private Object data = ""; + + public boolean isSuccess() { + return this.success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Object getData() { + return data; + } + + public void setData(Object data) { + this.data = data; + } + + public static ResultHolder success(Object obj) { + return new ResultHolder(obj); + } + + public static ResultHolder error(String message) { + return new ResultHolder(false, message, null); + } + + public static ResultHolder error(String message, Object object) { + return new ResultHolder(false, message, object); + } + + public String toString() { + return ReflectionToStringBuilder.toString(this, ToStringStyle.SHORT_PREFIX_STYLE); + } +} diff --git a/src/main/java/io/metersphere/node/controller/handler/ResultResponseBodyAdvice.java b/src/main/java/io/metersphere/node/controller/handler/ResultResponseBodyAdvice.java new file mode 100644 index 0000000..e784219 --- /dev/null +++ b/src/main/java/io/metersphere/node/controller/handler/ResultResponseBodyAdvice.java @@ -0,0 +1,50 @@ +package io.metersphere.node.controller.handler; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.metersphere.node.util.LogUtil; +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +/** + * 统一处理返回结果集 + */ +@RestControllerAdvice(value = {"io.metersphere.node"}) +public class ResultResponseBodyAdvice implements ResponseBodyAdvice { + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public boolean supports(MethodParameter methodParameter, Class> converterType) { + return MappingJackson2HttpMessageConverter.class.isAssignableFrom(converterType) || StringHttpMessageConverter.class.isAssignableFrom(converterType); + } + + @Override + public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class> converterType, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { + // 处理空值 + if (o == null && StringHttpMessageConverter.class.isAssignableFrom(converterType)) { + return null; + } + + if (!(o instanceof ResultHolder)) { + if (o instanceof String) { + ResultHolder success = ResultHolder.success(o); + try { + return objectMapper.writeValueAsString(success); + } catch (JsonProcessingException e) { + LogUtil.error(e); + return o; + } + } + return ResultHolder.success(o); + } + return o; + } + +} diff --git a/src/main/java/io/metersphere/controller/request/DockerLoginRequest.java b/src/main/java/io/metersphere/node/controller/request/DockerLoginRequest.java similarity index 53% rename from src/main/java/io/metersphere/controller/request/DockerLoginRequest.java rename to src/main/java/io/metersphere/node/controller/request/DockerLoginRequest.java index fa01205..52c854b 100644 --- a/src/main/java/io/metersphere/controller/request/DockerLoginRequest.java +++ b/src/main/java/io/metersphere/node/controller/request/DockerLoginRequest.java @@ -1,8 +1,10 @@ -package io.metersphere.controller.request; +package io.metersphere.node.controller.request; -import lombok.Data; +import lombok.Getter; +import lombok.Setter; -@Data +@Getter +@Setter public class DockerLoginRequest { private String username; private String password; diff --git a/src/main/java/io/metersphere/node/controller/request/TestRequest.java b/src/main/java/io/metersphere/node/controller/request/TestRequest.java new file mode 100644 index 0000000..ccb1729 --- /dev/null +++ b/src/main/java/io/metersphere/node/controller/request/TestRequest.java @@ -0,0 +1,14 @@ +package io.metersphere.node.controller.request; + +import lombok.Getter; +import lombok.Setter; + +import java.util.HashMap; +import java.util.Map; + +@Getter +@Setter +public class TestRequest extends DockerLoginRequest { + private String image; + private Map env = new HashMap<>(); +} diff --git a/src/main/java/io/metersphere/node/service/JmeterOperateService.java b/src/main/java/io/metersphere/node/service/JmeterOperateService.java new file mode 100644 index 0000000..b545025 --- /dev/null +++ b/src/main/java/io/metersphere/node/service/JmeterOperateService.java @@ -0,0 +1,276 @@ +package io.metersphere.node.service; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.command.WaitContainerResultCallback; +import com.github.dockerjava.api.model.Container; +import com.github.dockerjava.api.model.Frame; +import com.github.dockerjava.api.model.HostConfig; +import com.github.dockerjava.api.model.Image; +import com.github.dockerjava.core.InvocationBuilder; +import io.metersphere.node.controller.request.TestRequest; +import io.metersphere.node.util.CompressUtils; +import io.metersphere.node.util.DockerClientService; +import io.metersphere.node.util.LogUtil; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import javax.annotation.Resource; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.zip.ZipOutputStream; + +@Service +public class JmeterOperateService { + @Resource + private KafkaProducer kafkaProducer; + + public void startContainer(TestRequest testRequest) { + Map env = testRequest.getEnv(); + String testId = env.get("TEST_ID"); + LogUtil.info("Receive start container request, test id: {}", testId); + String bootstrapServers = env.get("BOOTSTRAP_SERVERS"); + // 检查kafka连通性 + checkKafka(bootstrapServers); + + DockerClient dockerClient = DockerClientService.connectDocker(testRequest); + + String containerImage = testRequest.getImage(); + + // 查找镜像 + searchImage(dockerClient, testRequest.getImage()); + // 检查容器是否存在 + checkContainerExists(dockerClient, testId); + // 启动测试 + startContainer(testRequest, dockerClient, testId, containerImage); + } + + private void startContainer(TestRequest testRequest, DockerClient dockerClient, String testId, String containerImage) { + // 创建 hostConfig + HostConfig hostConfig = HostConfig.newHostConfig(); + hostConfig.withNetworkMode("host"); + String[] envs = getEnvs(testRequest); + String containerId = DockerClientService.createContainers(dockerClient, testId, containerImage, hostConfig, envs).getId(); + + DockerClientService.startContainer(dockerClient, containerId); + LogUtil.info("Container create started containerId: " + containerId); + + String topic = testRequest.getEnv().getOrDefault("LOG_TOPIC", "JMETER_LOGS"); + String reportId = testRequest.getEnv().get("REPORT_ID"); + String resourceIndex = testRequest.getEnv().get("RESOURCE_INDEX"); + + dockerClient.waitContainerCmd(containerId) + .exec(new WaitContainerResultCallback() { + @Override + public void onComplete() { + // 清理文件夹 + try { + if (DockerClientService.existContainer(dockerClient, containerId) > 0) { + +// copyTestResources(dockerClient, containerId, reportId, resourceIndex); + + DockerClientService.removeContainer(dockerClient, containerId); + } + // 上传结束消息 + String[] contents = new String[]{reportId, "none", "0", "Remove container completed"}; + String log = StringUtils.join(contents, " "); + kafkaProducer.sendMessage(topic, log); + LogUtil.info("Remove container completed: " + containerId); + } catch (Exception e) { + LogUtil.error("Remove container error: ", e); + } + LogUtil.info("completed...."); + } + }); + + dockerClient.logContainerCmd(containerId) + .withFollowStream(true) + .withStdOut(true) + .withStdErr(true) + .withTailAll() + .exec(new InvocationBuilder.AsyncResultCallback() { + @Override + public void onNext(Frame item) { + String log = new String(item.getPayload()).trim(); + String oomMessage = "There is insufficient memory for the Java Runtime Environment to continue."; + if (StringUtils.contains(log, oomMessage)) { + // oom 退出 + String[] contents = new String[]{reportId, "none", "0", oomMessage}; + String message = StringUtils.join(contents, " "); + kafkaProducer.sendMessage(topic, message); + } + LogUtil.info(log); + } + }); + } + + private void copyTestResources(DockerClient dockerClient, String containerId, String reportId, String resourceIndex) throws IOException { + InputStream testIn = dockerClient + .copyArchiveFromContainerCmd(containerId, "/test/") + .exec(); + testIn.available(); + String pathname = reportId + "_" + resourceIndex; + File dir = new File(pathname); + + FileUtils.forceMkdir(dir); + File testDir = new File(pathname + "/test.tar"); + FileUtils.copyInputStreamToFile(testIn, testDir); + + + InputStream jtlIn = dockerClient + .copyArchiveFromContainerCmd(containerId, "/jmeter-log/" + reportId + "_" + resourceIndex + ".jtl") + .exec(); + jtlIn.available(); + File jtl = new File(pathname + "/report.jtl.tar"); + FileUtils.copyInputStreamToFile(jtlIn, jtl); + + InputStream logIn = dockerClient + .copyArchiveFromContainerCmd(containerId, "/jmeter-log/jmeter.log") + .exec(); + logIn.available(); + File log = new File(pathname + "/jmeter.log.tar"); + FileUtils.copyInputStreamToFile(logIn, log); + + InputStream generateLogIn = dockerClient + .copyArchiveFromContainerCmd(containerId, "/jmeter-log/generate-report.log") + .exec(); + generateLogIn.available(); + File generateLog = new File(pathname + "/generate-report.log.tar"); + FileUtils.copyInputStreamToFile(generateLogIn, generateLog); + + try ( + ZipOutputStream zipOutputStream = new ZipOutputStream(new FileOutputStream(reportId + "_" + resourceIndex + ".zip")) + ) { + CompressUtils.zipDirectory(dir, zipOutputStream, ""); + FileUtils.forceDelete(dir); + } + } + + private void checkContainerExists(DockerClient dockerClient, String testId) { + List list = dockerClient.listContainersCmd() + .withShowAll(true) + .withStatusFilter(Arrays.asList("created", "restarting", "running", "paused", "exited")) + .withNameFilter(Collections.singletonList(testId)) + .exec(); + if (!CollectionUtils.isEmpty(list)) { + list.forEach(container -> DockerClientService.removeContainer(dockerClient, container.getId())); + } + } + + private void checkKafka(String bootstrapServers) { + String[] servers = StringUtils.split(bootstrapServers, ","); + try { + for (String s : servers) { + String[] ipAndPort = s.split(":"); + //1,建立tcp + String ip = ipAndPort[0]; + int port = Integer.parseInt(ipAndPort[1]); + try ( + Socket soc = new Socket() + ) { + soc.connect(new InetSocketAddress(ip, port), 1000); // 1s timeout + } + } + } catch (Exception e) { + LogUtil.error(e); + throw new RuntimeException("Failed to connect to Kafka"); + } + } + + private String[] getEnvs(TestRequest testRequest) { + Map env = testRequest.getEnv(); + return env.keySet().stream().map(k -> k + "=" + env.get(k)).toArray(String[]::new); + } + + private void searchImage(DockerClient dockerClient, String imageName) { + // image + List imageList = dockerClient.listImagesCmd().exec(); + if (CollectionUtils.isEmpty(imageList)) { + throw new RuntimeException("Image List is empty"); + } + List collect = imageList.stream().filter(image -> { + String[] repoTags = image.getRepoTags(); + if (repoTags == null) { + return false; + } + for (String repoTag : repoTags) { + if (repoTag.equals(imageName)) { + return true; + } + } + return false; + }).collect(Collectors.toList()); + + if (collect.size() == 0) { + throw new RuntimeException("Image Not Found: " + imageName); + } + } + + + public void stopContainer(String testId) { + LogUtil.info("Receive stop container request, test: {}", testId); + DockerClient dockerClient = DockerClientService.connectDocker(); + + // container filter + List list = dockerClient.listContainersCmd() + .withShowAll(true) + .withStatusFilter(Collections.singletonList("running")) + .withNameFilter(Collections.singletonList(testId)) + .exec(); + // container stop + list.forEach(container -> DockerClientService.removeContainer(dockerClient, container.getId())); + } + + public List taskStatus(String testId) { + DockerClient dockerClient = DockerClientService.connectDocker(); + List containerList = dockerClient.listContainersCmd() + .withStatusFilter(Arrays.asList("created", "restarting", "running", "paused", "exited")) + .withNameFilter(Collections.singletonList(testId)) + .exec(); + // 查询执行的状态 + return containerList; + } + + public String logContainer(String testId) { + LogUtil.info("Receive logs container request, test: {}", testId); + DockerClient dockerClient = DockerClientService.connectDocker(); + + // container filter + List list = dockerClient.listContainersCmd() + .withShowAll(true) + .withStatusFilter(Collections.singletonList("running")) + .withNameFilter(Collections.singletonList(testId)) + .exec(); + + StringBuilder sb = new StringBuilder(); + if (list.size() > 0) { + try { + dockerClient.logContainerCmd(list.get(0).getId()) + .withFollowStream(true) + .withStdOut(true) + .withStdErr(true) + .withTailAll() + .exec(new InvocationBuilder.AsyncResultCallback() { + @Override + public void onNext(Frame item) { + sb.append(new String(item.getPayload()).trim()).append("\n"); + } + }).awaitCompletion(100, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + LogUtil.error(e); + } + } + return sb.toString(); + } +} diff --git a/src/main/java/io/metersphere/node/service/KafkaProducer.java b/src/main/java/io/metersphere/node/service/KafkaProducer.java new file mode 100644 index 0000000..b05ecd3 --- /dev/null +++ b/src/main/java/io/metersphere/node/service/KafkaProducer.java @@ -0,0 +1,17 @@ +package io.metersphere.node.service; + +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +@Service +public class KafkaProducer { + + @Resource + private KafkaTemplate kafkaTemplate; + + public void sendMessage(String topic, String report) { + this.kafkaTemplate.send(topic, report); + } +} diff --git a/src/main/java/io/metersphere/node/util/CompressUtils.java b/src/main/java/io/metersphere/node/util/CompressUtils.java new file mode 100644 index 0000000..04bcd95 --- /dev/null +++ b/src/main/java/io/metersphere/node/util/CompressUtils.java @@ -0,0 +1,153 @@ +package io.metersphere.node.util; + +import java.io.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +public class CompressUtils { + + /*** + * 压缩Zip + * + * @param data + * @return + */ + public static Object zip(Object data) { + if (!(data instanceof byte[])) { + return data; + } + + byte[] temp = (byte[]) data; + byte[] b = (byte[]) data; + try { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(bos); + ZipEntry entry = new ZipEntry("zip"); + entry.setSize(temp.length); + zip.putNextEntry(entry); + zip.write(temp); + zip.closeEntry(); + zip.close(); + b = bos.toByteArray(); + bos.close(); + } catch (Exception ex) { + LogUtil.error(ex); + } + + return b; + } + + /*** + * 解压Zip + * + * @param data + * @return + */ + public static Object unzip(Object data) { + if (!(data instanceof byte[])) { + return data; + } + byte[] temp = (byte[]) data; + byte[] b = (byte[]) data; + try { + ByteArrayInputStream bis = new ByteArrayInputStream(temp); + ZipInputStream zip = new ZipInputStream(bis); + while (zip.getNextEntry() != null) { + byte[] buf = new byte[1024]; + int num; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + while ((num = zip.read(buf, 0, buf.length)) != -1) { + baos.write(buf, 0, num); + } + b = baos.toByteArray(); + baos.flush(); + baos.close(); + } + zip.close(); + bis.close(); + } catch (Exception ex) { + LogUtil.error(ex); + } + return b; + } + + + public static void zipFiles(File srcfile, File targetFile) { + + ZipOutputStream out = null; + try { + out = new ZipOutputStream(new FileOutputStream(targetFile)); + + if (srcfile.isFile()) { + zipFile(srcfile, out, ""); + } else { + File[] list = srcfile.listFiles(); + for (int i = 0; i < list.length; i++) { + compress(list[i], out, ""); + } + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + if (out != null) + out.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + private static void compress(File file, ZipOutputStream out, String basedir) { + /* 判断是目录还是文件 */ + if (file.isDirectory()) { + zipDirectory(file, out, basedir); + } else { + zipFile(file, out, basedir); + } + } + + public static void zipDirectory(File dir, ZipOutputStream out, String basedir) { + if (!dir.exists()) + return; + + File[] files = dir.listFiles(); + for (int i = 0; i < files.length; i++) { + /* 递归 */ + compress(files[i], out, basedir + dir.getName() + "/"); + } + } + + + public static void zipFile(File srcfile, ZipOutputStream out, String basedir) { + if (!srcfile.exists()) + return; + + byte[] buf = new byte[8192]; + FileInputStream in = null; + + try { + int len; + in = new FileInputStream(srcfile); + out.putNextEntry(new ZipEntry(basedir + srcfile.getName())); + + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + if (out != null) + out.closeEntry(); + if (in != null) + in.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + +} diff --git a/src/main/java/io/metersphere/util/DockerClientService.java b/src/main/java/io/metersphere/node/util/DockerClientService.java similarity index 67% rename from src/main/java/io/metersphere/util/DockerClientService.java rename to src/main/java/io/metersphere/node/util/DockerClientService.java index 7222232..9d0766a 100644 --- a/src/main/java/io/metersphere/util/DockerClientService.java +++ b/src/main/java/io/metersphere/node/util/DockerClientService.java @@ -1,14 +1,17 @@ -package io.metersphere.util; +package io.metersphere.node.util; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.CreateContainerResponse; +import com.github.dockerjava.api.model.Container; import com.github.dockerjava.api.model.HostConfig; -import com.github.dockerjava.api.model.Info; import com.github.dockerjava.core.DefaultDockerClientConfig; import com.github.dockerjava.core.DockerClientBuilder; -import io.metersphere.controller.request.DockerLoginRequest; +import io.metersphere.node.controller.request.DockerLoginRequest; import org.apache.commons.lang.StringUtils; +import java.util.Collections; +import java.util.List; + public class DockerClientService { /** @@ -17,11 +20,7 @@ public class DockerClientService { * @return */ public static DockerClient connectDocker() { - DockerClient dockerClient = DockerClientBuilder.getInstance().build(); - Info info = dockerClient.infoCmd().exec(); - LogUtil.info("docker的环境信息如下:================="); - LogUtil.info(info); - return dockerClient; + return DockerClientBuilder.getInstance().build(); } public static DockerClient connectDocker(DockerLoginRequest request) { @@ -33,11 +32,7 @@ public static DockerClient connectDocker(DockerLoginRequest request) { .withRegistryUsername(request.getUsername()) .withRegistryPassword(request.getPassword()) .build(); - DockerClient dockerClient = DockerClientBuilder.getInstance(config).build(); - Info info = dockerClient.infoCmd().exec(); - LogUtil.info("docker的环境信息如下:================="); - LogUtil.info(info); - return dockerClient; + return DockerClientBuilder.getInstance(config).build(); } /** @@ -46,7 +41,8 @@ public static DockerClient connectDocker(DockerLoginRequest request) { * @param client * @return */ - public static CreateContainerResponse createContainers(DockerClient client, String containerName, String imageName, HostConfig hostConfig, String... env) { + public static CreateContainerResponse createContainers(DockerClient client, String containerName, String imageName, + HostConfig hostConfig, String... env) { CreateContainerResponse container = client.createContainerCmd(imageName) .withName(containerName) .withHostConfig(hostConfig) @@ -83,7 +79,24 @@ public static void stopContainer(DockerClient client, String containerId) { * @param containerId */ public static void removeContainer(DockerClient client, String containerId) { - client.removeContainerCmd(containerId).exec(); + client.removeContainerCmd(containerId) + .withForce(true) + .withRemoveVolumes(true) + .exec(); + } + + /** + * 容器是否存在 + * + * @param client + * @param containerId + */ + public static int existContainer(DockerClient client, String containerId) { + List list = client.listContainersCmd() + .withShowAll(true) + .withIdFilter(Collections.singleton(containerId)) + .exec(); + return list.size(); } } diff --git a/src/main/java/io/metersphere/util/LogUtil.java b/src/main/java/io/metersphere/node/util/LogUtil.java similarity index 99% rename from src/main/java/io/metersphere/util/LogUtil.java rename to src/main/java/io/metersphere/node/util/LogUtil.java index c955137..df77159 100644 --- a/src/main/java/io/metersphere/util/LogUtil.java +++ b/src/main/java/io/metersphere/node/util/LogUtil.java @@ -1,4 +1,4 @@ -package io.metersphere.util; +package io.metersphere.node.util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/io/metersphere/service/JmeterOperateService.java b/src/main/java/io/metersphere/service/JmeterOperateService.java deleted file mode 100644 index 743a623..0000000 --- a/src/main/java/io/metersphere/service/JmeterOperateService.java +++ /dev/null @@ -1,176 +0,0 @@ -package io.metersphere.service; - -import com.github.dockerjava.api.DockerClient; -import com.github.dockerjava.api.command.WaitContainerResultCallback; -import com.github.dockerjava.api.model.Container; -import com.github.dockerjava.api.model.Frame; -import com.github.dockerjava.api.model.HostConfig; -import com.github.dockerjava.api.model.Image; -import com.github.dockerjava.core.InvocationBuilder; -import io.metersphere.controller.request.TestRequest; -import io.metersphere.util.DockerClientService; -import io.metersphere.util.LogUtil; -import org.apache.commons.io.FileUtils; -import org.apache.commons.lang.StringUtils; -import org.springframework.stereotype.Service; -import org.springframework.util.CollectionUtils; - -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.*; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -@Service -public class JmeterOperateService { - - public void startContainer(TestRequest testRequest) throws IOException { - LogUtil.info("Receive start container request, test id: {}", testRequest.getTestId()); - DockerClient dockerClient = DockerClientService.connectDocker(testRequest); - int size = testRequest.getSize(); - String testId = testRequest.getTestId(); - - String containerImage = testRequest.getImage(); - String filePath = StringUtils.join(new String[]{"", "opt", "node-data", testId}, File.separator); - String fileName = testId + ".jmx"; - - - // 每个测试生成一个文件夹 - FileUtils.writeStringToFile(new File(filePath + File.separator + fileName), testRequest.getFileString(), StandardCharsets.UTF_8); - // 保存测试数据文件 - Map testData = testRequest.getTestData(); - if (!CollectionUtils.isEmpty(testData)) { - for (String k : testData.keySet()) { - String v = testData.get(k); - FileUtils.writeStringToFile(new File(filePath + File.separator + k), v, StandardCharsets.UTF_8); - } - } - - // 查找镜像 - searchImage(dockerClient, testRequest.getImage()); - - ArrayList containerIdList = new ArrayList<>(); - for (int i = 0; i < size; i++) { - String containerName = testId + "-" + i; - // 创建 hostConfig - HostConfig hostConfig = HostConfig.newHostConfig(); - String[] envs = getEnvs(testRequest); - String containerId = DockerClientService.createContainers(dockerClient, containerName, containerImage, hostConfig, envs).getId(); - // 从主机复制文件到容器 - dockerClient.copyArchiveToContainerCmd(containerId) - .withHostResource(filePath) - .withDirChildrenOnly(true) - .withRemotePath("/test") - .exec(); - containerIdList.add(containerId); - } - - containerIdList.forEach(containerId -> { - DockerClientService.startContainer(dockerClient, containerId); - LogUtil.info("Container create started containerId: " + containerId); - dockerClient.waitContainerCmd(containerId) - .exec(new WaitContainerResultCallback() { - @Override - public void onComplete() { - // 清理文件夹 - try { - FileUtils.forceDelete(new File(filePath)); - LogUtil.info("Remove dir completed."); - DockerClientService.removeContainer(dockerClient, containerId); - LogUtil.info("Remove container completed: " + containerId); - } catch (IOException e) { - LogUtil.error("Remove dir error: ", e); - } - LogUtil.info("completed...."); - } - }); - }); - } - - private String[] getEnvs(TestRequest testRequest) { - Map env = testRequest.getEnv(); - return env.keySet().stream().map(k -> k + "=" + env.get(k)).toArray(String[]::new); - } - - private void searchImage(DockerClient dockerClient, String imageName) { - // image - List imageList = dockerClient.listImagesCmd().exec(); - if (CollectionUtils.isEmpty(imageList)) { - throw new RuntimeException("Image List is empty"); - } - List collect = imageList.stream().filter(image -> { - String[] repoTags = image.getRepoTags(); - if (repoTags == null) { - return false; - } - for (String repoTag : repoTags) { - if (repoTag.equals(imageName)) { - return true; - } - } - return false; - }).collect(Collectors.toList()); - - if (collect.size() == 0) { - throw new RuntimeException("Image Not Found."); - } - } - - - public void stopContainer(String testId) { - LogUtil.info("Receive stop container request, test: {}", testId); - DockerClient dockerClient = DockerClientService.connectDocker(); - - // container filter - List list = dockerClient.listContainersCmd() - .withShowAll(true) - .withStatusFilter(Collections.singletonList("running")) - .withNameFilter(Collections.singletonList(testId)) - .exec(); - // container stop - list.forEach(container -> DockerClientService.stopContainer(dockerClient, container.getId())); - } - - public List taskStatus(String testId) { - DockerClient dockerClient = DockerClientService.connectDocker(); - List containerList = dockerClient.listContainersCmd() - .withStatusFilter(Arrays.asList("created", "restarting", "running", "paused", "exited")) - .withNameFilter(Collections.singletonList(testId)) - .exec(); - // 查询执行的状态 - return containerList; - } - - public String logContainer(String testId) { - LogUtil.info("Receive logs container request, test: {}", testId); - DockerClient dockerClient = DockerClientService.connectDocker(); - - // container filter - List list = dockerClient.listContainersCmd() - .withShowAll(true) - .withStatusFilter(Collections.singletonList("running")) - .withNameFilter(Collections.singletonList(testId)) - .exec(); - - StringBuilder sb = new StringBuilder(); - if (list.size() > 0) { - try { - dockerClient.logContainerCmd(list.get(0).getId()) - .withFollowStream(true) - .withStdOut(true) - .withStdErr(true) - .withTailAll() - .exec(new InvocationBuilder.AsyncResultCallback() { - @Override - public void onNext(Frame item) { - sb.append(new String(item.getPayload()).trim()).append("\n"); - } - }).awaitCompletion(100, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - LogUtil.error(e); - } - } - return sb.toString(); - } -} diff --git a/src/main/java/org/apache/jmeter/NewDriver.java b/src/main/java/org/apache/jmeter/NewDriver.java new file mode 100644 index 0000000..9b30948 --- /dev/null +++ b/src/main/java/org/apache/jmeter/NewDriver.java @@ -0,0 +1,378 @@ +/* + * 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; + +import java.io.File; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * Main class for JMeter - sets up initial classpath and the loader. + * + */ +public final class NewDriver { + + private static final String CLASSPATH_SEPARATOR = File.pathSeparator; + + private static final String OS_NAME = System.getProperty("os.name");// $NON-NLS-1$ + + private static final String OS_NAME_LC = OS_NAME.toLowerCase(java.util.Locale.ENGLISH); + + private static final String JAVA_CLASS_PATH = "java.class.path";// $NON-NLS-1$ + + private static final String JMETER_LOGFILE_SYSTEM_PROPERTY = "jmeter.logfile";// $NON-NLS-1$ + + private static final String HEADLESS_MODE_PROPERTY = "java.awt.headless";// $NON-NLS-1$ + /** The class loader to use for loading JMeter classes. */ + private static final DynamicClassLoader loader; + + /** The directory JMeter is installed in. */ + private static final String JMETER_INSTALLATION_DIRECTORY; + + private static final List EXCEPTIONS_IN_INIT = new ArrayList<>(); + + // 将当前类加载器设置为 loader ,解决由系统类加载器加载的 JMeter 无法动态加载 jar 包问题 + public static void setContextClassLoader() { + Thread.currentThread().setContextClassLoader(loader); + } + + public static void loaderClass(String name) { + try { + loader.loadClass(name); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + } + + static { + final List jars = new LinkedList<>(); + final String initiaClasspath = System.getProperty(JAVA_CLASS_PATH); + + // Find JMeter home dir from the initial classpath + String tmpDir; + + //只从 jmeter.home 加载 + tmpDir = System.getProperty("jmeter.home","");// Allow override $NON-NLS-1$ $NON-NLS-2$ + if (tmpDir.length() == 0) { + File userDir = new File(System.getProperty("user.dir"));// $NON-NLS-1$ + tmpDir = userDir.getAbsoluteFile().getParent(); + } + + JMETER_INSTALLATION_DIRECTORY=tmpDir; + + /* + * Does the system support UNC paths? If so, may need to fix them up + * later + */ + boolean usesUNC = OS_NAME_LC.startsWith("windows");// $NON-NLS-1$ + + // Add standard jar locations to initial classpath + StringBuilder classpath = new StringBuilder(); + File[] libDirs = new File[] { new File(JMETER_INSTALLATION_DIRECTORY + File.separator + "lib"),// $NON-NLS-1$ $NON-NLS-2$ + new File(JMETER_INSTALLATION_DIRECTORY + File.separator + "lib" + File.separator + "ext"),// $NON-NLS-1$ $NON-NLS-2$ + new File(JMETER_INSTALLATION_DIRECTORY + File.separator + "lib" + File.separator + "junit")};// $NON-NLS-1$ $NON-NLS-2$ + for (File libDir : libDirs) { + File[] libJars = libDir.listFiles((dir, name) -> name.endsWith(".jar")); + if (libJars == null) { + new Throwable("Could not access " + libDir).printStackTrace(); // NOSONAR No logging here + continue; + } + Arrays.sort(libJars); // Bug 50708 Ensure predictable order of jars + for (File libJar : libJars) { + try { + String s = libJar.getPath(); + + // Fix path to allow the use of UNC URLs + if (usesUNC) { + if (s.startsWith("\\\\") && !s.startsWith("\\\\\\")) {// $NON-NLS-1$ $NON-NLS-2$ + s = "\\\\" + s;// $NON-NLS-1$ + } else if (s.startsWith("//") && !s.startsWith("///")) {// $NON-NLS-1$ $NON-NLS-2$ + s = "//" + s;// $NON-NLS-1$ + } + } // usesUNC + + jars.add(new File(s).toURI().toURL());// See Java bug 4496398 + classpath.append(CLASSPATH_SEPARATOR); + classpath.append(s); + } catch (MalformedURLException e) { // NOSONAR + EXCEPTIONS_IN_INIT.add(new Exception("Error adding jar:"+libJar.getAbsolutePath(), e)); + } + } + } + + // ClassFinder needs the classpath + System.setProperty(JAVA_CLASS_PATH, initiaClasspath + classpath.toString()); + loader = AccessController.doPrivileged( + (PrivilegedAction) () -> + new DynamicClassLoader(jars.toArray(new URL[jars.size()]), Thread.currentThread().getContextClassLoader()) + ); + } + + /** + * Prevent instantiation. + */ + private NewDriver() { + } + + /** + * Generate an array of jar files located in a directory. + * Jar files located in sub directories will not be added. + * + * @param dir to search for the jar files. + */ + private static File[] listJars(File dir) { + if (dir.isDirectory()) { + return dir.listFiles((f, name) -> { + if (name.endsWith(".jar")) {// $NON-NLS-1$ + File jar = new File(f, name); + return jar.isFile() && jar.canRead(); + } + return false; + }); + } + return new File[0]; + } + + /** + * Add a URL to the loader classpath only; does not update the system classpath. + * + * @param path to be added. + * @throws MalformedURLException when path points to an invalid url + */ + public static void addURL(String path) throws MalformedURLException { + File furl = new File(path); + loader.addURL(furl.toURI().toURL()); // See Java bug 4496398 + File[] jars = listJars(furl); + for (File jar : jars) { + loader.addURL(jar.toURI().toURL()); // See Java bug 4496398 + } + } + + /** + * Add a URL to the loader classpath only; does not update the system + * classpath. + * + * @param url + * The {@link URL} to add to the classpath + */ + public static void addURL(URL url) { + loader.addURL(url); + } + + /** + * Add a directory or jar to the loader and system classpaths. + * + * @param path + * to add to the loader and system classpath + * @throws MalformedURLException + * if path can not be transformed to a valid + * {@link URL} + */ + public static void addPath(String path) throws MalformedURLException { + File file = new File(path); + // Ensure that directory URLs end in "/" + if (file.isDirectory() && !path.endsWith("/")) {// $NON-NLS-1$ + file = new File(path + "/");// $NON-NLS-1$ + } + loader.addURL(file.toURI().toURL()); // See Java bug 4496398 + StringBuilder sb = new StringBuilder(System.getProperty(JAVA_CLASS_PATH)); + sb.append(CLASSPATH_SEPARATOR); + sb.append(path); + File[] jars = listJars(file); + for (File jar : jars) { + loader.addURL(jar.toURI().toURL()); // See Java bug 4496398 + sb.append(CLASSPATH_SEPARATOR); + sb.append(jar.getPath()); + } + + // ClassFinder needs this + System.setProperty(JAVA_CLASS_PATH,sb.toString()); + } + + /** + * Get the directory where JMeter is installed. This is the absolute path + * name. + * + * @return the directory where JMeter is installed. + */ + public static String getJMeterDir() { + return JMETER_INSTALLATION_DIRECTORY; + } + + /** + * The main program which actually runs JMeter. + * + * @param args + * the command line arguments + */ + public static void main(String[] args) { + if(!EXCEPTIONS_IN_INIT.isEmpty()) { + System.err.println("Configuration error during init, see exceptions:"+exceptionsToString(EXCEPTIONS_IN_INIT)); // NOSONAR Intentional System.err use + } else { + Thread.currentThread().setContextClassLoader(loader); + + + setLoggingProperties(args); + + try { + // Only set property if it has not been set explicitely + if(System.getProperty(HEADLESS_MODE_PROPERTY) == null && shouldBeHeadless(args)) { + System.setProperty(HEADLESS_MODE_PROPERTY, "true"); + } + Class initialClass = loader.loadClass("org.apache.jmeter.JMeter");// $NON-NLS-1$ + Object instance = initialClass.getDeclaredConstructor().newInstance(); + Method startup = initialClass.getMethod("start", new Class[] { new String[0].getClass() });// $NON-NLS-1$ + startup.invoke(instance, new Object[] { args }); + } catch(Throwable e){ // NOSONAR We want to log home directory in case of exception + e.printStackTrace(); // NOSONAR No logger at this step + System.err.println("JMeter home directory was detected as: "+JMETER_INSTALLATION_DIRECTORY); // NOSONAR Intentional System.err use + } + } + } + + /** + * @param exceptionsInInit List of {@link Exception} + * @return String + */ + private static String exceptionsToString(List exceptionsInInit) { + StringBuilder builder = new StringBuilder(); + for (Exception exception : exceptionsInInit) { + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + exception.printStackTrace(printWriter); // NOSONAR + builder.append(stringWriter.toString()) + .append("\r\n"); + } + return builder.toString(); + } + + /* + * Set logging related system properties. + */ + private static void setLoggingProperties(String[] args) { + String jmLogFile = getCommandLineArgument(args, 'j', "jmeterlogfile");// $NON-NLS-1$ $NON-NLS-2$ + + if (jmLogFile != null && !jmLogFile.isEmpty()) { + jmLogFile = replaceDateFormatInFileName(jmLogFile); + System.setProperty(JMETER_LOGFILE_SYSTEM_PROPERTY, jmLogFile);// $NON-NLS-1$ + } else if (System.getProperty(JMETER_LOGFILE_SYSTEM_PROPERTY) == null) {// $NON-NLS-1$ + System.setProperty(JMETER_LOGFILE_SYSTEM_PROPERTY, "jmeter.log");// $NON-NLS-1$ $NON-NLS-2$ + } + + String jmLogConf = getCommandLineArgument(args, 'i', "jmeterlogconf");// $NON-NLS-1$ $NON-NLS-2$ + File logConfFile = null; + + if (jmLogConf != null && !jmLogConf.isEmpty()) { + logConfFile = new File(jmLogConf); + } else if (System.getProperty("log4j.configurationFile") == null) {// $NON-NLS-1$ + logConfFile = new File("log4j2.xml");// $NON-NLS-1$ + if (!logConfFile.isFile()) { + logConfFile = new File(JMETER_INSTALLATION_DIRECTORY, "jmeter.bin" + File.separator + "log4j2.xml");// $NON-NLS-1$ $NON-NLS-2$ + } + } + + if (logConfFile != null) { + System.setProperty("log4j.configurationFile", logConfFile.toURI().toString());// $NON-NLS-1$ + } + } + + private static boolean shouldBeHeadless(String[] args) { + for (String arg : args) { + if("-n".equals(arg) || "-s".equals(arg) || "-g".equals(arg)) { + return true; + } + } + return false; + } + /* + * Find command line argument option value by the id and name. + */ + private static String getCommandLineArgument(String[] args, int id, String name) { + final String shortArgName = "-" + ((char) id);// $NON-NLS-1$ + final String longArgName = "--" + name;// $NON-NLS-1$ + + String value = null; + + for (int i = 0; i < args.length; i++) { + if ((shortArgName.equals(args[i]) && i < args.length - 1) + || longArgName.equals(args[i])) { + if (!args[i + 1].startsWith("-")) {// $NON-NLS-1$ + value = args[i + 1]; + } + break; + } else if (!shortArgName.equals(args[i]) && args[i].startsWith(shortArgName)) { + value = args[i].substring(shortArgName.length()); + break; + } + } + + return value; + } + + /* + * If the fileName contains at least one set of paired single-quotes, reformat using DateFormat + */ + private static String replaceDateFormatInFileName(String fileName) { + try { + StringBuilder builder = new StringBuilder(); + + final Date date = new Date(); + int fromIndex = 0; + int begin = fileName.indexOf('\'', fromIndex);// $NON-NLS-1$ + int end; + + String format; + SimpleDateFormat dateFormat; + + while (begin != -1) { + builder.append(fileName.substring(fromIndex, begin)); + + fromIndex = begin + 1; + end = fileName.indexOf('\'', fromIndex);// $NON-NLS-1$ + if (end == -1) { + throw new IllegalArgumentException("Invalid pairs of single-quotes in the file name: " + fileName);// $NON-NLS-1$ + } + + format = fileName.substring(begin + 1, end); + dateFormat = new SimpleDateFormat(format); + builder.append(dateFormat.format(date)); + + fromIndex = end + 1; + begin = fileName.indexOf('\'', fromIndex);// $NON-NLS-1$ + } + + if (fromIndex < fileName.length() - 1) { + builder.append(fileName.substring(fromIndex)); + } + + return builder.toString(); + } catch (Exception ex) { + System.err.println("Error replacing date format in file name:"+fileName+", error:"+ex.getMessage()); // NOSONAR + } + + return fileName; + } +} diff --git a/src/main/java/org/apache/jmeter/assertions/JSONPathAssertion.java b/src/main/java/org/apache/jmeter/assertions/JSONPathAssertion.java new file mode 100644 index 0000000..ef459e7 --- /dev/null +++ b/src/main/java/org/apache/jmeter/assertions/JSONPathAssertion.java @@ -0,0 +1,272 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by Fernflower decompiler) +// + +package org.apache.jmeter.assertions; + +import com.google.gson.Gson; +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.Predicate; +import net.minidev.json.JSONArray; +import org.apache.commons.lang3.StringUtils; +import org.apache.jmeter.samplers.SampleResult; +import org.apache.jmeter.testelement.AbstractTestElement; +import org.apache.jmeter.testelement.ThreadListener; +import org.apache.jmeter.util.JMeterUtils; +import org.apache.oro.text.regex.Pattern; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Serializable; +import java.text.DecimalFormat; +import java.util.Map; + +public class JSONPathAssertion extends AbstractTestElement implements Serializable, Assertion, ThreadListener { + private static final Logger log = LoggerFactory.getLogger(JSONPathAssertion.class); + private static final long serialVersionUID = 2L; + public static final String JSONPATH = "JSON_PATH"; + public static final String EXPECTEDVALUE = "EXPECTED_VALUE"; + public static final String JSONVALIDATION = "JSONVALIDATION"; + public static final String EXPECT_NULL = "EXPECT_NULL"; + public static final String INVERT = "INVERT"; + public static final String ISREGEX = "ISREGEX"; + private static ThreadLocal decimalFormatter = ThreadLocal.withInitial(JSONPathAssertion::createDecimalFormat); + + public JSONPathAssertion() { + } + + private static DecimalFormat createDecimalFormat() { + DecimalFormat decimalFormatter = new DecimalFormat("#.#"); + decimalFormatter.setMaximumFractionDigits(340); + decimalFormatter.setMinimumFractionDigits(1); + return decimalFormatter; + } + + public String getOption() { + return getPropertyAsString("ASS_OPTION"); + } + + public String getJsonPath() { + return this.getPropertyAsString("JSON_PATH"); + } + + public void setJsonPath(String jsonPath) { + this.setProperty("JSON_PATH", jsonPath); + } + + public String getExpectedValue() { + return this.getPropertyAsString("EXPECTED_VALUE"); + } + + public void setExpectedValue(String expectedValue) { + this.setProperty("EXPECTED_VALUE", expectedValue); + } + + public void setJsonValidationBool(boolean jsonValidation) { + this.setProperty("JSONVALIDATION", jsonValidation); + } + + public void setExpectNull(boolean val) { + this.setProperty("EXPECT_NULL", val); + } + + public boolean isExpectNull() { + return this.getPropertyAsBoolean("EXPECT_NULL"); + } + + public boolean isJsonValidationBool() { + return this.getPropertyAsBoolean("JSONVALIDATION"); + } + + public void setInvert(boolean invert) { + this.setProperty("INVERT", invert); + } + + public boolean isInvert() { + return this.getPropertyAsBoolean("INVERT"); + } + + public void setIsRegex(boolean flag) { + this.setProperty("ISREGEX", flag); + } + + public boolean isUseRegex() { + return this.getPropertyAsBoolean("ISREGEX", true); + } + + private void doAssert(String jsonString) { + Object value = JsonPath.read(jsonString, this.getJsonPath(), new Predicate[0]); + if (this.isJsonValidationBool()) { + if (value instanceof JSONArray) { + if (this.arrayMatched((JSONArray) value)) { + return; + } + } else if (this.isExpectNull() && value == null || this.isEquals(value)) { + return; + } + + if (this.isExpectNull()) { + throw new IllegalStateException(String.format("Value expected to be null, but found '%s'", value)); + } else { + String msg = ""; + if (this.isUseRegex()) { + msg = "Value expected to match regexp '%s', but it did not match: '%s'"; + } else if (StringUtils.isNotEmpty(getOption()) && !this.isEquals(value)) { + switch (getOption()) { + case "CONTAINS": + msg = "Value contains to be '%s', but found '%s'"; + break; + case "NOT_CONTAINS": + msg = "Value not contains to be '%s', but found '%s'"; + break; + case "EQUALS": + msg = "Value equals to be '%s', but found '%s'"; + break; + case "NOT_EQUALS": + msg = "Value not equals to be '%s', but found '%s'"; + break; + case "GT": + msg = "Value > '%s', but found '%s'"; + break; + case "LT": + msg = "Value < '%s', but found '%s'"; + break; + } + } else { + msg = "Value expected to be '%s', but found '%s'"; + } + throw new IllegalStateException(String.format(msg, this.getExpectedValue(), objectToString(value))); + } + } + } + + private boolean arrayMatched(JSONArray value) { + if (value.isEmpty() && "[]".equals(this.getExpectedValue())) { + return true; + } else { + Object[] var2 = value.toArray(); + int var3 = var2.length; + + for (int var4 = 0; var4 < var3; ++var4) { + Object subj = var2[var4]; + if (subj == null && this.isExpectNull() || this.isEquals(subj)) { + return true; + } + } + + return this.isEquals(value); + } + } + + private boolean isGt(String v1, String v2) { + try { + return Long.parseLong(v1) > Long.parseLong(v2); + } catch (Exception e) { + return false; + } + } + + private boolean isLt(String v1, String v2) { + try { + return Long.parseLong(v1) < Long.parseLong(v2); + } catch (Exception e) { + return false; + } + } + + private boolean isEquals(Object subj) { + String str = objectToString(subj); + if (this.isUseRegex()) { + Pattern pattern = JMeterUtils.getPatternCache().getPattern(this.getExpectedValue()); + return JMeterUtils.getMatcher().matches(str, pattern); + } else { + if (StringUtils.isNotEmpty(getOption())) { + boolean refFlag = false; + switch (getOption()) { + case "CONTAINS": + refFlag = str.contains(getExpectedValue()); + break; + case "NOT_CONTAINS": + refFlag = !str.contains(getExpectedValue()); + break; + case "EQUALS": + refFlag = str.equals(getExpectedValue()); + break; + case "NOT_EQUALS": + refFlag = !str.equals(getExpectedValue()); + break; + case "GT": + refFlag = isGt(str, getExpectedValue()); + break; + case "LT": + refFlag = isLt(str, getExpectedValue()); + break; + + } + return refFlag; + } + return str.equals(this.getExpectedValue()); + } + } + + public AssertionResult getResult(SampleResult samplerResult) { + AssertionResult result = new AssertionResult(this.getName()); + String responseData = samplerResult.getResponseDataAsString(); + if (responseData.isEmpty()) { + return result.setResultForNull(); + } else { + result.setFailure(false); + result.setFailureMessage(""); + if (!this.isInvert()) { + try { + this.doAssert(responseData); + } catch (Exception var6) { + log.debug("Assertion failed", var6); + result.setFailure(true); + result.setFailureMessage(var6.getMessage()); + } + } else { + try { + this.doAssert(responseData); + result.setFailure(true); + if (this.isJsonValidationBool()) { + if (this.isExpectNull()) { + result.setFailureMessage("Failed that JSONPath " + this.getJsonPath() + " not matches null"); + } else { + result.setFailureMessage("Failed that JSONPath " + this.getJsonPath() + " not matches " + this.getExpectedValue()); + } + } else { + result.setFailureMessage("Failed that JSONPath not exists: " + this.getJsonPath()); + } + } catch (Exception var5) { + log.debug("Assertion failed, as expected", var5); + } + } + + return result; + } + } + + public static String objectToString(Object subj) { + String str; + if (subj == null) { + str = "null"; + } else if (subj instanceof Map) { + str = new Gson().toJson(subj); + } else if (!(subj instanceof Double) && !(subj instanceof Float)) { + str = subj.toString(); + } else { + str = ((DecimalFormat) decimalFormatter.get()).format(subj); + } + + return str; + } + + public void threadStarted() { + } + + public void threadFinished() { + decimalFormatter.remove(); + } +} diff --git a/src/main/java/org/apache/jmeter/config/CSVDataSet.java b/src/main/java/org/apache/jmeter/config/CSVDataSet.java new file mode 100644 index 0000000..7fd554f --- /dev/null +++ b/src/main/java/org/apache/jmeter/config/CSVDataSet.java @@ -0,0 +1,346 @@ +/* + * 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.config; + +import org.apache.commons.lang3.StringUtils; +import org.apache.jmeter.engine.event.LoopIterationEvent; +import org.apache.jmeter.engine.event.LoopIterationListener; +import org.apache.jmeter.engine.util.NoConfigMerge; +import org.apache.jmeter.gui.GUIMenuSortOrder; +import org.apache.jmeter.gui.TestElementMetadata; +import org.apache.jmeter.save.CSVSaveService; +import org.apache.jmeter.services.FileServer; +import org.apache.jmeter.testbeans.TestBean; +import org.apache.jmeter.testbeans.gui.GenericTestBeanCustomizer; +import org.apache.jmeter.testelement.property.JMeterProperty; +import org.apache.jmeter.testelement.property.StringProperty; +import org.apache.jmeter.threads.JMeterContext; +import org.apache.jmeter.threads.JMeterVariables; +import org.apache.jmeter.util.JMeterUtils; +import org.apache.jorphan.util.JMeterStopThreadException; +import org.apache.jorphan.util.JOrphanUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.io.IOException; +import java.util.ResourceBundle; + +/** + * Read lines from a file and split int variables. + *

+ * The iterationStart() method is used to set up each set of values. + *

+ * By default, the same file is shared between all threads + * (and other thread groups, if they use the same file name). + *

+ * The shareMode can be set to: + *

    + *
  • All threads - default, as described above
  • + *
  • Current thread group
  • + *
  • Current thread
  • + *
  • Identifier - all threads sharing the same identifier
  • + *
+ *

+ * The class uses the FileServer alias mechanism to provide the different share modes. + * For all threads, the file alias is set to the file name. + * Otherwise, a suffix is appended to the filename to make it unique within the required context. + * For current thread group, the thread group identityHashcode is used; + * for individual threads, the thread hashcode is used as the suffix. + * Or the user can provide their own suffix, in which case the file is shared between all + * threads with the same suffix. + */ +@GUIMenuSortOrder(1) +@TestElementMetadata(labelResource = "displayName") +public class CSVDataSet extends ConfigTestElement + implements TestBean, LoopIterationListener, NoConfigMerge { + private static final Logger log = LoggerFactory.getLogger(CSVDataSet.class); + + private static final long serialVersionUID = 233L; + + private static final String EOFVALUE = // value to return at EOF + JMeterUtils.getPropDefault("csvdataset.eofstring", ""); //$NON-NLS-1$ //$NON-NLS-2$ + + private transient String filename; + + private transient String fileEncoding; + + private transient String variableNames; + + private transient String delimiter; + + private transient boolean quoted; + + private transient boolean recycle = true; + + private transient boolean stopThread; + + private transient String[] vars; + + private transient String alias; + + private transient String shareMode; + + private boolean firstLineIsNames = false; + + private boolean ignoreFirstLine = false; + + private Object readResolve() { + recycle = true; + return this; + } + + /** + * Override the setProperty method in order to convert + * the original String shareMode property. + * This used the locale-dependent display value, so caused + * problems when the language was changed. + * If the "shareMode" value matches a resource value then it is converted + * into the resource key. + * To reduce the need to look up resources, we only attempt to + * convert values with spaces in them, as these are almost certainly + * not variables (and they are definitely not resource keys). + */ + @Override + public void setProperty(JMeterProperty property) { + if (!(property instanceof StringProperty)) { + super.setProperty(property); + return; + } + + final String propName = property.getName(); + if (!"shareMode".equals(propName)) { + super.setProperty(property); + return; + } + + final String propValue = property.getStringValue(); + if (propValue.contains(" ")) { // variables are unlikely to contain spaces, so most likely a translation + try { + final BeanInfo beanInfo = Introspector.getBeanInfo(this.getClass()); + final ResourceBundle rb = (ResourceBundle) beanInfo.getBeanDescriptor().getValue(GenericTestBeanCustomizer.RESOURCE_BUNDLE); + for (String resKey : CSVDataSetBeanInfo.getShareTags()) { + if (propValue.equals(rb.getString(resKey))) { + if (log.isDebugEnabled()) { + log.debug("Converted {}={} to {} using Locale: {}", propName, propValue, resKey, rb.getLocale()); + } + ((StringProperty) property).setValue(resKey); // reset the value + super.setProperty(property); + return; + } + } + // This could perhaps be a variable name + log.warn("Could not translate {}={} using Locale: {}", propName, propValue, rb.getLocale()); + } catch (IntrospectionException e) { + log.error("Could not find BeanInfo; cannot translate shareMode entries", e); + } + } + super.setProperty(property); + } + + @Override + public void iterationStart(LoopIterationEvent iterEvent) { + FileServer server = FileServer.getFileServer(); + final JMeterContext context = getThreadContext(); + String delim = getDelimiter(); + if ("\\t".equals(delim)) { // $NON-NLS-1$ + delim = "\t";// Make it easier to enter a Tab // $NON-NLS-1$ + } else if (delim.isEmpty()) { + log.debug("Empty delimiter, will use ','"); + delim = ","; + } + //if (vars == null) { + initVars(server, context, delim); + //} + + // TODO: fetch this once as per vars above? + JMeterVariables threadVars = context.getVariables(); + String[] lineValues = {}; + try { + if (getQuotedData()) { + lineValues = server.getParsedLine(alias, recycle, + firstLineIsNames || ignoreFirstLine, delim.charAt(0)); + } else { + String line = server.readLine(alias, recycle, + firstLineIsNames || ignoreFirstLine); + lineValues = JOrphanUtils.split(line, delim, false); + } + for (int a = 0; a < vars.length && a < lineValues.length; a++) { + threadVars.put(vars[a], lineValues[a]); + } + } catch (IOException e) { // treat the same as EOF + log.error(e.toString()); + } + if (lineValues.length == 0) {// i.e. EOF + if (getStopThread()) { + throw new JMeterStopThreadException("End of file:" + getFilename() + " detected for CSV DataSet:" + + getName() + " configured with stopThread:" + getStopThread() + ", recycle:" + getRecycle()); + } + for (String var : vars) { + threadVars.put(var, EOFVALUE); + } + } + } + + private void initVars(FileServer server, final JMeterContext context, String delim) { + String fileName = getFilename().trim(); + setAlias(context, fileName); + final String names = getVariableNames(); + if (StringUtils.isEmpty(names)) { + String header = server.reserveFile(fileName, getFileEncoding(), alias, true); + try { + vars = CSVSaveService.csvSplitString(header, delim.charAt(0)); + firstLineIsNames = true; + } catch (IOException e) { + throw new IllegalArgumentException("Could not split CSV header line from file:" + fileName, e); + } + } else { + server.reserveFile(fileName, getFileEncoding(), alias, ignoreFirstLine); + vars = JOrphanUtils.split(names, ","); // $NON-NLS-1$ + } + trimVarNames(vars); + } + + private void setAlias(final JMeterContext context, String alias) { + String mode = getShareMode(); + int modeInt = CSVDataSetBeanInfo.getShareModeAsInt(mode); + switch (modeInt) { + case CSVDataSetBeanInfo.SHARE_ALL: + this.alias = alias; + break; + case CSVDataSetBeanInfo.SHARE_GROUP: + this.alias = alias + "@" + System.identityHashCode(context.getThreadGroup()); + break; + case CSVDataSetBeanInfo.SHARE_THREAD: + this.alias = alias + "@" + System.identityHashCode(context.getThread()); + break; + default: + this.alias = alias + "@" + mode; // user-specified key + break; + } + } + + /** + * trim content of array varNames + * + * @param varsNames + */ + private void trimVarNames(String[] varsNames) { + for (int i = 0; i < varsNames.length; i++) { + varsNames[i] = varsNames[i].trim(); + } + } + + /** + * @return Returns the filename. + */ + public String getFilename() { + return filename; + } + + /** + * @param filename The filename to set. + */ + public void setFilename(String filename) { + this.filename = filename; + } + + /** + * @return Returns the file encoding. + */ + public String getFileEncoding() { + return fileEncoding; + } + + /** + * @param fileEncoding The fileEncoding to set. + */ + public void setFileEncoding(String fileEncoding) { + this.fileEncoding = fileEncoding; + } + + /** + * @return Returns the variableNames. + */ + public String getVariableNames() { + return variableNames; + } + + /** + * @param variableNames The variableNames to set. + */ + public void setVariableNames(String variableNames) { + this.variableNames = variableNames; + } + + public String getDelimiter() { + return delimiter; + } + + public void setDelimiter(String delimiter) { + this.delimiter = delimiter; + } + + public boolean getQuotedData() { + return quoted; + } + + public void setQuotedData(boolean quoted) { + this.quoted = quoted; + } + + public boolean getRecycle() { + return recycle; + } + + public void setRecycle(boolean recycle) { + this.recycle = recycle; + } + + public boolean getStopThread() { + return stopThread; + } + + public void setStopThread(boolean value) { + this.stopThread = value; + } + + public String getShareMode() { + return shareMode; + } + + public void setShareMode(String value) { + this.shareMode = value; + } + + /** + * @return the ignoreFirstLine + */ + public boolean isIgnoreFirstLine() { + return ignoreFirstLine; + } + + /** + * @param ignoreFirstLine the ignoreFirstLine to set + */ + public void setIgnoreFirstLine(boolean ignoreFirstLine) { + this.ignoreFirstLine = ignoreFirstLine; + } +} diff --git a/src/main/java/org/apache/jmeter/config/KeystoreConfig.java b/src/main/java/org/apache/jmeter/config/KeystoreConfig.java new file mode 100644 index 0000000..16e4fcf --- /dev/null +++ b/src/main/java/org/apache/jmeter/config/KeystoreConfig.java @@ -0,0 +1,174 @@ +/* + * 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.config; + +import org.apache.commons.lang3.StringUtils; +import org.apache.jmeter.gui.TestElementMetadata; +import org.apache.jmeter.testbeans.TestBean; +import org.apache.jmeter.testelement.TestStateListener; +import org.apache.jmeter.util.JMeterUtils; +import org.apache.jmeter.util.SSLManager; +import org.apache.jorphan.util.JMeterStopTestException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Configure Keystore + */ +@TestElementMetadata(labelResource = "displayName") +public class KeystoreConfig extends ConfigTestElement implements TestBean, TestStateListener { + + private static final long serialVersionUID = 1L; + private static final Logger log = LoggerFactory.getLogger(KeystoreConfig.class); + + private static final String KEY_STORE_START_INDEX = "https.keyStoreStartIndex"; // $NON-NLS-1$ + private static final String KEY_STORE_END_INDEX = "https.keyStoreEndIndex"; // $NON-NLS-1$ + + private String startIndex; + private String endIndex; + private String preload; + private String clientCertAliasVarName; + + public KeystoreConfig() { + super(); + } + + @Override + public void testEnded() { + testEnded(null); + } + + @Override + public void testEnded(String host) { + log.info("Destroying Keystore"); + SSLManager.getInstance().destroyKeystore(); + } + + @Override + public void testStarted() { + testStarted(null); + } + + @Override + public void testStarted(String host) { + String reuseSSLContext = JMeterUtils.getProperty("https.use.cached.ssl.context"); + if (StringUtils.isEmpty(reuseSSLContext) || "true".equals(reuseSSLContext)) { + log.warn("https.use.cached.ssl.context property must be set to false to ensure Multiple Certificates are used"); + } + int startIndexAsInt = JMeterUtils.getPropDefault(KEY_STORE_START_INDEX, 0); + int endIndexAsInt = JMeterUtils.getPropDefault(KEY_STORE_END_INDEX, -1); + + if (!StringUtils.isEmpty(this.startIndex)) { + try { + startIndexAsInt = Integer.parseInt(this.startIndex); + } catch (NumberFormatException e) { + log.warn("Failed parsing startIndex: {}, will default to: {}, error message: {}", this.startIndex, + startIndexAsInt, e, e); + } + } + + if (!StringUtils.isEmpty(this.endIndex)) { + try { + endIndexAsInt = Integer.parseInt(this.endIndex); + } catch (NumberFormatException e) { + log.warn("Failed parsing endIndex: {}, will default to: {}, error message: {}", this.endIndex, + endIndexAsInt, e, e); + } + } + if (endIndexAsInt != -1 && startIndexAsInt > endIndexAsInt) { + throw new JMeterStopTestException("Keystore Config error : Alias start index must be lower than Alias end index"); + } + log.info( + "Configuring Keystore with (preload: '{}', startIndex: {}, endIndex: {}, clientCertAliasVarName: '{}')", + preload, startIndexAsInt, endIndexAsInt, clientCertAliasVarName); + // 加载认证文件 + String path = this.getPropertyAsString("MS-KEYSTORE-FILE-PATH"); + String password = this.getPropertyAsString("MS-KEYSTORE-FILE-PASSWORD"); + InputStream in = null; + try { + in = new FileInputStream(new File(path)); + } catch (IOException e) { + log.error(e.getMessage()); + } + SSLManager.getInstance().configureKeystore(Boolean.parseBoolean(preload), + startIndexAsInt, + endIndexAsInt, + clientCertAliasVarName, in, password); + } + + /** + * @return the endIndex + */ + public String getEndIndex() { + return endIndex; + } + + /** + * @param endIndex the endIndex to set + */ + public void setEndIndex(String endIndex) { + this.endIndex = endIndex; + } + + /** + * @return the startIndex + */ + public String getStartIndex() { + return startIndex; + } + + /** + * @param startIndex the startIndex to set + */ + public void setStartIndex(String startIndex) { + this.startIndex = startIndex; + } + + /** + * @return the preload + */ + public String getPreload() { + return preload; + } + + /** + * @param preload the preload to set + */ + public void setPreload(String preload) { + this.preload = preload; + } + + /** + * @return the clientCertAliasVarName + */ + public String getClientCertAliasVarName() { + return clientCertAliasVarName; + } + + /** + * @param clientCertAliasVarName the clientCertAliasVarName to set + */ + public void setClientCertAliasVarName(String clientCertAliasVarName) { + this.clientCertAliasVarName = clientCertAliasVarName; + } +} diff --git a/src/main/java/org/apache/jmeter/reporters/ResultAction.java b/src/main/java/org/apache/jmeter/reporters/ResultAction.java new file mode 100644 index 0000000..10e611e --- /dev/null +++ b/src/main/java/org/apache/jmeter/reporters/ResultAction.java @@ -0,0 +1,98 @@ +/* + * 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.reporters; + +import org.apache.jmeter.samplers.SampleEvent; +import org.apache.jmeter.samplers.SampleListener; +import org.apache.jmeter.samplers.SampleResult; +import org.apache.jmeter.testelement.OnErrorTestElement; +import org.apache.jmeter.threads.JMeterContext.TestLogicalAction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Serializable; + +/** + * ResultAction - take action based on the status of the last Result + */ +public class ResultAction extends OnErrorTestElement implements Serializable, SampleListener { + + private static final long serialVersionUID = 242L; + + private static final Logger log = LoggerFactory.getLogger(ResultAction.class); + + /** + * Constructor is initially called once for each occurrence in the test plan + * For GUI, several more instances are created Then clear is called at start + * of test Called several times during test startup The name will not + * necessarily have been set at this point. + */ + public ResultAction() { + super(); + } + + /** + * Examine the sample(s) and take appropriate action + * + * @see SampleListener#sampleOccurred(SampleEvent) + */ + @Override + public void sampleOccurred(SampleEvent e) { + SampleResult s = e.getResult(); + if (log.isDebugEnabled()) { + log.debug("ResultStatusHandler {} for {} OK? {}", getName(), s.getSampleLabel(), s.isSuccessful()); + } + if (!s.isSuccessful()) { + if (isStopTestNow()) { + s.setStopTestNow(true); + } else if (isStopTest()) { + s.setStopTest(true); + } else if (isStopThread()) { + s.setStopThread(true); + } else if (isStartNextThreadLoop()) { + s.setTestLogicalAction(TestLogicalAction.START_NEXT_ITERATION_OF_THREAD); + } else if (isStartNextIterationOfCurrentLoop()) { + s.setTestLogicalAction(TestLogicalAction.START_NEXT_ITERATION_OF_CURRENT_LOOP); + } else if (isBreakCurrentLoop()) { + s.setTestLogicalAction(TestLogicalAction.BREAK_CURRENT_LOOP); + } + } else { + if (getErrorAction() == 1000) { + s.setTestLogicalAction(TestLogicalAction.BREAK_CURRENT_LOOP); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public void sampleStarted(SampleEvent e) { + // not used + } + + /** + * {@inheritDoc} + */ + @Override + public void sampleStopped(SampleEvent e) { + // not used + } + +} diff --git a/src/main/java/org/apache/jmeter/samplers/SampleResult.java b/src/main/java/org/apache/jmeter/samplers/SampleResult.java new file mode 100644 index 0000000..529ab10 --- /dev/null +++ b/src/main/java/org/apache/jmeter/samplers/SampleResult.java @@ -0,0 +1,1644 @@ +/* + * 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.samplers; + +import org.apache.jmeter.assertions.AssertionResult; +import org.apache.jmeter.gui.Searchable; +import org.apache.jmeter.testelement.TestPlan; +import org.apache.jmeter.threads.JMeterContext.TestLogicalAction; +import org.apache.jmeter.threads.JMeterContextService; +import org.apache.jmeter.util.JMeterUtils; +import org.apache.jorphan.util.JOrphanUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +// For unit tests, @see TestSampleResult + +/** + * This is a nice packaging for the various information returned from taking a + * sample of an entry. + */ +public class SampleResult implements Serializable, Cloneable, Searchable { + + private static final long serialVersionUID = 241L; + + // Needs to be accessible from Test code + static Logger log = LoggerFactory.getLogger(SampleResult.class); + + /** + * The default encoding to be used if not overridden. + * The value is ISO-8859-1. + */ + public static final String DEFAULT_HTTP_ENCODING = StandardCharsets.ISO_8859_1.name(); + + private static final String OK_CODE = Integer.toString(HttpURLConnection.HTTP_OK); + private static final String OK_MSG = "OK"; // $NON-NLS-1$ + private static final String INVALID_CALL_SEQUENCE_MSG = "Invalid call sequence"; // $NON-NLS-1$ + + + // Bug 33196 - encoding ISO-8859-1 is only suitable for Western countries + // However the suggested System.getProperty("file.encoding") is Cp1252 on + // Windows + // So use a new property with the original value as default + // needs to be accessible from test code + /** + * The default encoding to be used to decode the responseData byte array. + * The value is defined by the property "sampleresult.default.encoding" + * with a default of DEFAULT_HTTP_ENCODING if that is not defined. + */ + protected static final String DEFAULT_ENCODING + = JMeterUtils.getPropDefault("sampleresult.default.encoding", // $NON-NLS-1$ + DEFAULT_HTTP_ENCODING); + + /** + * The default used by {@link #setResponseData(String, String)} + */ + private static final String DEFAULT_CHARSET = Charset.defaultCharset().name(); + + /** + * Data type value ({@value}) indicating that the response data is text. + * + * @see #getDataType + * @see #setDataType(String) + */ + public static final String TEXT = "text"; // $NON-NLS-1$ + + /** + * Data type value ({@value}) indicating that the response data is binary. + * + * @see #getDataType + * @see #setDataType(String) + */ + public static final String BINARY = "bin"; // $NON-NLS-1$ + + private static final boolean DISABLE_SUBRESULTS_RENAMING = JMeterUtils.getPropDefault("subresults.disable_renaming", false); + + // List of types that are known to be binary + private static final String[] BINARY_TYPES = { + "image/", //$NON-NLS-1$ + "audio/", //$NON-NLS-1$ + "video/", //$NON-NLS-1$ + }; + + // List of types that are known to be ascii, although they may appear to be binary + private static final String[] NON_BINARY_TYPES = { + "audio/x-mpegurl", //$NON-NLS-1$ (HLS Media Manifest) + "audio/mpegurl", //$NON-NLS-1$ (HLS Media Manifest) + "video/f4m", //$NON-NLS-1$ (Flash Media Manifest) + "image/svg+xml" //$NON-NLS-1$ (SVG is xml) + }; + + + /** + * empty array which can be returned instead of null + */ + private static final byte[] EMPTY_BA = new byte[0]; + + private static final SampleResult[] EMPTY_SR = new SampleResult[0]; + + private static final AssertionResult[] EMPTY_AR = new AssertionResult[0]; + + private static final boolean START_TIMESTAMP = + JMeterUtils.getPropDefault("sampleresult.timestamp.start", false); // $NON-NLS-1$ + + /** + * Allow read-only access from test code + */ + private static final boolean USE_NANO_TIME = + JMeterUtils.getPropDefault("sampleresult.useNanoTime", true); // $NON-NLS-1$ + + /** + * How long between checks of nanotime; default 5000ms; set to <=0 to disable the thread + */ + private static final long NANOTHREAD_SLEEP = + JMeterUtils.getPropDefault("sampleresult.nanoThreadSleep", 5000); // $NON-NLS-1$ + + private static final String NULL_FILENAME = "NULL"; + + static { + if (START_TIMESTAMP) { + log.info("Note: Sample TimeStamps are START times"); + } else { + log.info("Note: Sample TimeStamps are END times"); + } + log.info("sampleresult.default.encoding is set to {}", DEFAULT_ENCODING); + log.info("sampleresult.useNanoTime={}", USE_NANO_TIME); + log.info("sampleresult.nanoThreadSleep={}", NANOTHREAD_SLEEP); + + if (USE_NANO_TIME && NANOTHREAD_SLEEP > 0) { + // Make sure we start with a reasonable value + NanoOffset.nanoOffset = System.currentTimeMillis() - SampleResult.sampleNsClockInMs(); + NanoOffset nanoOffset = new NanoOffset(); + nanoOffset.setDaemon(true); + nanoOffset.setName("NanoOffset"); + nanoOffset.start(); + } + } + + /** + * 定制自定义添加 ================= + */ + private String samplerId; + + private String resourceId; + + public String getSamplerId() { + return this.samplerId; + } + + public String getResourceId() { + return this.resourceId; + } + + // 数据格式 List 多层父级按照同级统计 + private String scenario; + + public String getScenario() { + return this.scenario; + } + + /** + * 定制自定义添加 ================= + */ + private SampleSaveConfiguration saveConfig; + + private SampleResult parent; + + private byte[] responseData = EMPTY_BA; + + private String responseCode = "";// Never return null + + private String label = "";// Never return null + + /** + * Filename used by ResultSaver + */ + private String resultFileName = ""; + + /** + * The data used by the sampler + */ + private String samplerData; + + private String threadName = ""; // Never return null + + private String responseMessage = ""; + + private String responseHeaders = ""; // Never return null + + private String requestHeaders = ""; + + /** + * timeStamp == 0 means either not yet initialised or no stamp available (e.g. when loading a results file) + * the time stamp - can be start or end + */ + private long timeStamp = 0; + + private long startTime = 0; + + private long endTime = 0; + + private long idleTime = 0;// Allow for non-sample time + + /** + * Start of pause (if any) + */ + private long pauseTime = 0; + + private List assertionResults; + + private List subResults; + + /** + * The data type of the sample + * + * @see #getDataType() + * @see #setDataType(String) + * @see #TEXT + * @see #BINARY + */ + private String dataType = ""; // Don't return null if not set + + private boolean success; + + /** + * Files that this sample has been saved in. + * In Non GUI mode and when best config is used, size never exceeds 1, + * but as a compromise set it to 2 + */ + private final Set files = ConcurrentHashMap.newKeySet(2); + + // TODO do contentType and/or dataEncoding belong in HTTPSampleResult instead? + private String dataEncoding;// (is this really the character set?) e.g. + // ISO-8895-1, UTF-8 + + private String contentType = ""; // e.g. text/html; charset=utf-8 + + /** + * elapsed time + */ + private long elapsedTime = 0; + + /** + * time to first response + */ + private long latency = 0; + + /** + * time to end connecting + */ + private long connectTime = 0; + + /** + * Way to signal what to do on Test + */ + private TestLogicalAction testLogicalAction = TestLogicalAction.CONTINUE; + + /** + * Should thread terminate? + */ + private boolean stopThread = false; + + /** + * Should test terminate? + */ + private boolean stopTest = false; + + /** + * Should test terminate abruptly? + */ + private boolean stopTestNow = false; + + private int sampleCount = 1; + + private long bytes = 0; // Allows override of sample size in case sampler does not want to store all the data + + private int headersSize = 0; + + private long bodySize = 0; + + /** + * Currently active threads in this thread group + */ + private volatile int groupThreads = 0; + + /** + * Currently active threads in all thread groups + */ + private volatile int allThreads = 0; + + private final long nanoTimeOffset; + + // Allow testcode access to the settings + final boolean useNanoTime; + + final long nanoThreadSleep; + + private long sentBytes; + + private URL location; + + private transient boolean ignore; + + private transient int subResultIndex; + + /** + * Cache for responseData as string to avoid multiple computations + */ + private transient volatile String responseDataAsString; + + public SampleResult() { + this(USE_NANO_TIME, NANOTHREAD_SLEEP); + } + + // Allow test code to change the default useNanoTime setting + SampleResult(boolean nanoTime) { + this(nanoTime, NANOTHREAD_SLEEP); + } + + // Allow test code to change the default useNanoTime and nanoThreadSleep settings + SampleResult(boolean nanoTime, long nanoThreadSleep) { + this.elapsedTime = 0; + this.useNanoTime = nanoTime; + this.nanoThreadSleep = nanoThreadSleep; + this.nanoTimeOffset = initOffset(); + // 增加请求ID的获取 + Sampler sampler = JMeterContextService.getContext().getCurrentSampler(); + if (sampler != null) { + this.samplerId = sampler.getPropertyAsString("MS-ID"); + this.resourceId = sampler.getPropertyAsString("MS-RESOURCE-ID"); + this.scenario = sampler.getPropertyAsString("MS-SCENARIO"); + } + + } + + /** + * Copy constructor. + * + * @param res existing sample result + */ + public SampleResult(SampleResult res) { + this(); + allThreads = res.allThreads;//OK + assertionResults = res.assertionResults; + bytes = res.bytes; + headersSize = res.headersSize; + bodySize = res.bodySize; + contentType = res.contentType;//OK + dataEncoding = res.dataEncoding;//OK + dataType = res.dataType;//OK + endTime = res.endTime;//OK + // files is created automatically, and applies per instance + groupThreads = res.groupThreads;//OK + idleTime = res.idleTime; + label = res.label;//OK + latency = res.latency; + connectTime = res.connectTime; + location = res.location;//OK + parent = res.parent; + pauseTime = res.pauseTime; + requestHeaders = res.requestHeaders;//OK + responseCode = res.responseCode;//OK + responseData = res.responseData;//OK + responseDataAsString = null; + responseHeaders = res.responseHeaders;//OK + responseMessage = res.responseMessage;//OK + + // Don't copy this; it is per instance resultFileName = res.resultFileName; + + sampleCount = res.sampleCount; + samplerData = res.samplerData; + saveConfig = res.saveConfig; + sentBytes = res.sentBytes; + startTime = res.startTime;//OK + stopTest = res.stopTest; + stopTestNow = res.stopTestNow; + stopThread = res.stopThread; + testLogicalAction = res.testLogicalAction; + subResults = res.subResults; + success = res.success;//OK + threadName = res.threadName;//OK + elapsedTime = res.elapsedTime; + timeStamp = res.timeStamp; + } + + /** + * Create a sample with a specific elapsed time but don't allow the times to + * be changed later + *

+ * (only used by HTTPSampleResult) + * + * @param elapsed time + * @param atend create the sample finishing now, else starting now + */ + protected SampleResult(long elapsed, boolean atend) { + this(); + long now = currentTimeInMillis(); + if (atend) { + setTimes(now - elapsed, now); + } else { + setTimes(now, now + elapsed); + } + } + + /** + * Allow users to create a sample with specific timestamp and elapsed times + * for cloning purposes, but don't allow the times to be changed later + *

+ * Currently used by CSVSaveService and + * StatisticalSampleResult + * + * @param stamp this may be a start time or an end time (both in + * milliseconds) + * @param elapsed time in milliseconds + */ + public SampleResult(long stamp, long elapsed) { + this(); + stampAndTime(stamp, elapsed); + } + + private long initOffset() { + if (useNanoTime) { + return nanoThreadSleep > 0 ? NanoOffset.getNanoOffset() : System.currentTimeMillis() - sampleNsClockInMs(); + } else { + return Long.MIN_VALUE; + } + } + + /** + * @param propertiesToSave The propertiesToSave to set. + */ + public void setSaveConfig(SampleSaveConfiguration propertiesToSave) { + this.saveConfig = propertiesToSave; + } + + public SampleSaveConfiguration getSaveConfig() { + return saveConfig; + } + + public boolean isStampedAtStart() { + return START_TIMESTAMP; + } + + /** + * Create a sample with specific start and end times for test purposes, but + * don't allow the times to be changed later + *

+ * (used by StatVisualizerModel.Test) + * + * @param start start time in milliseconds since unix epoch + * @param end end time in milliseconds since unix epoch + * @return sample with given start and end time + */ + public static SampleResult createTestSample(long start, long end) { + SampleResult res = new SampleResult(); + res.setStartTime(start); + res.setEndTime(end); + return res; + } + + /** + * Create a sample with a specific elapsed time for test purposes, but don't + * allow the times to be changed later + * + * @param elapsed - desired elapsed time in milliseconds + * @return sample that starts 'now' and ends elapsed milliseconds later + */ + public static SampleResult createTestSample(long elapsed) { + long now = System.currentTimeMillis(); + return createTestSample(now, now + elapsed); + } + + private static long sampleNsClockInMs() { + return System.nanoTime() / 1000000; + } + + /** + * Helper method to get 1 ms resolution timing. + * + * @return the current time in milliseconds + * @throws RuntimeException when useNanoTime is true but + * nanoTimeOffset is not set + */ + public long currentTimeInMillis() { + if (useNanoTime) { + if (nanoTimeOffset == Long.MIN_VALUE) { + throw new IllegalStateException("Invalid call; nanoTimeOffset has not been set"); + } + return sampleNsClockInMs() + nanoTimeOffset; + } + return System.currentTimeMillis(); + } + + // Helper method to maintain timestamp relationships + private void stampAndTime(long stamp, long elapsed) { + if (START_TIMESTAMP) { + startTime = stamp; + endTime = stamp + elapsed; + } else { + startTime = stamp - elapsed; + endTime = stamp; + } + timeStamp = stamp; + elapsedTime = elapsed; + } + + /** + * For use by SaveService only. + * + * @param stamp this may be a start time or an end time (both in milliseconds) + * @param elapsed time in milliseconds + * @throws RuntimeException when startTime or endTime has been + * set already + */ + public void setStampAndTime(long stamp, long elapsed) { + if (startTime != 0 || endTime != 0) { + throw new IllegalStateException("Calling setStampAndTime() after start/end times have been set"); + } + stampAndTime(stamp, elapsed); + } + + /** + * Set the "marked" flag to show that the result has been written to the file. + * + * @param filename the name of the file + * @return true if the result was previously marked + */ + public boolean markFile(String filename) { + return !files.add(filename != null ? filename : NULL_FILENAME); + } + + public String getResponseCode() { + return responseCode; + } + + /** + * Set response code to OK, i.e. "200" + */ + public void setResponseCodeOK() { + responseCode = OK_CODE; + } + + public void setResponseCode(String code) { + responseCode = code; + } + + public boolean isResponseCodeOK() { + return responseCode.equals(OK_CODE); + } + + public String getResponseMessage() { + return responseMessage; + } + + public void setResponseMessage(String msg) { + responseMessage = msg; + } + + public void setResponseMessageOK() { + responseMessage = OK_MSG; + } + + /** + * Set result statuses OK - shorthand method to set: + *

    + *
  • ResponseCode
  • + *
  • ResponseMessage
  • + *
  • Successful status
  • + *
+ */ + public void setResponseOK() { + setResponseCodeOK(); + setResponseMessageOK(); + setSuccessful(true); + } + + public String getThreadName() { + return threadName; + } + + public void setThreadName(String threadName) { + this.threadName = threadName; + } + + /** + * Get the sample timestamp, which may be either the start time or the end time. + * + * @return timeStamp in milliseconds + * @see #getStartTime() + * @see #getEndTime() + */ + public long getTimeStamp() { + return timeStamp; + } + + public String getSampleLabel() { + return label; + } + + /** + * Get the sample label for use in summary reports etc. + * + * @param includeGroup whether to include the thread group name + * @return the label + */ + public String getSampleLabel(boolean includeGroup) { + if (includeGroup) { + return threadName.substring(0, threadName.lastIndexOf(' ')) + ":" + label; + } + return label; + } + + public void setSampleLabel(String label) { + this.label = label; + } + + public void addAssertionResult(AssertionResult assertResult) { + if (assertionResults == null) { + assertionResults = new ArrayList<>(); + } + assertionResults.add(assertResult); + } + + /** + * Gets the assertion results associated with this sample. + * + * @return an array containing the assertion results for this sample. + * Returns empty array if there are no assertion results. + */ + public AssertionResult[] getAssertionResults() { + if (assertionResults == null) { + return EMPTY_AR; + } + return assertionResults.toArray(new AssertionResult[assertionResults.size()]); + } + + /** + * Add a subresult and adjust the parent byte count and end-time. + * + * @param subResult the {@link SampleResult} to be added + */ + public void addSubResult(SampleResult subResult) { + addSubResult(subResult, isRenameSampleLabel()); + } + + /** + * see https://bz.apache.org/bugzilla/show_bug.cgi?id=63055 + * + * @return true if TestPlan is in functional mode or property subresults.disable_renaming is true + */ + public static boolean isRenameSampleLabel() { + return !(TestPlan.getFunctionalMode() || DISABLE_SUBRESULTS_RENAMING); + } + + /** + * Add a subresult and adjust the parent byte count and end-time. + * + * @param subResult the {@link SampleResult} to be added + * @param renameSubResults boolean do we rename subResults based on position + */ + public void addSubResult(SampleResult subResult, boolean renameSubResults) { + if (subResult == null) { + // see https://bz.apache.org/bugzilla/show_bug.cgi?id=54778 + return; + } + String tn = getThreadName(); + if (tn.length() == 0) { + tn = Thread.currentThread().getName(); + this.setThreadName(tn); + } + subResult.setThreadName(tn); + + // Extend the time to the end of the added sample + setEndTime(Math.max(getEndTime(), subResult.getEndTime() + nanoTimeOffset - subResult.nanoTimeOffset)); // Bug 51855 + // Include the byte count for the added sample + setBytes(getBytesAsLong() + subResult.getBytesAsLong()); + setSentBytes(getSentBytes() + subResult.getSentBytes()); + setHeadersSize(getHeadersSize() + subResult.getHeadersSize()); + setBodySize(getBodySizeAsLong() + subResult.getBodySizeAsLong()); + addRawSubResult(subResult, renameSubResults); + } + + /** + * Add a subresult to the collection without updating any parent fields. + * + * @param subResult the {@link SampleResult} to be added + */ + public void addRawSubResult(SampleResult subResult) { + storeSubResult(subResult, isRenameSampleLabel()); + } + + /** + * Add a subresult to the collection without updating any parent fields. + * + * @param subResult the {@link SampleResult} to be added + */ + private void addRawSubResult(SampleResult subResult, boolean renameSubResults) { + storeSubResult(subResult, renameSubResults); + } + + /** + * Add a subresult read from a results file. + *

+ * As for {@link SampleResult#addSubResult(SampleResult) + * addSubResult(SampleResult)}, except that the fields don't need to be + * accumulated + * + * @param subResult the {@link SampleResult} to be added + */ + public void storeSubResult(SampleResult subResult) { + storeSubResult(subResult, isRenameSampleLabel()); + } + + /** + * Add a subresult read from a results file. + *

+ * As for {@link SampleResult#addSubResult(SampleResult) + * addSubResult(SampleResult)}, except that the fields don't need to be + * accumulated + * + * @param subResult the {@link SampleResult} to be added + * @param renameSubResults boolean do we rename subResults based on position + */ + private void storeSubResult(SampleResult subResult, boolean renameSubResults) { + if (subResults == null) { + subResults = new ArrayList<>(); + } + if (renameSubResults) { + subResult.setSampleLabel(getSampleLabel() + "-" + subResultIndex++); + } + subResults.add(subResult); + subResult.setParent(this); + } + + /** + * Gets the subresults associated with this sample. + * + * @return an array containing the subresults for this sample. Returns an + * empty array if there are no subresults. + */ + public SampleResult[] getSubResults() { + if (subResults == null) { + return EMPTY_SR; + } + return subResults.toArray(new SampleResult[subResults.size()]); + } + + /** + * Sets the responseData attribute of the SampleResult object. + *

+ * If the parameter is null, then the responseData is set to an empty byte array. + * This ensures that getResponseData() can never be null. + * + * @param response the new responseData value + */ + public void setResponseData(byte[] response) { + responseDataAsString = null; + responseData = response == null ? EMPTY_BA : response; + } + + /** + * Sets the responseData attribute of the SampleResult object. + * Should only be called after setting the dataEncoding (if necessary) + * + * @param response the new responseData value (String) + * @deprecated - only intended for use from BeanShell code + */ + @Deprecated + public void setResponseData(String response) { + responseDataAsString = null; + try { + responseData = response.getBytes(getDataEncodingWithDefault()); + } catch (UnsupportedEncodingException e) { + log.warn("Could not convert string, using default encoding. " + e.getLocalizedMessage()); + responseData = response.getBytes(Charset.defaultCharset()); // N.B. default charset is used deliberately here + } + } + + /** + * Sets the encoding and responseData attributes of the SampleResult object. + * + * @param response the new responseData value (String) + * @param encoding the encoding to set and then use (if null, use platform default) + */ + public void setResponseData(final String response, final String encoding) { + responseDataAsString = null; + String encodeUsing = encoding != null ? encoding : DEFAULT_CHARSET; + try { + responseData = response.getBytes(encodeUsing); + setDataEncoding(encodeUsing); + } catch (UnsupportedEncodingException e) { + log.warn("Could not convert string using '" + encodeUsing + + "', using default encoding: " + DEFAULT_CHARSET, e); + responseData = response.getBytes(Charset.defaultCharset()); // N.B. default charset is used deliberately here + setDataEncoding(DEFAULT_CHARSET); + } + } + + /** + * Gets the responseData attribute of the SampleResult object. + *

+ * Note that some samplers may not store all the data, in which case + * getResponseData().length will be incorrect. + *

+ * Instead, always use {@link #getBytes()} to obtain the sample result byte count. + *

+ * + * @return the responseData value (cannot be null) + */ + public byte[] getResponseData() { + return responseData; + } + + /** + * Gets the responseData of the SampleResult object as a String + * + * @return the responseData value as a String, converted according to the encoding + */ + public String getResponseDataAsString() { + try { + if (responseDataAsString == null) { + responseDataAsString = new String(responseData, getDataEncodingWithDefault()); + } + return responseDataAsString; + } catch (UnsupportedEncodingException e) { + log.warn("Using platform default as " + getDataEncodingWithDefault() + " caused " + e); + return new String(responseData, Charset.defaultCharset()); // N.B. default charset is used deliberately here + } + } + + public void setSamplerData(String s) { + samplerData = s; + } + + public String getSamplerData() { + return samplerData; + } + + /** + * Get the time it took this sample to occur. + * + * @return elapsed time in milliseconds + */ + public long getTime() { + return elapsedTime; + } + + public boolean isSuccessful() { + return success; + } + + /** + * Sets the data type of the sample. + * + * @param dataType String containing {@link #BINARY} or {@link #TEXT} + * @see #BINARY + * @see #TEXT + */ + public void setDataType(String dataType) { + this.dataType = dataType; + } + + /** + * Returns the data type of the sample. + * + * @return String containing {@link #BINARY} or {@link #TEXT} or the empty string + * @see #BINARY + * @see #TEXT + */ + public String getDataType() { + return dataType; + } + + /** + * Extract and save the DataEncoding and DataType from the parameter provided. + * Does not save the full content Type. + * + * @param ct - content type (may be null) + * @see #setContentType(String) which should be used to save the full content-type string + */ + public void setEncodingAndType(String ct) { + if (ct != null) { + // Extract charset and store as DataEncoding + // N.B. The meta tag: + // + // is now processed by HTTPSampleResult#getDataEncodingWithDefault + final String charsetPrefix = "charset="; // $NON-NLS-1$ + int cset = ct.toLowerCase(java.util.Locale.ENGLISH).indexOf(charsetPrefix); + if (cset >= 0) { + String charSet = ct.substring(cset + charsetPrefix.length()); + // handle: ContentType: text/plain; charset=ISO-8859-1; format=flowed + int semiColon = charSet.indexOf(';'); + if (semiColon >= 0) { + charSet = charSet.substring(0, semiColon); + } + // Check for quoted string + if (charSet.startsWith("\"") || charSet.startsWith("\'")) { // $NON-NLS-1$ + setDataEncoding(charSet.substring(1, charSet.length() - 1)); // remove quotes + } else { + setDataEncoding(charSet); + } + } + if (isBinaryType(ct)) { + setDataType(BINARY); + } else { + setDataType(TEXT); + } + } + } + + /* + * Determine if content-type is known to be binary, i.e. not displayable as text. + * + * @param ct content type + * @return true if content-type is of type binary. + */ + public static boolean isBinaryType(String ct) { + for (String entry : NON_BINARY_TYPES) { + if (ct.startsWith(entry)) { + return false; + } + } + for (String binaryType : BINARY_TYPES) { + if (ct.startsWith(binaryType)) { + return true; + } + } + return false; + } + + /** + * Sets the successful attribute of the SampleResult object. + * + * @param success the new successful value + */ + public void setSuccessful(boolean success) { + this.success = success; + } + + /** + * Returns the display name. + * + * @return display name of this sample result + */ + @Override + public String toString() { + return getSampleLabel(); + } + + /** + * Returns the dataEncoding or the default if no dataEncoding was provided. + * + * @return the value of the dataEncoding or DEFAULT_ENCODING + */ + public String getDataEncodingWithDefault() { + return getDataEncodingWithDefault(DEFAULT_ENCODING); + } + + /** + * Returns the dataEncoding or the default if no dataEncoding was provided. + * + * @param defaultEncoding the default to be applied + * @return the value of the dataEncoding or the provided default + */ + protected String getDataEncodingWithDefault(String defaultEncoding) { + if (dataEncoding != null && dataEncoding.length() > 0) { + return dataEncoding; + } + return defaultEncoding; + } + + /** + * Returns the dataEncoding. May be null or the empty String. + * + * @return the value of the dataEncoding + */ + public String getDataEncodingNoDefault() { + return dataEncoding; + } + + /** + * Sets the dataEncoding. + * + * @param dataEncoding the dataEncoding to set, e.g. ISO-8895-1, UTF-8 + */ + public void setDataEncoding(String dataEncoding) { + this.dataEncoding = dataEncoding; + } + + /** + * @return whether to stop the test waiting for current running Sampler to end + */ + public boolean isStopTest() { + return stopTest; + } + + /** + * @return whether to stop the test now interrupting current running samplers + */ + public boolean isStopTestNow() { + return stopTestNow; + } + + /** + * @return whether to stop this thread + */ + public boolean isStopThread() { + return stopThread; + } + + public void setStopTest(boolean b) { + stopTest = b; + } + + public void setStopTestNow(boolean b) { + stopTestNow = b; + } + + public void setStopThread(boolean b) { + stopThread = b; + } + + /** + * @return the request headers + */ + public String getRequestHeaders() { + return requestHeaders; + } + + /** + * @return the response headers + */ + public String getResponseHeaders() { + return responseHeaders; + } + + /** + * @param string - + * request headers + */ + public void setRequestHeaders(String string) { + requestHeaders = string; + } + + /** + * @param string - + * response headers + */ + public void setResponseHeaders(String string) { + responseHeaders = string; + } + + /** + * @return the full content type - e.g. text/html [;charset=utf-8 ] + */ + public String getContentType() { + return contentType; + } + + /** + * Get the media type from the Content Type + * + * @return the media type - e.g. text/html (without charset, if any) + */ + public String getMediaType() { + return JOrphanUtils.trim(contentType, " ;").toLowerCase(java.util.Locale.ENGLISH); + } + + /** + * Stores the content-type string, e.g. text/xml; charset=utf-8 + * + * @param string the content-type to be set + * @see #setEncodingAndType(String) which can be used to extract the charset. + */ + public void setContentType(String string) { + contentType = string; + } + + /** + * @return idleTime + */ + public long getIdleTime() { + return idleTime; + } + + /** + * @return the end time + */ + public long getEndTime() { + return endTime; + } + + /** + * @return the start time + */ + public long getStartTime() { + return startTime; + } + + /* + * Helper methods N.B. setStartTime must be called before setEndTime + * + * setStartTime is used by HTTPSampleResult to clone the parent sampler and + * allow the original start time to be kept + */ + protected final void setStartTime(long start) { + startTime = start; + if (START_TIMESTAMP) { + timeStamp = startTime; + } + } + + public void setEndTime(long end) { + endTime = end; + if (!START_TIMESTAMP) { + timeStamp = endTime; + } + if (startTime == 0) { + log.error("setEndTime must be called after setStartTime", new Throwable(INVALID_CALL_SEQUENCE_MSG)); + } else { + elapsedTime = endTime - startTime - idleTime; + } + } + + /** + * Set idle time pause. + * For use by SampleResultConverter/CSVSaveService. + * + * @param idle long + */ + public void setIdleTime(long idle) { + idleTime = idle; + } + + private void setTimes(long start, long end) { + setStartTime(start); + setEndTime(end); + } + + /** + * Record the start time of a sample + */ + public void sampleStart() { + if (startTime == 0) { + setStartTime(currentTimeInMillis()); + } else { + log.error("sampleStart called twice", new Throwable(INVALID_CALL_SEQUENCE_MSG)); + } + } + + /** + * Record the end time of a sample and calculate the elapsed time + */ + public void sampleEnd() { + if (endTime == 0) { + setEndTime(currentTimeInMillis()); + } else { + log.error("sampleEnd called twice", new Throwable(INVALID_CALL_SEQUENCE_MSG)); + } + } + + /** + * Pause a sample + */ + public void samplePause() { + if (pauseTime != 0) { + log.error("samplePause called twice", new Throwable(INVALID_CALL_SEQUENCE_MSG)); + } + pauseTime = currentTimeInMillis(); + } + + /** + * Resume a sample + */ + public void sampleResume() { + if (pauseTime == 0) { + log.error("sampleResume without samplePause", new Throwable(INVALID_CALL_SEQUENCE_MSG)); + } + idleTime += currentTimeInMillis() - pauseTime; + pauseTime = 0; + } + + /** + * When a Sampler is working as a monitor + * + * @param monitor flag whether this sampler is working as a monitor + * @deprecated since 3.2 NOOP + */ + @Deprecated + public void setMonitor(boolean monitor) { + // NOOP + } + + /** + * If the sampler is a monitor, method will return true. + * + * @return true if the sampler is a monitor + * @deprecated since 3.2 always return false + */ + @Deprecated + public boolean isMonitor() { + return false; + } + + /** + * The statistical sample sender aggregates several samples to save on + * transmission costs. + * + * @param count number of samples represented by this instance + */ + public void setSampleCount(int count) { + sampleCount = count; + } + + /** + * return the sample count. by default, the value is 1. + * + * @return the sample count + */ + public int getSampleCount() { + return sampleCount; + } + + /** + * Returns the count of errors. + * + * @return 0 - or 1 if the sample failed + *

+ * TODO do we need allow for nested samples? + */ + public int getErrorCount() { + return success ? 0 : 1; + } + + public void setErrorCount(int i) {// for reading from CSV files + // ignored currently + } + + /* + * TODO: error counting needs to be sorted out. + * + * At present the Statistical Sampler tracks errors separately + * It would make sense to move the error count here, but this would + * mean lots of changes. + * It's also tricky maintaining the count - it can't just be incremented/decremented + * when the success flag is set as this may be done multiple times. + * The work-round for now is to do the work in the StatisticalSampleResult, + * which overrides this method. + * Note that some JMS samplers also create samples with > 1 sample count + * Also the Transaction Controller probably needs to be changed to do + * proper sample and error accounting. + * The purpose of this work-round is to allow at least minimal support for + * errors in remote statistical batch mode. + * + */ + + /** + * In the event the sampler does want to pass back the actual contents, we + * still want to calculate the throughput. The bytes are the bytes of the + * response data. + * + * @param length the number of bytes of the response data for this sample + */ + public void setBytes(long length) { + bytes = length; + } + + /** + * In the event the sampler does want to pass back the actual contents, we + * still want to calculate the throughput. The bytes are the bytes of the + * response data. + * + * @param length the number of bytes of the response data for this sample + * @deprecated use setBytes(long) + */ + @Deprecated + public void setBytes(int length) { + setBytes((long) length); + } + + /** + * @param sentBytesCount long sent bytes + */ + public void setSentBytes(long sentBytesCount) { + sentBytes = sentBytesCount; + } + + /** + * @return the sentBytes + */ + public long getSentBytes() { + return sentBytes; + } + + /** + * return the bytes returned by the response. + * + * @return byte count + * @deprecated use getBytesAsLong + */ + @Deprecated + public int getBytes() { + return (int) getBytesAsLong(); + } + + /** + * return the bytes returned by the response. + * + * @return byte count + */ + public long getBytesAsLong() { + long tmpSum = this.getHeadersSize() + this.getBodySizeAsLong(); + return tmpSum == 0 ? bytes : tmpSum; + } + + /** + * @return Returns the latency. + */ + public long getLatency() { + return latency; + } + + /** + * Set the time to the first response + */ + public void latencyEnd() { + latency = currentTimeInMillis() - startTime - idleTime; + } + + /** + * This is only intended for use by SampleResultConverter! + * + * @param latency The latency to set. + */ + public void setLatency(long latency) { + this.latency = latency; + } + + /** + * @return Returns the connect time. + */ + public long getConnectTime() { + return connectTime; + } + + /** + * Set the time to the end of connecting + */ + public void connectEnd() { + connectTime = currentTimeInMillis() - startTime - idleTime; + } + + /** + * This is only intended for use by SampleResultConverter! + * + * @param time The connect time to set. + */ + public void setConnectTime(long time) { + this.connectTime = time; + } + + /** + * This is only intended for use by SampleResultConverter! + * + * @param timeStamp The timeStamp to set. + */ + public void setTimeStamp(long timeStamp) { + this.timeStamp = timeStamp; + } + + + public void setURL(URL location) { + this.location = location; + } + + public URL getURL() { + return location; + } + + /** + * Get a String representation of the URL (if defined). + * + * @return ExternalForm of URL, or empty string if url is null + */ + public String getUrlAsString() { + return location == null ? "" : location.toExternalForm(); + } + + /** + * @return Returns the parent. + */ + public SampleResult getParent() { + return parent; + } + + /** + * @param parent The parent to set. + */ + public void setParent(SampleResult parent) { + this.parent = parent; + } + + public String getResultFileName() { + return resultFileName; + } + + public void setResultFileName(String resultFileName) { + this.resultFileName = resultFileName; + } + + public int getGroupThreads() { + return groupThreads; + } + + public void setGroupThreads(int n) { + this.groupThreads = n; + } + + public int getAllThreads() { + return allThreads; + } + + public void setAllThreads(int n) { + this.allThreads = n; + } + + // Bug 47394 + + /** + * Allow custom SampleSenders to drop unwanted assertionResults + */ + public void removeAssertionResults() { + this.assertionResults = null; + } + + /** + * Allow custom SampleSenders to drop unwanted subResults + */ + public void removeSubResults() { + this.subResults = null; + } + + /** + * Set the headers size in bytes + * + * @param size the number of bytes of the header + */ + public void setHeadersSize(int size) { + this.headersSize = size; + } + + /** + * Get the headers size in bytes + * + * @return the headers size + */ + public int getHeadersSize() { + return headersSize; + } + + /** + * @return the body size in bytes + * @deprecated replaced by getBodySizeAsLong() + */ + @Deprecated + public int getBodySize() { + return (int) getBodySizeAsLong(); + } + + /** + * @return the body size in bytes + */ + public long getBodySizeAsLong() { + return bodySize == 0 ? responseData.length : bodySize; + } + + /** + * @param bodySize the body size to set + */ + public void setBodySize(long bodySize) { + this.bodySize = bodySize; + } + + /** + * @param bodySize the body size to set + * @deprecated use setBodySize(long) + */ + @Deprecated + public void setBodySize(int bodySize) { + this.bodySize = bodySize; + } + + private static class NanoOffset extends Thread { + + private static volatile long nanoOffset; + + static long getNanoOffset() { + return nanoOffset; + } + + @Override + public void run() { + // Wait longer than a clock pulse (generally 10-15ms) + getOffset(30L); // Catch an early clock pulse to reduce slop. + while (true) { + getOffset(NANOTHREAD_SLEEP); // Can now afford to wait a bit longer between checks + } + } + + private static void getOffset(long wait) { + try { + TimeUnit.MILLISECONDS.sleep(wait); + long clock = System.currentTimeMillis(); + long nano = SampleResult.sampleNsClockInMs(); + nanoOffset = clock - nano; + } catch (InterruptedException ignore) { + // ignored + Thread.currentThread().interrupt(); + } + } + } + + /** + * @return the startNextThreadLoop + * @deprecated use {@link SampleResult#getTestLogicalAction()} + */ + @Deprecated + public boolean isStartNextThreadLoop() { + return testLogicalAction == TestLogicalAction.START_NEXT_ITERATION_OF_THREAD; + } + + /** + * @param startNextThreadLoop the startNextLoop to set + * @deprecated use SampleResult#setTestLogicalAction(TestLogicalAction) + */ + @Deprecated + public void setStartNextThreadLoop(boolean startNextThreadLoop) { + if (startNextThreadLoop) { + testLogicalAction = TestLogicalAction.START_NEXT_ITERATION_OF_THREAD; + } else { + testLogicalAction = TestLogicalAction.CONTINUE; + } + } + + /** + * Clean up cached data + */ + public void cleanAfterSample() { + this.responseDataAsString = null; + } + + @Override + public Object clone() { + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + throw new IllegalStateException("This should not happen"); + } + } + + @Override + public List getSearchableTokens() throws Exception { + List datasToSearch = new ArrayList<>(4); + datasToSearch.add(getSampleLabel()); + datasToSearch.add(getResponseDataAsString()); + datasToSearch.add(getRequestHeaders()); + datasToSearch.add(getResponseHeaders()); + return datasToSearch; + } + + /** + * @return boolean true if this SampleResult should not be sent to Listeners + */ + public boolean isIgnore() { + return ignore; + } + + /** + * Call this method to tell JMeter to ignore this SampleResult by Listeners + */ + public void setIgnore() { + this.ignore = true; + } + + /** + * @return String first non null assertion failure message if assertionResults is not null, null otherwise + */ + public String getFirstAssertionFailureMessage() { + String message = null; + AssertionResult[] results = getAssertionResults(); + + if (results != null) { + // Find the first non-null message + for (AssertionResult result : results) { + message = result.getFailureMessage(); + if (message != null) { + break; + } + } + } + return message; + } + + /** + * @return the testLogicalAction + */ + public TestLogicalAction getTestLogicalAction() { + return testLogicalAction; + } + + /** + * @param testLogicalAction the testLogicalAction to set + */ + public void setTestLogicalAction(TestLogicalAction testLogicalAction) { + this.testLogicalAction = testLogicalAction; + } +} diff --git a/src/main/java/org/apache/jmeter/util/JSR223BeanInfoSupport.java b/src/main/java/org/apache/jmeter/util/JSR223BeanInfoSupport.java new file mode 100644 index 0000000..67568d3 --- /dev/null +++ b/src/main/java/org/apache/jmeter/util/JSR223BeanInfoSupport.java @@ -0,0 +1,93 @@ +/* + * 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.util; + +import org.apache.jmeter.testbeans.TestBean; + +import javax.script.ScriptEngineFactory; +import javax.script.ScriptEngineManager; +import java.util.*; +import java.util.Map.Entry; + +/** + * 解决JSR233加载 ScriptEngineFactory 空指针问题 + */ +public abstract class JSR223BeanInfoSupport extends ScriptingBeanInfoSupport { + + private static final String[] LANGUAGE_TAGS; + + /** + * Will be removed in next version following 3.2 + * @deprecated use {@link JSR223BeanInfoSupport#getLanguageNames()} + */ + @Deprecated + public static final String[][] LANGUAGE_NAMES; // NOSONAR Kept for backward compatibility + + private static final String[][] CONSTANT_LANGUAGE_NAMES; + + static { + Map nameMap = new HashMap<>(); + ScriptEngineManager sem = new ScriptEngineManager(); + final List engineFactories = sem.getEngineFactories(); + for(ScriptEngineFactory fact : engineFactories){ + List names = fact.getNames(); + for(String shortName : names) { + if (shortName != null) { + nameMap.put(shortName.toLowerCase(Locale.ENGLISH), fact); + } + } + } + LANGUAGE_TAGS = nameMap.keySet().toArray(new String[nameMap.size()]); + Arrays.sort(LANGUAGE_TAGS); + CONSTANT_LANGUAGE_NAMES = new String[nameMap.size()][2]; + int i = 0; + for(Entry me : nameMap.entrySet()) { + final String key = me.getKey(); + CONSTANT_LANGUAGE_NAMES[i][0] = key; + final ScriptEngineFactory fact = me.getValue(); + CONSTANT_LANGUAGE_NAMES[i++][1] = key + + " (" // $NON-NLS-1$ + + fact.getLanguageName() + " " + fact.getLanguageVersion() // $NON-NLS-1$ + + " / " // $NON-NLS-1$ + + fact.getEngineName() + " " + fact.getEngineVersion() // $NON-NLS-1$ + + ")"; // $NON-NLS-1$ + } + + LANGUAGE_NAMES = getLanguageNames(); // NOSONAR Kept for backward compatibility + } + + private static final ResourceBundle NAME_BUNDLE = new ListResourceBundle() { + @Override + protected Object[][] getContents() { + return CONSTANT_LANGUAGE_NAMES; + } + }; + + protected JSR223BeanInfoSupport(Class beanClass) { + super(beanClass, LANGUAGE_TAGS, NAME_BUNDLE); + } + + /** + * @return String array of 2 columns array containing Script engine short name / Script Language details + */ + public static final String[][] getLanguageNames() { + return CONSTANT_LANGUAGE_NAMES.clone(); + } + +} diff --git a/src/main/java/org/apache/jmeter/util/JSR223TestElement.java b/src/main/java/org/apache/jmeter/util/JSR223TestElement.java new file mode 100644 index 0000000..2dd7645 --- /dev/null +++ b/src/main/java/org/apache/jmeter/util/JSR223TestElement.java @@ -0,0 +1,389 @@ +/* + * 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.util; + +import io.metersphere.api.jmeter.utils.FileUtils; +import io.metersphere.node.util.LogUtil; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.collections.map.LRUMap; +import org.apache.commons.lang3.StringUtils; +import org.apache.jmeter.NewDriver; +import org.apache.jmeter.samplers.SampleResult; +import org.apache.jmeter.samplers.Sampler; +import org.apache.jmeter.testelement.TestStateListener; +import org.apache.jmeter.threads.JMeterContext; +import org.apache.jmeter.threads.JMeterContextService; +import org.apache.jmeter.threads.JMeterVariables; +import org.apache.jorphan.util.JOrphanUtils; +import org.codehaus.groovy.jsr223.GroovyScriptEngineImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.script.*; +import java.io.*; +import java.util.Collections; +import java.util.Map; +import java.util.Properties; + + +/** + * Base class for JSR223 Test elements + *

+ * 将当前类加载器设置为 loader ,解决由系统类加载器加载的 JMeter 无法动态加载 jar 包问题 + * 同时隔离在 beanshell 中访问由系统类加载器加载的其他类 + */ +public abstract class JSR223TestElement extends ScriptingTestElement + implements Serializable, TestStateListener { + private static final long serialVersionUID = 232L; + + private static final Logger logger = LoggerFactory.getLogger(JSR223TestElement.class); + /** + * Cache of compiled scripts + */ + @SuppressWarnings("unchecked") // LRUMap does not support generics (yet) + private static final Map compiledScriptsCache = + Collections.synchronizedMap( + new LRUMap(JMeterUtils.getPropDefault("jsr223.compiled_scripts_cache_size", 100))); + + /** + * If not empty then script in ScriptText will be compiled and cached + */ + private String cacheKey = ""; + + /** + * md5 of the script, used as an unique key for the cache + */ + private String scriptMd5 = null; + + /** + * Initialization On Demand Holder pattern + */ + private static class LazyHolder { + private LazyHolder() { + super(); + } + + public static final ScriptEngineManager INSTANCE = new ScriptEngineManager(); + } + + /** + * @return ScriptEngineManager singleton + */ + public static ScriptEngineManager getInstance() { + return LazyHolder.INSTANCE; + } + + public JSR223TestElement() { + super(); + } + + /** + * @return {@link ScriptEngine} for language defaulting to groovy if language is not set + * @throws ScriptException when no {@link ScriptEngine} could be found + */ + protected ScriptEngine getScriptEngine() throws ScriptException { + String lang = getScriptLanguageWithDefault(); + ScriptEngine scriptEngine = getInstance().getEngineByName(lang); + if (scriptEngine == null) { + throw new ScriptException("Cannot find engine named: '" + lang + "', ensure you set language field in JSR223 Test Element: " + getName()); + } + + return scriptEngine; + } + + /** + * @return script language or DEFAULT_SCRIPT_LANGUAGE if none is set + */ + private String getScriptLanguageWithDefault() { + String lang = getScriptLanguage(); + if (StringUtils.isNotEmpty(lang)) { + return lang; + } + return DEFAULT_SCRIPT_LANGUAGE; + } + + /** + * Populate variables to be passed to scripts + * + * @param bindings Bindings + */ + protected void populateBindings(Bindings bindings) { + final String label = getName(); + final String fileName = getFilename(); + final String scriptParameters = getParameters(); + // Use actual class name for log + final Logger elementLogger = LoggerFactory.getLogger(getClass().getName() + "." + getName()); + bindings.put("log", elementLogger); // $NON-NLS-1$ (this name is fixed) + bindings.put("Label", label); // $NON-NLS-1$ (this name is fixed) + bindings.put("FileName", fileName); // $NON-NLS-1$ (this name is fixed) + bindings.put("Parameters", scriptParameters); // $NON-NLS-1$ (this name is fixed) + String[] args = JOrphanUtils.split(scriptParameters, " ");//$NON-NLS-1$ + bindings.put("args", args); // $NON-NLS-1$ (this name is fixed) + // Add variables for access to context and variables + JMeterContext jmctx = JMeterContextService.getContext(); + bindings.put("ctx", jmctx); // $NON-NLS-1$ (this name is fixed) + JMeterVariables vars = jmctx.getVariables(); + bindings.put("vars", vars); // $NON-NLS-1$ (this name is fixed) + Properties props = JMeterUtils.getJMeterProperties(); + bindings.put("props", props); // $NON-NLS-1$ (this name is fixed) + // For use in debugging: + bindings.put("OUT", System.out); // NOSONAR $NON-NLS-1$ (this name is fixed) + + // Most subclasses will need these: + Sampler sampler = jmctx.getCurrentSampler(); + bindings.put("sampler", sampler); // $NON-NLS-1$ (this name is fixed) + SampleResult prev = jmctx.getPreviousResult(); + bindings.put("prev", prev); // $NON-NLS-1$ (this name is fixed) + } + + + /** + * This method will run inline script or file script with special behaviour for file script: + * - If ScriptEngine implements Compilable script will be compiled and cached + * - If not if will be run + * + * @param scriptEngine ScriptEngine + * @param pBindings {@link Bindings} might be null + * @return Object returned by script + * @throws IOException when reading the script fails + * @throws ScriptException when compiling or evaluation of the script fails + */ + protected Object processFileOrScript(ScriptEngine scriptEngine, final Bindings pBindings) + throws IOException, ScriptException { + + this.loadGroovyJar(scriptEngine); + + Bindings bindings = pBindings; + if (bindings == null) { + bindings = scriptEngine.createBindings(); + } + populateBindings(bindings); + File scriptFile = new File(getFilename()); + // Hack: bsh-2.0b5.jar BshScriptEngine implements Compilable but throws + // "java.lang.Error: unimplemented" + boolean supportsCompilable = scriptEngine instanceof Compilable + && !("bsh.engine.BshScriptEngine".equals(scriptEngine.getClass().getName())); // NOSONAR // $NON-NLS-1$ + + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + // 将当前类加载器设置为 loader ,解决由系统类加载器加载的 JMeter 无法动态加载 jar 包问题 + // 同时隔离在 beanshell 中访问由系统类加载器加载的其他类 + NewDriver.setContextClassLoader(); + + try { + if (!StringUtils.isEmpty(getFilename())) { + if (scriptFile.exists() && scriptFile.canRead()) { + if (supportsCompilable) { + String newCacheKey = getScriptLanguage() + "#" + // $NON-NLS-1$ + scriptFile.getAbsolutePath() + "#" + // $NON-NLS-1$ + scriptFile.lastModified(); + CompiledScript compiledScript = compiledScriptsCache.get(newCacheKey); + if (compiledScript == null) { + synchronized (compiledScriptsCache) { + compiledScript = compiledScriptsCache.get(newCacheKey); + if (compiledScript == null) { + // TODO Charset ? + try (BufferedReader fileReader = new BufferedReader(new FileReader(scriptFile), + (int) scriptFile.length())) { + compiledScript = ((Compilable) scriptEngine).compile(fileReader); + compiledScriptsCache.put(newCacheKey, compiledScript); + } + } + } + } + return compiledScript.eval(bindings); + } else { + // TODO Charset ? + try (BufferedReader fileReader = new BufferedReader(new FileReader(scriptFile), + (int) scriptFile.length())) { + return scriptEngine.eval(fileReader, bindings); + } + } + } else { + throw new ScriptException("Script file '" + scriptFile.getAbsolutePath() + + "' does not exist or is unreadable for element:" + getName()); + } + } else if (!StringUtils.isEmpty(getScript())) { + if (supportsCompilable && + !ScriptingBeanInfoSupport.FALSE_AS_STRING.equals(cacheKey)) { + computeScriptMD5(); + CompiledScript compiledScript = compiledScriptsCache.get(this.scriptMd5); + if (compiledScript == null) { + synchronized (compiledScriptsCache) { + compiledScript = compiledScriptsCache.get(this.scriptMd5); + if (compiledScript == null) { + compiledScript = ((Compilable) scriptEngine).compile(getScript()); + compiledScriptsCache.put(this.scriptMd5, compiledScript); + } + } + } + + return compiledScript.eval(bindings); + } else { + return scriptEngine.eval(getScript(), bindings); + } + } else { + throw new ScriptException("Both script file and script text are empty for element:" + getName()); + } + } catch (ScriptException ex) { + Throwable rootCause = ex.getCause(); + if (isStopCondition(rootCause)) { + throw (RuntimeException) ex.getCause(); + } else { + throw ex; + } + } finally { + // 将当前类加载器设置回来,避免加载无法加载到系统类加载加载类 + Thread.currentThread().setContextClassLoader(contextClassLoader); + } + } + + + /** + * groovy 使用的是自己的类加载器, + * 这里再执行脚本前,使用 groovy的加载器加载jar包, + * 解决groovy脚本无法使用jar包的问题 + * + * @param scriptEngine + * @Auth jianxing + */ + public static void loadGroovyJar(ScriptEngine scriptEngine) { + if (scriptEngine instanceof GroovyScriptEngineImpl) { + GroovyScriptEngineImpl groovyScriptEngine = (GroovyScriptEngineImpl) scriptEngine; + + try { + File file = new File(FileUtils.JAR_FILE_DIR); + if (file.isFile()) { + groovyScriptEngine.getClassLoader().addURL(file.toURI().toURL()); + } else { + File[] files = file.listFiles(); + if(files!= null && files.length > 0){ + for (File f : files) { + groovyScriptEngine.getClassLoader().addURL(f.toURI().toURL()); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + LogUtil.error(e.getMessage(), e); + } + } + } + + /** + * @return boolean true if element is not compilable or if compilation succeeds + * @throws IOException if script is missing + * @throws ScriptException if compilation fails + */ + public boolean compile() + throws ScriptException, IOException { + String lang = getScriptLanguageWithDefault(); + ScriptEngine scriptEngine = getInstance().getEngineByName(lang); + boolean supportsCompilable = scriptEngine instanceof Compilable + && !("bsh.engine.BshScriptEngine".equals(scriptEngine.getClass().getName())); // NOSONAR // $NON-NLS-1$ + if (!supportsCompilable) { + return true; + } + if (!StringUtils.isEmpty(getScript())) { + try { + ((Compilable) scriptEngine).compile(getScript()); + return true; + } catch (ScriptException e) { // NOSONAR + logger.error("Error compiling script for test element {}, error:{}", getName(), e.getMessage()); + return false; + } + } else { + File scriptFile = new File(getFilename()); + try (BufferedReader fileReader = new BufferedReader(new FileReader(scriptFile), + (int) scriptFile.length())) { + try { + ((Compilable) scriptEngine).compile(fileReader); + return true; + } catch (ScriptException e) { // NOSONAR + logger.error("Error compiling script for test element {}, error:{}", getName(), e.getMessage()); + return false; + } + } + } + } + + /** + * compute MD5 if it is null + */ + private void computeScriptMD5() { + // compute the md5 of the script if needed + if (scriptMd5 == null) { + scriptMd5 = DigestUtils.md5Hex(getScript()); + } + } + + /** + * @return the cacheKey + */ + public String getCacheKey() { + return cacheKey; + } + + /** + * @param cacheKey the cacheKey to set + */ + public void setCacheKey(String cacheKey) { + this.cacheKey = cacheKey; + } + + /** + * @see org.apache.jmeter.testelement.TestStateListener#testStarted() + */ + @Override + public void testStarted() { + // NOOP + } + + /** + * @see org.apache.jmeter.testelement.TestStateListener#testStarted(java.lang.String) + */ + @Override + public void testStarted(String host) { + // NOOP + } + + /** + * @see org.apache.jmeter.testelement.TestStateListener#testEnded() + */ + @Override + public void testEnded() { + testEnded(""); + } + + /** + * @see org.apache.jmeter.testelement.TestStateListener#testEnded(java.lang.String) + */ + @Override + public void testEnded(String host) { + compiledScriptsCache.clear(); + this.scriptMd5 = null; + } + + public String getScriptLanguage() { + return scriptLanguage; + } + + public void setScriptLanguage(String s) { + scriptLanguage = s; + } +} diff --git a/src/main/java/org/apache/jmeter/util/SSLManager.java b/src/main/java/org/apache/jmeter/util/SSLManager.java new file mode 100644 index 0000000..93363ed --- /dev/null +++ b/src/main/java/org/apache/jmeter/util/SSLManager.java @@ -0,0 +1,378 @@ +/* + * 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.util; + +import org.apache.commons.lang3.Validate; +import org.apache.jmeter.gui.GuiPackage; +import org.apache.jmeter.util.keystore.JmeterKeyStore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.security.*; +import java.security.cert.CertificateException; +import java.util.Locale; + +/** + * The SSLManager handles the KeyStore information for JMeter. Basically, it + * handles all the logic for loading and initializing all the JSSE parameters + * and selecting the alias to authenticate against if it is available. + * SSLManager will try to automatically select the client certificate for you, + * but if it can't make a decision, it will pop open a dialog asking you for + * more information. + *

+ * TODO? - N.B. does not currently allow the selection of a client certificate. + */ +public abstract class SSLManager { + private static final Logger log = LoggerFactory.getLogger(SSLManager.class); + + private static final String SSL_TRUST_STORE = "javax.net.ssl.trustStore";// $NON-NLS-1$ + + private static final String KEY_STORE_PASSWORD = "javax.net.ssl.keyStorePassword"; // $NON-NLS-1$ NOSONAR no hard coded password + + public static final String JAVAX_NET_SSL_KEY_STORE = "javax.net.ssl.keyStore"; // $NON-NLS-1$ + + private static final String JAVAX_NET_SSL_KEY_STORE_TYPE = "javax.net.ssl.keyStoreType"; // $NON-NLS-1$ + + private static final String PKCS12 = "pkcs12"; // $NON-NLS-1$ + + /** + * Singleton instance of the manager + */ + private static SSLManager manager; + + private static final boolean IS_SSL_SUPPORTED = true; + + /** + * Cache the KeyStore instance + */ + private JmeterKeyStore keyStore; + + /** + * Cache the TrustStore instance - null if no truststore name was provided + */ + private KeyStore trustStore = null; + // Have we yet tried to load the truststore? + private volatile boolean truststoreLoaded = false; + + /** + * Have the password available + */ + protected volatile String defaultpw = System.getProperty(KEY_STORE_PASSWORD); + + private int keystoreAliasStartIndex; + + private int keystoreAliasEndIndex; + + private String clientCertAliasVarName; + + /** + * Resets the SSLManager so that we can create a new one with a new keystore + */ + public static synchronized void reset() { + SSLManager.manager = null; + } + + public abstract void setContext(HttpURLConnection conn); + + /** + * Default implementation of setting the Provider + * + * @param provider the provider to use + */ + protected void setProvider(Provider provider) { + if (null != provider) { + Security.addProvider(provider); + } + } + + protected synchronized JmeterKeyStore getKeyStore() { + if (null == this.keyStore) { + String fileName = System.getProperty(JAVAX_NET_SSL_KEY_STORE, ""); // empty if not provided + String fileType = System.getProperty(JAVAX_NET_SSL_KEY_STORE_TYPE, // use the system property to determine the type + fileName.toLowerCase(Locale.ENGLISH).endsWith(".p12") ? PKCS12 : "JKS"); // otherwise use the name + log.info("JmeterKeyStore Location: {} type {}", fileName, fileType); + try { + this.keyStore = JmeterKeyStore.getInstance(fileType, keystoreAliasStartIndex, keystoreAliasEndIndex, clientCertAliasVarName); + log.info("KeyStore created OK"); + } catch (Exception e) { + this.keyStore = null; + throw new IllegalArgumentException("Could not create keystore: " + e.getMessage(), e); + } + + try { + + // The string 'NONE' is used for the keystore location when using PKCS11 + // https://docs.oracle.com/javase/8/docs/technotes/guides/security/p11guide.html#JSSE + if ("NONE".equalsIgnoreCase(fileName)) { + retryLoadKeys(null, false); + log.info("Total of {} aliases loaded OK from PKCS11", keyStore.getAliasCount()); + } else { + File initStore = new File(fileName); + if (fileName.length() > 0 && initStore.exists()) { + retryLoadKeys(initStore, true); + if (log.isInfoEnabled()) { + log.info("Total of {} aliases loaded OK from keystore {}", + keyStore.getAliasCount(), fileName); + } + } else { + log.warn("Keystore file not found, loading empty keystore"); + this.defaultpw = ""; // Ensure not null + this.keyStore.load(null, ""); + } + } + } catch (Exception e) { + log.error("Problem loading keystore: {}", e.getMessage(), e); + } + + if (log.isDebugEnabled()) { + log.debug("JmeterKeyStore type: {}", this.keyStore.getClass()); + } + } + + return this.keyStore; + } + + /** + * Opens and initializes the KeyStore. If the password for the KeyStore is + * not set, this method will prompt you to enter it. Unfortunately, there is + * no PasswordEntryField available from JOptionPane. + * + * @return the configured {@link JmeterKeyStore} + */ + protected synchronized JmeterKeyStore getKeyStore(InputStream is, String password) { + if (null == this.keyStore) { + String fileName = System.getProperty(JAVAX_NET_SSL_KEY_STORE, ""); // empty if not provided + String fileType = System.getProperty(JAVAX_NET_SSL_KEY_STORE_TYPE, // use the system property to determine the type + fileName.toLowerCase(Locale.ENGLISH).endsWith(".p12") ? PKCS12 : "JKS"); // otherwise use the name + log.info("JmeterKeyStore Location: {} type {}", fileName, fileType); + try { + this.keyStore = JmeterKeyStore.getInstance(fileType, keystoreAliasStartIndex, keystoreAliasEndIndex, clientCertAliasVarName); + log.info("KeyStore created OK"); + } catch (Exception e) { + this.keyStore = null; + throw new IllegalArgumentException("Could not create keystore: " + e.getMessage(), e); + } + + try { + + // The string 'NONE' is used for the keystore location when using PKCS11 + // https://docs.oracle.com/javase/8/docs/technotes/guides/security/p11guide.html#JSSE + if ("NONE".equalsIgnoreCase(fileName)) { + retryLoadKeys(null, false); + log.info("Total of {} aliases loaded OK from PKCS11", keyStore.getAliasCount()); + } else { + File initStore = new File(fileName); + if (fileName.length() > 0 && initStore.exists()) { + retryLoadKeys(initStore, true); + if (log.isInfoEnabled()) { + log.info("Total of {} aliases loaded OK from keystore {}", + keyStore.getAliasCount(), fileName); + } + } else { + log.warn("Keystore file not found, loading empty keystore"); + this.defaultpw = ""; // Ensure not null + this.keyStore.load(is, password); + } + } + } catch (Exception e) { + log.error("Problem loading keystore: {}", e.getMessage(), e); + } + + if (log.isDebugEnabled()) { + log.debug("JmeterKeyStore type: {}", this.keyStore.getClass()); + } + } + + return this.keyStore; + } + + private void retryLoadKeys(File initStore, boolean allowEmptyPassword) throws NoSuchAlgorithmException, + CertificateException, IOException, KeyStoreException, UnrecoverableKeyException { + for (int i = 0; i < 3; i++) { + String password = getPassword(); + if (!allowEmptyPassword) { + Validate.notNull(password, "Password for keystore must not be null"); + } + try { + if (initStore == null) { + this.keyStore.load(null, password); + } else { + try (InputStream fis = new FileInputStream(initStore)) { + this.keyStore.load(fis, password); + } + } + return; + } catch (IOException e) { + log.debug("Could not load keystore. Wrong password for keystore?", e); + } + this.defaultpw = null; + } + } + + /* + * The password can be defined as a property; this dialogue is provided to allow it + * to be entered at run-time. + */ + private String getPassword() { + String password = this.defaultpw; + if (null == password) { + final GuiPackage guiInstance = GuiPackage.getInstance(); +// if (guiInstance != null) { +// JPanel panel = new JPanel(new MigLayout("fillx, wrap 2", "[][fill, grow]")); +// JLabel passwordLabel = new JLabel("Password: "); +// JPasswordField pwf = new JPasswordField(64); +// pwf.setEchoChar('*'); +// passwordLabel.setLabelFor(pwf); +// panel.add(passwordLabel); +// panel.add(pwf); +// int choice = JOptionPane.showConfirmDialog(guiInstance.getMainFrame(), panel, +// JMeterUtils.getResString("ssl_pass_prompt"), JOptionPane.OK_CANCEL_OPTION, +// JOptionPane.PLAIN_MESSAGE); +// if (choice == JOptionPane.OK_OPTION) { +// char[] pwchars = pwf.getPassword(); +// this.defaultpw = new String(pwchars); +// Arrays.fill(pwchars, '*'); +// } +// System.setProperty(KEY_STORE_PASSWORD, this.defaultpw); +// password = this.defaultpw; +// } + } else { + log.warn("No password provided, and no GUI present so cannot prompt"); + } + return password; + } + + /** + * Opens and initializes the TrustStore. + *

+ * There are 3 possibilities: + *

    + *
  • no truststore name provided, in which case the default Java truststore + * should be used
  • + *
  • truststore name is provided, and loads OK
  • + *
  • truststore name is provided, but is not found or does not load OK, in + * which case an empty + * truststore is created
  • + *
+ * If the KeyStore object cannot be created, then this is currently treated the + * same as if no truststore name was provided. + * + * @return {@code null} when Java truststore should be used. + * Otherwise the truststore, which may be empty if the file could not be + * loaded. + */ + protected KeyStore getTrustStore() { + if (!truststoreLoaded) { + + truststoreLoaded = true;// we've tried ... + + String fileName = System.getProperty(SSL_TRUST_STORE); + if (fileName == null) { + return null; + } + log.info("TrustStore Location: {}", fileName); + + try { + this.trustStore = KeyStore.getInstance("JKS"); + log.info("TrustStore created OK, Type: JKS"); + } catch (Exception e) { + this.trustStore = null; + throw new RuntimeException("Problem creating truststore: " + e.getMessage(), e); + } + + try { + File initStore = new File(fileName); + + if (initStore.exists()) { + try (InputStream fis = new FileInputStream(initStore)) { + this.trustStore.load(fis, null); + log.info("Truststore loaded OK from file"); + } + } else { + log.warn("Truststore file not found, loading empty truststore"); + this.trustStore.load(null, null); + } + } catch (Exception e) { + throw new RuntimeException("Can't load TrustStore: " + e.getMessage(), e); + } + } + + return this.trustStore; + } + + /** + * Protected Constructor to remove the possibility of directly instantiating + * this object. Create the SSLContext, and wrap all the X509KeyManagers with + * our X509KeyManager so that we can choose our alias. + */ + protected SSLManager() { + } + + /** + * Static accessor for the SSLManager object. The SSLManager is a singleton. + * + * @return the singleton {@link SSLManager} + */ + public static synchronized SSLManager getInstance() { + if (null == SSLManager.manager) { + SSLManager.manager = new JsseSSLManager(null); + } + + return SSLManager.manager; + } + + /** + * Test whether SSL is supported or not. + * + * @return flag whether SSL is supported + */ + public static boolean isSSLSupported() { + return SSLManager.IS_SSL_SUPPORTED; + } + + /** + * Configure Keystore + * + * @param preload flag whether the keystore should be opened within this method, + * or the opening should be delayed + * @param startIndex first index to consider for a key + * @param endIndex last index to consider for a key + * @param clientCertAliasVarName name of the default key, if empty the first key will be used + * as default key + */ + public synchronized void configureKeystore(boolean preload, int startIndex, int endIndex, String clientCertAliasVarName, InputStream is, String password) { + this.keystoreAliasStartIndex = startIndex; + this.keystoreAliasEndIndex = endIndex; + this.clientCertAliasVarName = clientCertAliasVarName; + if (preload) { + keyStore = getKeyStore(is, password); + } + } + + /** + * Destroy Keystore + */ + public synchronized void destroyKeystore() { + keyStore = null; + } +} diff --git a/src/main/java/org/apache/jorphan/collections/HashTree.java b/src/main/java/org/apache/jorphan/collections/HashTree.java new file mode 100644 index 0000000..828ab58 --- /dev/null +++ b/src/main/java/org/apache/jorphan/collections/HashTree.java @@ -0,0 +1,1095 @@ +/* + * 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.jorphan.collections; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.*; + +/** + * This class is used to create a tree structure of objects. Each element in the + * tree is also a key to the next node down in the tree. It provides many ways + * to add objects and branches, as well as many ways to retrieve. + *

+ * HashTree implements the Map interface for convenience reasons. The main + * difference between a Map and a HashTree is that the HashTree organizes the + * data into a recursive tree structure, and provides the means to manipulate + * that structure. + *

+ * Of special interest is the {@link #traverse(HashTreeTraverser)} method, which + * provides an expedient way to traverse any HashTree by implementing the + * {@link HashTreeTraverser} interface in order to perform some operation on the + * tree, or to extract information from the tree. + * + * @see HashTreeTraverser + * @see SearchByClass + */ +public class HashTree implements Serializable, Map, Cloneable { + + private static final long serialVersionUID = 240L; + + // Used for the RuntimeException to short-circuit the traversal + private static final String FOUND = "found"; // $NON-NLS-1$ + + // N.B. The keys can be either JMeterTreeNode or TestElement + protected final Map data; + + /** + * Creates an empty new HashTree. + */ + public HashTree() { + this(null, null); + } + + /** + * Allow subclasses to provide their own Map. + * @param _map {@link Map} to use + */ + protected HashTree(Map _map) { + this(_map, null); + } + + /** + * Creates a new HashTree and adds the given object as a top-level node. + * + * @param key + * name of the new top-level node + */ + public HashTree(Object key) { + this(new LinkedHashMap(), key); + } + + /** + * Uses the new HashTree if not null and adds the given object as a + * top-level node if not null + * + * @param _map + * the map to be used. If null a new {@link LinkedHashMap} + * will be created + * @param key + * the object to be used as the key for the root node (may be + * null, in which case no root node will be created) + */ + private HashTree(Map _map, Object key) { + if(_map != null) { + data = _map; + } else { + data = new LinkedHashMap<>(); + } + if(key != null) { + data.put(key, new HashTree()); + } + } + + /** + * The Map given must also be a HashTree, otherwise an + * UnsupportedOperationException is thrown. If it is a HashTree, this is + * like calling the add(HashTree) method. + * + * @see #add(HashTree) + * @see Map#putAll(Map) + */ + @Override + public void putAll(Map map) { + if (map instanceof HashTree) { + this.add((HashTree) map); + } else { + throw new UnsupportedOperationException("can only putAll other HashTree objects"); + } + } + + /** + * Exists to satisfy the Map interface. + * + * @see Map#entrySet() + */ + @Override + public Set> entrySet() { + return data.entrySet(); + } + + /** + * Implemented as required by the Map interface, but is not very useful + * here. All 'values' in a HashTree are HashTree's themselves. + * + * @param value + * Object to be tested as a value. + * @return True if the HashTree contains the value, false otherwise. + * @see Map#containsValue(Object) + */ + @Override + public boolean containsValue(Object value) { + return data.containsValue(value); + } + + /** + * This is the same as calling HashTree.add(key,value). + * + * @param key + * to use + * @param value + * to store against key + * @see Map#put(Object, Object) + */ + @Override + public HashTree put(Object key, HashTree value) { + HashTree previous = data.get(key); + add(key, value); + return previous; + } + + /** + * Clears the HashTree of all contents. + * + * @see Map#clear() + */ + @Override + public void clear() { + data.clear(); + } + + /** + * Returns a collection of all the sub-trees of the current tree. + * + * @see Map#values() + */ + @Override + public Collection values() { + return data.values(); + } + + /** + * Adds a key as a node at the current level and then adds the given + * HashTree to that new node. + * + * @param key + * key to create in this tree + * @param subTree + * sub tree to add to the node created for the first argument. + */ + public void add(Object key, HashTree subTree) { + add(key).add(subTree); + } + + /** + * Adds all the nodes and branches of the given tree to this tree. Is like + * merging two trees. Duplicates are ignored. + * + * @param newTree the tree to be added + */ + public void add(HashTree newTree) { + for (Object item : newTree.list()) { + add(item).add(newTree.getTree(item)); + } + } + + /** + * Creates a new HashTree and adds all the objects in the given collection + * as top-level nodes in the tree. + * + * @param keys + * a collection of objects to be added to the created HashTree. + */ + public HashTree(Collection keys) { + data = new LinkedHashMap<>(); + for (Object o : keys) { + data.put(o, new HashTree()); + } + } + + /** + * Creates a new HashTree and adds all the objects in the given array as + * top-level nodes in the tree. + * + * @param keys + * array with names for the new top-level nodes + */ + public HashTree(Object[] keys) { + data = new LinkedHashMap<>(); + for (Object key : keys) { + data.put(key, new HashTree()); + } + } + + /** + * If the HashTree contains the given object as a key at the top level, then + * a true result is returned, otherwise false. + * + * @param o + * Object to be tested as a key. + * @return True if the HashTree contains the key, false otherwise. + * @see Map#containsKey(Object) + */ + @Override + public boolean containsKey(Object o) { + return data.containsKey(o); + } + + /** + * If the HashTree is empty, true is returned, false otherwise. + * + * @return True if HashTree is empty, false otherwise. + */ + @Override + public boolean isEmpty() { + return data.isEmpty(); + } + + /** + * Sets a key and it's value in the HashTree. It actually sets up a key, and + * then creates a node for the key and sets the value to the new node, as a + * key. Any previous nodes that existed under the given key are lost. + * + * @param key + * key to be set up + * @param value + * value to be set up as a key in the secondary node + */ + public void set(Object key, Object value) { + data.put(key, createNewTree(value)); + } + + /** + * Sets a key into the current tree and assigns it a HashTree as its + * subtree. Any previous entries under the given key are removed. + * + * @param key + * key to be set up + * @param t + * HashTree that the key maps to + */ + public void set(Object key, HashTree t) { + data.put(key, t); + } + + /** + * Sets a key and its values in the HashTree. It sets up a key in the + * current node, and then creates a node for that key, and sets all the + * values in the array as keys in the new node. Any keys previously held + * under the given key are lost. + * + * @param key + * Key to be set up + * @param values + * Array of objects to be added as keys in the secondary node + */ + public void set(Object key, Object[] values) { + data.put(key, createNewTree(Arrays.asList(values))); + } + + /** + * Sets a key and its values in the HashTree. It sets up a key in the + * current node, and then creates a node for that key, and set all the + * values in the array as keys in the new node. Any keys previously held + * under the given key are removed. + * + * @param key + * key to be set up + * @param values + * Collection of objects to be added as keys in the secondary + * node + */ + public void set(Object key, Collection values) { + data.put(key, createNewTree(values)); + } + + /** + * Sets a series of keys into the HashTree. It sets up the first object in + * the key array as a key in the current node, recurses into the next + * HashTree node through that key and adds the second object in the array. + * Continues recursing in this manner until the end of the first array is + * reached, at which point all the values of the second array are set as + * keys to the bottom-most node. All previous keys of that bottom-most node + * are removed. + * + * @param treePath + * array of keys to put into HashTree + * @param values + * array of values to be added as keys to bottom-most node + */ + public void set(Object[] treePath, Object[] values) { + if (treePath != null && values != null) { + set(Arrays.asList(treePath), Arrays.asList(values)); + } + } + + /** + * Sets a series of keys into the HashTree. It sets up the first object in + * the key array as a key in the current node, recurses into the next + * HashTree node through that key and adds the second object in the array. + * Continues recursing in this manner until the end of the first array is + * reached, at which point all the values of the Collection of values are + * set as keys to the bottom-most node. Any keys previously held by the + * bottom-most node are lost. + * + * @param treePath + * array of keys to put into HashTree + * @param values + * Collection of values to be added as keys to bottom-most node + */ + public void set(Object[] treePath, Collection values) { + if (treePath != null) { + set(Arrays.asList(treePath), values); + } + } + + /** + * Sets a series of keys into the HashTree. It sets up the first object in + * the key list as a key in the current node, recurses into the next + * HashTree node through that key and adds the second object in the list. + * Continues recursing in this manner until the end of the first list is + * reached, at which point all the values of the array of values are set as + * keys to the bottom-most node. Any previously existing keys of that bottom + * node are removed. + * + * @param treePath + * collection of keys to put into HashTree + * @param values + * array of values to be added as keys to bottom-most node + */ + public void set(Collection treePath, Object[] values) { + HashTree tree = addTreePath(treePath); + tree.set(Arrays.asList(values)); + } + + /** + * Sets the nodes of the current tree to be the objects of the given + * collection. Any nodes previously in the tree are removed. + * + * @param values + * Collection of objects to set as nodes. + */ + public void set(Collection values) { + clear(); + this.add(values); + } + + /** + * Sets a series of keys into the HashTree. It sets up the first object in + * the key list as a key in the current node, recurses into the next + * HashTree node through that key and adds the second object in the list. + * Continues recursing in this manner until the end of the first list is + * reached, at which point all the values of the Collection of values are + * set as keys to the bottom-most node. Any previously existing keys of that + * bottom node are lost. + * + * @param treePath + * list of keys to put into HashTree + * @param values + * collection of values to be added as keys to bottom-most node + */ + public void set(Collection treePath, Collection values) { + HashTree tree = addTreePath(treePath); + tree.set(values); + } + + /** + * Adds an key into the HashTree at the current level. If a HashTree exists + * for the key already, no new tree will be added + * + * @param key + * key to be added to HashTree + * @return newly generated tree, if no tree was found for the given key; + * existing key otherwise + */ + public HashTree add(Object key) { + if (!data.containsKey(key)) { + HashTree newTree = createNewTree(); + data.put(key, newTree); + return newTree; + } + return getTree(key); + } + + /** + * Adds all the given objects as nodes at the current level. + * + * @param keys + * Array of Keys to be added to HashTree. + */ + public void add(Object[] keys) { + for (Object key : keys) { + add(key); + } + } + + /** + * Adds a bunch of keys into the HashTree at the current level. + * + * @param keys + * Collection of Keys to be added to HashTree. + */ + public void add(Collection keys) { + for (Object o : keys) { + add(o); + } + } + + /** + * Adds a key and it's value in the HashTree. The first argument becomes a + * node at the current level, and the second argument becomes a node of it. + * + * @param key + * key to be added + * @param value + * value to be added as a key in the secondary node + * @return HashTree for which value is the key + */ + public HashTree add(Object key, Object value) { + return add(key).add(value); + } + + /** + * Adds a key and it's values in the HashTree. The first argument becomes a + * node at the current level, and adds all the values in the array to the + * new node. + * + * @param key + * key to be added + * @param values + * array of objects to be added as keys in the secondary node + */ + public void add(Object key, Object[] values) { + add(key).add(values); + } + + /** + * Adds a key as a node at the current level and then adds all the objects + * in the second argument as nodes of the new node. + * + * @param key + * key to be added + * @param values + * Collection of objects to be added as keys in the secondary + * node + */ + public void add(Object key, Collection values) { + add(key).add(values); + } + + /** + * Adds a series of nodes into the HashTree using the given path. The first + * argument is an array that represents a path to a specific node in the + * tree. If the path doesn't already exist, it is created (the objects are + * added along the way). At the path, all the objects in the second argument + * are added as nodes. + * + * @param treePath + * an array of objects representing a path + * @param values + * array of values to be added as keys to bottom-most node + */ + public void add(Object[] treePath, Object[] values) { + if (treePath != null) { + add(Arrays.asList(treePath), Arrays.asList(values)); + } + } + + /** + * Adds a series of nodes into the HashTree using the given path. The first + * argument is an array that represents a path to a specific node in the + * tree. If the path doesn't already exist, it is created (the objects are + * added along the way). At the path, all the objects in the second argument + * are added as nodes. + * + * @param treePath + * an array of objects representing a path + * @param values + * collection of values to be added as keys to bottom-most node + */ + public void add(Object[] treePath, Collection values) { + if (treePath != null) { + add(Arrays.asList(treePath), values); + } + } + + public HashTree add(Object[] treePath, Object value) { + return add(Arrays.asList(treePath), value); + } + + /** + * Adds a series of nodes into the HashTree using the given path. The first + * argument is a List that represents a path to a specific node in the tree. + * If the path doesn't already exist, it is created (the objects are added + * along the way). At the path, all the objects in the second argument are + * added as nodes. + * + * @param treePath + * a list of objects representing a path + * @param values + * array of values to be added as keys to bottom-most node + */ + public void add(Collection treePath, Object[] values) { + HashTree tree = addTreePath(treePath); + tree.add(Arrays.asList(values)); + } + + /** + * Adds a series of nodes into the HashTree using the given path. The first + * argument is a List that represents a path to a specific node in the tree. + * If the path doesn't already exist, it is created (the objects are added + * along the way). At the path, the object in the second argument is added + * as a node. + * + * @param treePath + * a list of objects representing a path + * @param value + * Object to add as a node to bottom-most node + * @return HashTree for which value is the key + */ + public HashTree add(Collection treePath, Object value) { + HashTree tree = addTreePath(treePath); + return tree.add(value); + } + + /** + * Adds a series of nodes into the HashTree using the given path. The first + * argument is a SortedSet that represents a path to a specific node in the + * tree. If the path doesn't already exist, it is created (the objects are + * added along the way). At the path, all the objects in the second argument + * are added as nodes. + * + * @param treePath + * a SortedSet of objects representing a path + * @param values + * Collection of values to be added as keys to bottom-most node + */ + public void add(Collection treePath, Collection values) { + HashTree tree = addTreePath(treePath); + tree.add(values); + } + + protected HashTree addTreePath(Collection treePath) { + HashTree tree = this; + for (Object temp : treePath) { + tree = tree.add(temp); + } + return tree; + } + + /** + * Gets the HashTree mapped to the given key. + * + * @param key + * Key used to find appropriate HashTree() + * @return the HashTree for key + */ + public HashTree getTree(Object key) { + return data.get(key); + } + + /** + * Returns the HashTree object associated with the given key. Same as + * calling {@link #getTree(Object)}. + * + * @see Map#get(Object) + */ + @Override + public HashTree get(Object key) { + return getTree(key); + } + + /** + * Gets the HashTree object mapped to the last key in the array by recursing + * through the HashTree structure one key at a time. + * + * @param treePath + * array of keys. + * @return HashTree at the end of the recursion. + */ + public HashTree getTree(Object[] treePath) { + if (treePath != null) { + return getTree(Arrays.asList(treePath)); + } + return this; + } + + /** + * Create a clone of this HashTree. This is not a deep clone (i.e., the + * contents of the tree are not cloned). + * + */ + @Override + public Object clone() { + HashTree newTree = new HashTree(); + cloneTree(newTree); + return newTree; + } + + protected void cloneTree(HashTree newTree) { + for (Object key : list()) { + newTree.set(key, (HashTree) getTree(key).clone()); + } + } + + /** + * Creates a new tree. This method exists to allow inheriting classes to + * generate the appropriate types of nodes. For instance, when a node is + * added, it's value is a HashTree. Rather than directly calling the + * HashTree() constructor, the createNewTree() method is called. Inheriting + * classes should override these methods and create the appropriate subclass + * of HashTree. + * + * @return HashTree + */ + protected HashTree createNewTree() { + return new HashTree(); + } + + /** + * Creates a new tree. This method exists to allow inheriting classes to + * generate the appropriate types of nodes. For instance, when a node is + * added, it's value is a HashTree. Rather than directly calling the + * HashTree() constructor, the createNewTree() method is called. Inheriting + * classes should override these methods and create the appropriate subclass + * of HashTree. + * + * @param key + * object to use as the key for the top level + * + * @return newly created {@link HashTree} + */ + protected HashTree createNewTree(Object key) { + return new HashTree(key); + } + + /** + * Creates a new tree. This method exists to allow inheriting classes to + * generate the appropriate types of nodes. For instance, when a node is + * added, it's value is a HashTree. Rather than directly calling the + * HashTree() constructor, the createNewTree() method is called. Inheriting + * classes should override these methods and create the appropriate subclass + * of HashTree. + * + * @param values objects to be added to the new {@link HashTree} + * + * @return newly created {@link HashTree} + */ + protected HashTree createNewTree(Collection values) { + return new HashTree(values); + } + + /** + * Gets the HashTree object mapped to the last key in the SortedSet by + * recursing through the HashTree structure one key at a time. + * + * @param treePath + * Collection of keys + * @return HashTree at the end of the recursion + */ + public HashTree getTree(Collection treePath) { + return getTreePath(treePath); + } + + /** + * Gets a Collection of all keys in the current HashTree node. If the + * HashTree represented a file system, this would be like getting a + * collection of all the files in the current folder. + * + * @return Set of all keys in this HashTree + */ + public Collection list() { + return data.keySet(); + } + + /** + * Gets a Set of all keys in the HashTree mapped to the given key of the + * current HashTree object (in other words, one level down. If the HashTree + * represented a file system, this would like getting a list of all files in + * a sub-directory (of the current directory) specified by the key argument. + * + * @param key + * key used to find HashTree to get list of + * @return Set of all keys in found HashTree. + */ + public Collection list(Object key) { + HashTree temp = data.get(key); + if (temp != null) { + return temp.list(); + } + return new HashSet<>(); + } + + /** + * Removes the entire branch specified by the given key. + * + * @see Map#remove(Object) + */ + @Override + public HashTree remove(Object key) { + return data.remove(key); + } + + /** + * Recurses down into the HashTree structure using each subsequent key in the + * array of keys, and returns the Set of keys of the HashTree object at the + * end of the recursion. If the HashTree represented a file system, this + * would be like getting a list of all the files in a directory specified by + * the treePath, relative from the current directory. + * + * @param treePath + * Array of keys used to recurse into HashTree structure + * @return Set of all keys found in end HashTree + */ + public Collection list(Object[] treePath) { + if (treePath != null) { + return list(Arrays.asList(treePath)); + } + return list(); + } + + /** + * Recurses down into the HashTree structure using each subsequent key in the + * List of keys, and returns the Set of keys of the HashTree object at the + * end of the recursion. If the HashTree represented a file system, this + * would be like getting a list of all the files in a directory specified by + * the treePath, relative from the current directory. + * + * @param treePath + * List of keys used to recurse into HashTree structure + * @return Set of all keys found in end HashTree + */ + public Collection list(Collection treePath) { + HashTree tree = getTreePath(treePath); + if (tree != null) { + return tree.list(); + } + return new HashSet<>(); + } + + /** + * Finds the given current key, and replaces it with the given new key. Any + * tree structure found under the original key is moved to the new key. + * + * @param currentKey name of the key to be replaced + * @param newKey name of the new key + */ + public void replaceKey(Object currentKey, Object newKey) { + HashTree tree = getTree(currentKey); + data.remove(currentKey); + data.put(newKey, tree); + } + + /** + * Gets an array of all keys in the current HashTree node. If the HashTree + * represented a file system, this would be like getting an array of all the + * files in the current folder. + * + * @return array of all keys in this HashTree. + */ + public Object[] getArray() { + return data.keySet().toArray(); + } + + /** + * Gets an array of all keys in the HashTree mapped to the given key of the + * current HashTree object (in other words, one level down). If the HashTree + * represented a file system, this would like getting a list of all files in + * a sub-directory (of the current directory) specified by the key argument. + * + * @param key + * key used to find HashTree to get list of + * @return array of all keys in found HashTree + */ + public Object[] getArray(Object key) { + HashTree t = getTree(key); + if (t != null) { + return t.getArray(); + } + return null; + } + + /** + * Recurses down into the HashTree structure using each subsequent key in the + * array of keys, and returns an array of keys of the HashTree object at the + * end of the recursion. If the HashTree represented a file system, this + * would be like getting a list of all the files in a directory specified by + * the treePath, relative from the current directory. + * + * @param treePath + * array of keys used to recurse into HashTree structure + * @return array of all keys found in end HashTree + */ + public Object[] getArray(Object[] treePath) { + if (treePath != null) { + return getArray(Arrays.asList(treePath)); + } + return getArray(); + } + + /** + * Recurses down into the HashTree structure using each subsequent key in the + * treePath argument, and returns an array of keys of the HashTree object at + * the end of the recursion. If the HashTree represented a file system, this + * would be like getting a list of all the files in a directory specified by + * the treePath, relative from the current directory. + * + * @param treePath + * list of keys used to recurse into HashTree structure + * @return array of all keys found in end HashTree + */ + public Object[] getArray(Collection treePath) { + HashTree tree = getTreePath(treePath); + return (tree != null) ? tree.getArray() : null; + } + + protected HashTree getTreePath(Collection treePath) { + HashTree tree = this; + for (Object aTreePath : treePath) { + tree = tree.getTree(aTreePath); + if (tree == null) { + return null; + } + } + return tree; + } + + /** + * Returns a hashcode for this HashTree. + * + * @see Object#hashCode() + */ + @Override + public int hashCode() { + return data.hashCode() * 7; + } + + /** + * Compares all objects in the tree and verifies that the two trees contain + * the same objects at the same tree levels. Returns true if they do, false + * otherwise. + * + * @param o + * Object to be compared against + * @see Object#equals(Object) + */ + @Override + public boolean equals(Object o) { + if (!(o instanceof HashTree)) { + return false; + } + HashTree oo = (HashTree) o; + if (oo.size() != this.size()) { + return false; + } + return data.equals(oo.data); + } + + /** + * Returns a Set of all the keys in the top-level of this HashTree. + * + * @see Map#keySet() + */ + @Override + public Set keySet() { + return data.keySet(); + } + + /** + * Searches the HashTree structure for the given key. If it finds the key, + * it returns the HashTree mapped to the key. If it finds nothing, it + * returns null. + * + * @param key + * Key to search for + * @return HashTree mapped to key, if found, otherwise null + */ + public HashTree search(Object key) { + HashTree result = getTree(key); + if (result != null) { + return result; + } + TreeSearcher searcher = new TreeSearcher(key); + try { + traverse(searcher); + } catch (RuntimeException e) { + if (!e.getMessage().equals(FOUND)){ + throw e; + } + // do nothing - means object is found + } + return searcher.getResult(); + } + + /** + * Method readObject. + * + * @param ois + * the stream to read the objects from + * @throws ClassNotFoundException + * when the class for the deserialization can not be found + * @throws IOException + * when I/O error occurs + */ + private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { + ois.defaultReadObject(); + } + + private void writeObject(ObjectOutputStream oos) throws IOException { + oos.defaultWriteObject(); + } + + /** + * Returns the number of top-level entries in the HashTree. + * + * @see Map#size() + */ + @Override + public int size() { + return data.size(); + } + + /** + * Allows any implementation of the HashTreeTraverser interface to easily + * traverse (depth-first) all the nodes of the HashTree. The Traverser + * implementation will be given notification of each node visited. + * + * @see HashTreeTraverser + * @param visitor + * the visitor that wants to traverse the tree + */ + public void traverse(HashTreeTraverser visitor) { + for (Object item : list()) { + visitor.addNode(item, getTree(item)); + getTree(item).traverseInto(visitor); + } + } + + /** + * The recursive method that accomplishes the tree-traversal and performs + * the callbacks to the HashTreeTraverser. + * + * @param visitor + * the {@link HashTreeTraverser} to be notified + */ + private void traverseInto(HashTreeTraverser visitor) { + if (list().isEmpty()) { + visitor.processPath(); + } else { + for (Object item : list()) { + final HashTree treeItem = getTree(item); + visitor.addNode(item, treeItem); + treeItem.traverseInto(visitor); + } + } + visitor.subtractNode(); + } + + /** + * Generate a printable representation of the tree. + * + * @return a representation of the tree + */ + @Override + public String toString() { + ConvertToString converter = new ConvertToString(); + try { + traverse(converter); + } catch (Exception e) { // Just in case + converter.reportError(e); + } + return converter.toString(); + } + + private static class TreeSearcher implements HashTreeTraverser { + + private final Object target; + + private HashTree result; + + public TreeSearcher(Object t) { + target = t; + } + + public HashTree getResult() { + return result; + } + + /** {@inheritDoc} */ + @Override + public void addNode(Object node, HashTree subTree) { + result = subTree.getTree(target); + if (result != null) { + // short circuit traversal when found + throw new RuntimeException(FOUND); + } + } + + /** {@inheritDoc} */ + @Override + public void processPath() { + // Not used + } + + /** {@inheritDoc} */ + @Override + public void subtractNode() { + // Not used + } + } + + private static class ConvertToString implements HashTreeTraverser { + private final StringBuilder string = new StringBuilder(getClass().getName() + "{"); + + private final StringBuilder spaces = new StringBuilder(); + + private int depth = 0; + + @Override + public void addNode(Object key, HashTree subTree) { + depth++; + string.append("\n").append(getSpaces()).append(key); + string.append(" {"); + } + + @Override + public void subtractNode() { + string.append("\n" + getSpaces() + "}"); + depth--; + } + + @Override + public void processPath() { + // NOOP + } + + @Override + public String toString() { + string.append("\n}"); + return string.toString(); + } + + void reportError(Throwable t){ + string.append("Error: ").append(t.toString()); + } + + private String getSpaces() { + if (spaces.length() < depth * 2) { + while (spaces.length() < depth * 2) { + spaces.append(" "); + } + } else if (spaces.length() > depth * 2) { + spaces.setLength(depth * 2); + } + return spaces.toString(); + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 6c51bae..21c0c57 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,4 +1,7 @@ spring.application.name=node-controller -logging.file.path=/opt/fit2cloud/logs/${spring.application.name} +logging.file.path=/opt/metersphere/logs/${spring.application.name} server.port=8082 - +spring.kafka.consumer.group-id=metersphere_group_id +spring.kafka.listener.ack-mode=MANUAL_IMMEDIATE +# jmeter +jmeter.home=/opt/jmeter \ No newline at end of file diff --git a/src/main/resources/jmeter/bin/jmeter.properties b/src/main/resources/jmeter/bin/jmeter.properties new file mode 100644 index 0000000..38e815f --- /dev/null +++ b/src/main/resources/jmeter/bin/jmeter.properties @@ -0,0 +1,1346 @@ +################################################################################ +# Apache JMeter Property file +################################################################################ + +## 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. + +################################################################################ +# +# THIS FILE SHOULD NOT BE MODIFIED +# +# This avoids having to re-apply the modifications when upgrading JMeter +# Instead only user.properties should be modified: +# 1/ copy the property you want to modify to user.properties from jmeter.properties +# 2/ Change its value there +# +################################################################################ + +# JMeter properties are described in the file +# http://jmeter.apache.org/usermanual/properties_reference.html +# A local copy can be found in +# printable_docs/usermanual/properties_reference.html + +#Preferred GUI language. Comment out to use the JVM default locale's language. +#language=en + + +# Additional locale(s) to add to the displayed list. +# The current default list is: en, fr, de, no, es, tr, ja, zh_CN, zh_TW, pl, pt_BR +# [see JMeterMenuBar#makeLanguageMenu()] +# The entries are a comma-separated list of language names +#locales.add=zu + + +#--------------------------------------------------------------------------- +# XML Parser +#--------------------------------------------------------------------------- + +# Path to a Properties file containing Namespace mapping in the form +# prefix=Namespace +# Example: +# ns=http://biz.aol.com/schema/2006-12-18 +#xpath.namespace.config= + + +# XPath2 query cache for storing compiled XPath queries +#xpath2query.parser.cache.size=400 + +#--------------------------------------------------------------------------- +# SSL configuration +#--------------------------------------------------------------------------- + +## SSL System properties are now in system.properties + +# JMeter no longer converts javax.xxx property entries in this file into System properties. +# These must now be defined in the system.properties file or on the command-line. +# The system.properties file gives more flexibility. + +# By default, SSL session contexts are now created per-thread, rather than being shared. +# The original behaviour can be enabled by setting the JMeter property to true +#https.sessioncontext.shared=false + +# Be aware that https default protocol may vary depending on the version of JVM +# See https://blogs.oracle.com/java-platform-group/entry/diagnosing_tls_ssl_and_https +# See https://bz.apache.org/bugzilla/show_bug.cgi?id=58236 +# Default HTTPS protocol level: +#https.default.protocol=TLS +# This may need to be changed here (or in user.properties) to: +#https.default.protocol=SSLv3 + +# List of protocols to enable. You may have to select only a subset if you find issues with target server. +# This is needed when server does not support Socket version negotiation, this can lead to: +# javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated +# java.net.SocketException: Connection reset +# see https://bz.apache.org/bugzilla/show_bug.cgi?id=54759 +#https.socket.protocols=SSLv2Hello SSLv3 TLSv1 + +# Control if we allow reuse of cached SSL context between iterations +# set the value to 'false' to reset the SSL context each iteration +# Deprecated since 5.0 +#https.use.cached.ssl.context=true + +# +# Reset HTTP State when starting a new Thread Group iteration which means: +# true means next iteration is associated to a new user +# false means next iteration is associated to same user +# true involves: +# - Closing opened connection +# - resetting SSL State +#httpclient.reset_state_on_thread_group_iteration=true + +# Start and end index to be used with keystores with many entries +# The default is to use entry 0, i.e. the first +#https.keyStoreStartIndex=0 +#https.keyStoreEndIndex=0 + +#--------------------------------------------------------------------------- +# Look and Feel configuration +#--------------------------------------------------------------------------- + +#Classname of the Swing default UI +# +# The LAF classnames that are available are now displayed as ToolTip text +# when hovering over the Options/Look and Feel selection list. +# +# You can either use a full class name, as shown below, +# or one of the strings "System" or "CrossPlatform" which means +# JMeter will use the corresponding string returned by UIManager.getLookAndFeelClassName() + +# LAF can be overridden by os.name (lowercased, spaces replaced by '_') +# Sample os.name LAF: +#jmeter.laf.windows_xp=javax.swing.plaf.metal.MetalLookAndFeel + +# Failing that, the OS family = os.name, but only up to first space: +# Sample OS family LAF: +#jmeter.laf.windows=com.sun.java.swing.plaf.windows.WindowsLookAndFeel + +# Custom settings for Mac using System LAF if you don't want to use Darcula +#jmeter.laf.mac=System + +# Failing that, the JMeter default laf can be defined: +#jmeter.laf=System + +# If none of the above jmeter.laf properties are defined, JMeter uses the CrossPlatform LAF. +# This is because the CrossPlatform LAF generally looks better than the System LAF. +# See https://bz.apache.org/bugzilla/show_bug.cgi?id=52026 for details +# N.B. the laf can be defined in user.properties. + +# LoggerPanel display +# default to false +#jmeter.loggerpanel.display=false + +# Enable LogViewer Panel to receive log event even if closed +# Enabled since 2.12 +# Note this has some impact on performances, but as GUI mode must +# not be used for Load Test it is acceptable +#jmeter.loggerpanel.enable_when_closed=true + +# Max lines kept in LoggerPanel, default to 1000 chars +# 0 means no limit +#jmeter.loggerpanel.maxlength=1000 + +# Interval period in ms to process the queue of events of the listeners +#jmeter.gui.refresh_period=500 + +# HiDPI mode (default: false) +# Activate a 'pseudo'-hidpi mode. Allows to increase size of some UI elements +# which are not correctly managed by JVM with high resolution screens in Linux or Windows +#jmeter.hidpi.mode=false +# To enable pseudo-hidpi mode change to true +#jmeter.hidpi.mode=true +# HiDPI scale factor +#jmeter.hidpi.scale.factor=1.0 +# Suggested value for HiDPI +#jmeter.hidpi.scale.factor=2.0 + +# Toolbar display +# Toolbar icon definitions +#jmeter.toolbar.icons=org/apache/jmeter/images/toolbar/icons-toolbar.properties +# Toolbar list +#jmeter.toolbar=new,open,close,save,save_as_testplan,|,cut,copy,paste,|,expand,collapse,toggle,|,test_start,test_stop,test_shutdown,|,test_start_remote_all,test_stop_remote_all,test_shutdown_remote_all,|,test_clear,test_clear_all,|,search,search_reset,|,function_helper,help +# Toolbar icons default size: 22x22. Available sizes are: 22x22, 32x32, 48x48 +#jmeter.toolbar.icons.size=22x22 +# Suggested value for HiDPI +#jmeter.toolbar.icons.size=48x48 + +# Icon definitions +# default: +#jmeter.icons=org/apache/jmeter/images/icon.properties +# alternate: +#jmeter.icons=org/apache/jmeter/images/icon_1.properties +# Historical icon set (deprecated) +#jmeter.icons=org/apache/jmeter/images/icon_old.properties + +# Tree icons default size: 19x19. Available sizes are: 19x19, 24x24, 32x32, 48x48 +# Useful for HiDPI display (see below) +#jmeter.tree.icons.size=19x19 +# Suggested value for HiDPI screen like 3200x1800: +#jmeter.tree.icons.size=32x32 + +#Components to not display in JMeter GUI (GUI class name or static label) +# These elements are deprecated and will be removed in next version: +# MongoDB Script, MongoDB Source Config, Monitor Results +# BSF Elements +not_in_menu=org.apache.jmeter.protocol.mongodb.sampler.MongoScriptSampler,org.apache.jmeter.protocol.mongodb.config.MongoSourceElement,\ + org.apache.jmeter.timers.BSFTimer,org.apache.jmeter.modifiers.BSFPreProcessor,org.apache.jmeter.extractor.BSFPostProcessor,org.apache.jmeter.assertions.BSFAssertion,\ + org.apache.jmeter.visualizers.BSFListener,org.apache.jmeter.protocol.java.sampler.BSFSampler,\ + org.apache.jmeter.protocol.http.control.gui.SoapSamplerGui + +# Number of items in undo history +# Feature is disabled by default (0) due to known and not fixed bugs: +# https://bz.apache.org/bugzilla/show_bug.cgi?id=57043 +# https://bz.apache.org/bugzilla/show_bug.cgi?id=57039 +# https://bz.apache.org/bugzilla/show_bug.cgi?id=57040 +# Set it to a number > 0 (25 can be a good default) +# The bigger it is, the more it consumes memory +#undo.history.size=0 + +# Hotkeys to add JMeter components, will add elements when you press Ctrl+0 .. Ctrl+9 (Command+0 .. Command+9 on Mac) +gui.quick_0=ThreadGroupGui +gui.quick_1=HttpTestSampleGui +gui.quick_2=RegexExtractorGui +gui.quick_3=AssertionGui +gui.quick_4=ConstantTimerGui +gui.quick_5=TestActionGui +gui.quick_6=JSR223PostProcessor +gui.quick_7=JSR223PreProcessor +gui.quick_8=DebugSampler +gui.quick_9=ViewResultsFullVisualizer + + +#--------------------------------------------------------------------------- +# JMX Backup configuration +#--------------------------------------------------------------------------- +#Enable auto backups of the .jmx file when a test plan is saved. +#When enabled, before the .jmx is saved, it will be backed up to the directory pointed +#by the jmeter.gui.action.save.backup_directory property (see below). Backup file names are built +#after the jmx file being saved. For example, saving test-plan.jmx will create a test-plan-000012.jmx +#in the backup directory provided that the last created backup file is test-plan-000011.jmx. +#Default value is true indicating that auto backups are enabled +#jmeter.gui.action.save.backup_on_save=true + +#Set the backup directory path where JMX backups will be created upon save in the GUI. +#If not set (what it defaults to) then backup files will be created in +#a sub-directory of the JMeter base installation. The default directory is ${JMETER_HOME}/backups +#If set and the directory does not exist, it will be created. +#jmeter.gui.action.save.backup_directory= + +#Set the maximum time (in hours) that backup files should be preserved since the save time. +#By default no expiration time is set which means we keep backups for ever. +#jmeter.gui.action.save.keep_backup_max_hours=0 + +#Set the maximum number of backup files that should be preserved. By default 10 backups will be preserved. +#Setting this to zero will cause the backups to not being deleted (unless keep_backup_max_hours is set to a non zero value) +#jmeter.gui.action.save.keep_backup_max_count=10 + +#Enable auto saving of the .jmx file before start run a test plan +#When enabled, before the run, the .jmx will be saved and also backed up to the directory pointed +#save_automatically_before_run=true + +#--------------------------------------------------------------------------- +# Remote hosts and RMI configuration +#--------------------------------------------------------------------------- + +# Remote Hosts - comma delimited +remote_hosts=127.0.0.1 +#remote_hosts=localhost:1099,localhost:2010 + +# RMI port to be used by the server (must start rmiregistry with same port) +#server_port=1099 + +# To change the port to (say) 1234: +# On the server(s) +# - set server_port=1234 +# - start rmiregistry with port 1234 +# On Windows this can be done by: +# SET SERVER_PORT=1234 +# JMETER-SERVER +# +# On Unix: +# SERVER_PORT=1234 jmeter-server +# +# On the client: +# - set remote_hosts=server:1234 + +# Parameter that controls the RMI port used by RemoteSampleListenerImpl (The Controller) +# Default value is 0 which means port is randomly assigned +# You may need to open Firewall port on the Controller machine +#client.rmi.localport=0 + +# When distributed test is starting, there may be several attempts to initialize +# remote engines. By default, only single try is made. Increase following property +# to make it retry for additional times +#client.tries=1 + +# If there is initialization retries, following property sets delay between attempts +#client.retries_delay=5000 + +# When all initialization tries was made, test will fail if some remote engines are failed +# Set following property to true to ignore failed nodes and proceed with test +#client.continue_on_fail=false + +# To change the default port (1099) used to access the server: +#server.rmi.port=1234 + +# To use a specific port for the JMeter server engine, define +# the following property before starting the server: +#server.rmi.localport=4000 + +# The jmeter server creates by default the RMI registry as part of the server process. +# To stop the server creating the RMI registry: +#server.rmi.create=false + +# Define the following property to cause JMeter to exit after the first test +#server.exitaftertest=true + +# +# Configuration of Secure RMI connection +# +# Type of keystore : JKS +#server.rmi.ssl.keystore.type=JKS +# +# Keystore file that contains private key +#server.rmi.ssl.keystore.file=rmi_keystore.jks +# +# Password of Keystore +#server.rmi.ssl.keystore.password=changeit +# +# Key alias +#server.rmi.ssl.keystore.alias=rmi +# +# Type of truststore : JKS +#server.rmi.ssl.truststore.type=JKS +# +# Keystore file that contains certificate +#server.rmi.ssl.truststore.file=rmi_keystore.jks +# +# Password of Trust store +#server.rmi.ssl.truststore.password=changeit +# +# Set this if you don't want to use SSL for RMI +#server.rmi.ssl.disable=false +#--------------------------------------------------------------------------- +# Include Controller +#--------------------------------------------------------------------------- + +# Prefix used by IncludeController when building file name +#includecontroller.prefix= + +#--------------------------------------------------------------------------- +# Shared HTTP configuration between HC4 and Java Implementations +#--------------------------------------------------------------------------- + +# +# Should JMeter add to POST request content-type header if missing: +# Content-Type: application/x-www-form-urlencoded +# Was true before version 5.0 +#post_add_content_type_if_missing=false + +#--------------------------------------------------------------------------- +# HTTP Java configuration +#--------------------------------------------------------------------------- + +# Number of connection retries performed by HTTP Java sampler before giving up +# 0 means no retry since version 3.0 +#http.java.sampler.retries=0 + +#--------------------------------------------------------------------------- +# Following properties apply to Apache HttpClient +#--------------------------------------------------------------------------- + +# set the socket timeout (or use the parameter http.socket.timeout) +# for AJP Sampler implementation. +# Value is in milliseconds +#httpclient.timeout=0 +# 0 == no timeout + +# Set the http version (defaults to 1.1) +#httpclient.version=1.1 (or use the parameter http.protocol.version) + +# Define characters per second > 0 to emulate slow connections +#httpclient.socket.http.cps=0 +#httpclient.socket.https.cps=0 + +#Enable loopback protocol +#httpclient.loopback=true + +# Define the local host address to be used for multi-homed hosts +#httpclient.localaddress=1.2.3.4 + +#--------------------------------------------------------------------------- +# AuthManager Kerberos configuration +#--------------------------------------------------------------------------- + +# AuthManager Kerberos configuration +# Name of application module used in jaas.conf +#kerberos_jaas_application=JMeter + +# Should ports be stripped from urls before constructing SPNs +# for SPNEGO authentication +#kerberos.spnego.strip_port=true + +# Should credentials be delegated to webservers when using +# SPNEGO authentication +#kerberos.spnego.delegate_cred=false + +#--------------------------------------------------------------------------- +# Apache HttpComponents HTTPClient configuration (HTTPClient4) +#--------------------------------------------------------------------------- + +# define a properties file for overriding Apache HttpClient parameters +# Uncomment this line if you put anything in hc.parameters file +#hc.parameters.file=hc.parameters + +# If true, default HC4 User-Agent will not be added +#httpclient4.default_user_agent_disabled=false + +# Preemptively send Authorization Header when BASIC auth is used +#httpclient4.auth.preemptive=true + +# Number of retries to attempt (default 0) +#httpclient4.retrycount=0 + +# true if it's OK to retry requests that have been sent +# This will retry Idempotent and non Idempotent requests +# This should usually be false, but it can be useful +# when testing against some Load Balancers like Amazon ELB +#httpclient4.request_sent_retry_enabled=false + +# Idle connection timeout (Milliseconds) to apply if the server does not send +# Keep-Alive headers (default 0) +# Set this > 0 to compensate for servers that don't send a Keep-Alive header +# If <= 0, idle timeout will only apply if the server sends a Keep-Alive header +#httpclient4.idletimeout=0 + +# Check connections if the elapsed time (Milliseconds) since the last +# use of the connection exceed this value +#httpclient4.validate_after_inactivity=1700 + +# TTL (in Milliseconds) represents an absolute value. +# No matter what, the connection will not be re-used beyond its TTL. +#httpclient4.time_to_live=2000 + +# Max size in bytes of PUT body to retain in result sampler. +# Bigger results will be clipped. +#httpclient4.max_body_retain_size=32768 + +# Ignore EOFException that some edgy application may emit to signal end of GZIP stream +# Defaults to false +#httpclient4.gzip_relax_mode=false + +# Ignore EOFException that some edgy application may emit to signal end of Deflated stream +# Defaults to false +#httpclient4.deflate_relax_mode=false + +#--------------------------------------------------------------------------- +# HTTP Cache Manager configuration +#--------------------------------------------------------------------------- +# +# Space or comma separated list of methods that can be cached +#cacheable_methods=GET +# N.B. This property is currently a temporary solution for Bug 56162 + +# Since 2.12, JMeter does not create anymore a Sample Result with 204 response +# code for a resource found in cache which is inline with what browser do. +#cache_manager.cached_resource_mode=RETURN_NO_SAMPLE + +# You can choose between 3 modes: +# RETURN_NO_SAMPLE (default) +# RETURN_200_CACHE +# RETURN_CUSTOM_STATUS + +# Those mode have the following behaviours: +# RETURN_NO_SAMPLE: +# this mode returns no Sample Result, it has no additional configuration + +# RETURN_200_CACHE: +# this mode will return Sample Result with response code to 200 and +# response message to "(ex cache)", you can modify response message by setting +# RETURN_200_CACHE.message=(ex cache) + +# RETURN_CUSTOM_STATUS: +# This mode lets you select what response code and message you want to return, +# if you use this mode you need to set those properties +# RETURN_CUSTOM_STATUS.code= +# RETURN_CUSTOM_STATUS.message= + +#--------------------------------------------------------------------------- +# Results file configuration +#--------------------------------------------------------------------------- + +# This section helps determine how result data will be saved. +# The commented out values are the defaults. + +# legitimate values: xml, csv, db. Only xml and csv are currently supported. +#jmeter.save.saveservice.output_format=csv + +# The below properties are true when field should be saved; false otherwise +# +# assertion_results_failure_message only affects CSV output +#jmeter.save.saveservice.assertion_results_failure_message=true +# +# legitimate values: none, first, all +#jmeter.save.saveservice.assertion_results=none +# +#jmeter.save.saveservice.data_type=true +#jmeter.save.saveservice.label=true +#jmeter.save.saveservice.response_code=true +# response_data is not currently supported for CSV output +#jmeter.save.saveservice.response_data=false +# Save ResponseData for failed samples +#jmeter.save.saveservice.response_data.on_error=false +#jmeter.save.saveservice.response_message=true +#jmeter.save.saveservice.successful=true +#jmeter.save.saveservice.thread_name=true +#jmeter.save.saveservice.time=true +#jmeter.save.saveservice.subresults=true +#jmeter.save.saveservice.assertions=true +#jmeter.save.saveservice.latency=true +# Only available with HttpClient4 +#jmeter.save.saveservice.connect_time=true +#jmeter.save.saveservice.samplerData=false +#jmeter.save.saveservice.responseHeaders=false +#jmeter.save.saveservice.requestHeaders=false +#jmeter.save.saveservice.encoding=false +#jmeter.save.saveservice.bytes=true +# Only available with HttpClient4 +#jmeter.save.saveservice.sent_bytes=true +#jmeter.save.saveservice.url=true +#jmeter.save.saveservice.filename=false +#jmeter.save.saveservice.hostname=false +#jmeter.save.saveservice.thread_counts=true +#jmeter.save.saveservice.sample_count=false +#jmeter.save.saveservice.idle_time=true + +# Timestamp format - this only affects CSV output files +# legitimate values: none, ms, or a format suitable for SimpleDateFormat +#jmeter.save.saveservice.timestamp_format=ms +#jmeter.save.saveservice.timestamp_format=yyyy/MM/dd HH:mm:ss.SSS + +# For use with Comma-separated value (CSV) files or other formats +# where the fields' values are separated by specified delimiters. +# Default: +#jmeter.save.saveservice.default_delimiter=, +# For TAB, one can use: +#jmeter.save.saveservice.default_delimiter=\t + +# Only applies to CSV format files: +# Print field names as first line in CSV +#jmeter.save.saveservice.print_field_names=true + +# Optional list of JMeter variable names whose values are to be saved in the result data files. +# Use commas to separate the names. For example: +#sample_variables=SESSION_ID,REFERENCE +# N.B. The current implementation saves the values in XML as attributes, +# so the names must be valid XML names. +# By default JMeter sends the variable to all servers +# to ensure that the correct data is available at the client. + +# Optional xml processing instruction for line 2 of the file: +# Example: +#jmeter.save.saveservice.xml_pi= +# Default value: +#jmeter.save.saveservice.xml_pi= + +# Prefix used to identify filenames that are relative to the current base +#jmeter.save.saveservice.base_prefix=~/ + +# AutoFlush on each line written in XML or CSV output +# Setting this to true will result in less test results data loss in case of Crash +# but with impact on performances, particularly for intensive tests (low or no pauses) +# Since JMeter 2.10, this is false by default +#jmeter.save.saveservice.autoflush=false + +#--------------------------------------------------------------------------- +# Settings that affect SampleResults +#--------------------------------------------------------------------------- + +# Save the start time stamp instead of the end +# This also affects the timestamp stored in result files +sampleresult.timestamp.start=true + +# Whether to use System.nanoTime() - otherwise only use System.currentTimeMillis() +#sampleresult.useNanoTime=true + +# Use a background thread to calculate the nanoTime offset +# Set this to <= 0 to disable the background thread +#sampleresult.nanoThreadSleep=5000 + +# Since version 5.0 JMeter has a new SubResult Naming Policy which numbers subresults by default +# This property if set to true discards renaming policy. This can be required if you're using JMeter for functional testing. +# Defaults to: false +#subresults.disable_renaming=false + +#--------------------------------------------------------------------------- +# Upgrade property +#--------------------------------------------------------------------------- + +# File that holds a record of name changes for backward compatibility issues +upgrade_properties=/bin/upgrade.properties + +#--------------------------------------------------------------------------- +# JMeter Test Script recorder configuration +# +# N.B. The element was originally called the Proxy recorder, which is why the +# properties have the prefix "proxy". +#--------------------------------------------------------------------------- + +# If the recorder detects a gap of at least 5s (default) between HTTP requests, +# it assumes that the user has clicked a new URL +#proxy.pause=5000 + +# Add numeric suffix to Sampler names (default true) +#proxy.number.requests=true + +# List of URL patterns that will be added to URL Patterns to exclude +# Separate multiple lines with ; +#proxy.excludes.suggested=.*\\.(bmp|css|js|gif|ico|jpe?g|png|swf|woff|woff2) + +# Change the default HTTP Sampler (currently HttpClient4) +# Java: +#jmeter.httpsampler=HTTPSampler +#or +#jmeter.httpsampler=Java +# +# HttpClient4.x +#jmeter.httpsampler=HttpClient4 + +# By default JMeter tries to be more lenient with RFC2616 redirects and allows +# relative paths. +# If you want to test strict conformance, set this value to true +# When the property is true, JMeter follows http://tools.ietf.org/html/rfc3986#section-5.2 +#jmeter.httpclient.strict_rfc2616=false + +# Default content-type include filter to use +#proxy.content_type_include=text/html|text/plain|text/xml +# Default content-type exclude filter to use +#proxy.content_type_exclude=image/.*|text/css|application/.* + +# Default headers to remove from Header Manager elements +# (Cookie and Authorization are always removed) +#proxy.headers.remove=If-Modified-Since,If-None-Match,Host + +# Binary content-type handling +# These content-types will be handled by saving the request in a file: +#proxy.binary.types=application/x-amf,application/x-java-serialized-object,binary/octet-stream +# The files will be saved in this directory: +#proxy.binary.directory=user.dir +# The files will be created with this file filesuffix: +#proxy.binary.filesuffix=.binary + +#--------------------------------------------------------------------------- +# Test Script Recorder certificate configuration +#--------------------------------------------------------------------------- + +#proxy.cert.directory= +#proxy.cert.file=proxyserver.jks +#proxy.cert.type=JKS +#proxy.cert.keystorepass=password +#proxy.cert.keypassword=password +#proxy.cert.factory=SunX509 +# define this property if you wish to use your own keystore +#proxy.cert.alias= +# The default validity for certificates created by JMeter +#proxy.cert.validity=7 +# Use dynamic key generation (if supported by JMeter/JVM) +# If false, will revert to using a single key with no certificate +#proxy.cert.dynamic_keys=true + +#--------------------------------------------------------------------------- +# Test Script Recorder miscellaneous configuration +#--------------------------------------------------------------------------- + +# Whether to attempt disabling of samples that resulted from redirects +# where the generated samples use auto-redirection +#proxy.redirect.disabling=true + +# SSL configuration +#proxy.ssl.protocol=TLS + +#--------------------------------------------------------------------------- +# JMeter Proxy configuration +#--------------------------------------------------------------------------- +# use command-line flags for user-name and password +#http.proxyDomain=NTLM domain, if required by HTTPClient sampler + +#--------------------------------------------------------------------------- +# HTTPSampleResponse Parser configuration +#--------------------------------------------------------------------------- + +# Space-separated list of parser groups +HTTPResponse.parsers=htmlParser wmlParser cssParser +# for each parser, there should be a parser.types and a parser.className property + +# CSS Parser based on ph-css +cssParser.className=org.apache.jmeter.protocol.http.parser.CssParser +cssParser.types=text/css + +# CSS parser LRU cache size +# This cache stores the URLs found in a CSS to avoid continuously parsing the CSS +# By default the cache size is 400 +# It can be disabled by setting its value to 0 +#css.parser.cache.size=400 + +# Let the CSS Parser ignore all css errors +#css.parser.ignore_all_css_errors=true + +#--------------------------------------------------------------------------- +# HTML Parser configuration +#--------------------------------------------------------------------------- + +# Define the HTML parser to be used. +# Default parser: +# This new parser (since 2.10) should perform better than all others +# see https://bz.apache.org/bugzilla/show_bug.cgi?id=55632 +# Do not comment this property +htmlParser.className=org.apache.jmeter.protocol.http.parser.LagartoBasedHtmlParser + +# Other parsers: +# Default parser before 2.10 +#htmlParser.className=org.apache.jmeter.protocol.http.parser.JTidyHTMLParser +# Note that Regexp extractor may detect references that have been commented out. +# In many cases it will work OK, but you should be aware that it may generate +# additional references. +#htmlParser.className=org.apache.jmeter.protocol.http.parser.RegexpHTMLParser +# This parser is based on JSoup, it should be the most accurate but less +# performant than LagartoBasedHtmlParser +#htmlParser.className=org.apache.jmeter.protocol.http.parser.JsoupBasedHtmlParser + +#Used by HTTPSamplerBase to associate htmlParser with content types below +htmlParser.types=text/html application/xhtml+xml application/xml text/xml + +#--------------------------------------------------------------------------- +# WML Parser configuration +#--------------------------------------------------------------------------- + +wmlParser.className=org.apache.jmeter.protocol.http.parser.RegexpHTMLParser + +#Used by HTTPSamplerBase to associate wmlParser with content types below +wmlParser.types=text/vnd.wap.wml + +#--------------------------------------------------------------------------- +# Remote batching configuration +#--------------------------------------------------------------------------- +# How is Sample sender implementations configured: +# - true (default) means client configuration will be used +# - false means server configuration will be used +#sample_sender_client_configured=true + +# By default when Stripping modes are used JMeter since 3.1 will strip +# response even for SampleResults in error. +# If you want to revert to previous behaviour (no stripping of Responses in error) +# set this property to false +#sample_sender_strip_also_on_error=true + +# Remote batching support +# Since JMeter 2.9, default is MODE_STRIPPED_BATCH, which returns samples in +# batch mode (every 100 samples or every minute by default) +# Note also that MODE_STRIPPED_BATCH strips response data from SampleResult, so if you need it change to +# another mode +# Batch returns samples in batches +# Statistical returns sample summary statistics +# mode can also be the class name of an implementation of org.apache.jmeter.samplers.SampleSender +#mode=Standard +#mode=Batch +#mode=Statistical +#Set to true to key statistical samples on threadName rather than threadGroup +#key_on_threadname=false +#mode=Stripped +#mode=StrippedBatch +#mode=org.example.load.MySampleSender +# +#num_sample_threshold=100 +# Value is in milliseconds +#time_threshold=60000 +# +# Asynchronous sender; uses a queue and background worker process to return the samples +#mode=Asynch +# default queue size +#asynch.batch.queue.size=100 +# Same as Asynch but strips response data from SampleResult +#mode=StrippedAsynch +# +# DiskStore: Serialises the samples to disk, rather than saving in memory +#mode=DiskStore +# Same as DiskStore but strips response data from SampleResult +#mode=StrippedDiskStore +# Note: the mode is currently resolved on the client; +# other properties (e.g. time_threshold) are resolved on the server. + +#--------------------------------------------------------------------------- +# JDBC Request configuration +#--------------------------------------------------------------------------- + +# String used to indicate a null value +#jdbcsampler.nullmarker=]NULL[ +# +# Max size of BLOBs and CLOBs to store in JDBC sampler. Result will be cut off +#jdbcsampler.max_retain_result_size=65536 + +# Database validation query +# based in https://stackoverflow.com/questions/10684244/dbcp-validationquery-for-different-databases list +jdbc.config.check.query=select 1 from INFORMATION_SCHEMA.SYSTEM_USERS|select 1 from dual|select 1 from sysibm.sysdummy1|select 1|select 1 from rdb$database +jdbc.config.jdbc.driver.class=com.mysql.jdbc.Driver|org.postgresql.Driver|oracle.jdbc.OracleDriver|com.ingres.jdbc.IngresDriver|com.microsoft.sqlserver.jdbc.SQLServerDriver|com.microsoft.jdbc.sqlserver.SQLServerDriver|org.apache.derby.jdbc.ClientDriver|org.hsqldb.jdbc.JDBCDriver|com.ibm.db2.jcc.DB2Driver|org.apache.derby.jdbc.ClientDriver|org.h2.Driver|org.firebirdsql.jdbc.FBDriver|org.mariadb.jdbc.Driver|org.sqlite.JDBC|net.sourceforge.jtds.jdbc.Driver|com.exasol.jdbc.EXADriver + +#--------------------------------------------------------------------------- +# OS Process Sampler configuration +#--------------------------------------------------------------------------- +# Polling to see if process has finished its work, used when a timeout is configured on sampler +#os_sampler.poll_for_timeout=100 + +#--------------------------------------------------------------------------- +# TCP Sampler configuration +#--------------------------------------------------------------------------- + +# The default handler class +#tcp.handler=TCPClientImpl +# +# eolByte = byte value for end of line +# set this to a value outside the range -128 to +127 to skip eol checking +#tcp.eolByte=1000 +# +# TCP Charset, used by org.apache.jmeter.protocol.tcp.sampler.TCPClientImpl +# default to Platform defaults charset as returned by Charset.defaultCharset().name() +#tcp.charset= +# +# status.prefix and suffix = strings that enclose the status response code +#tcp.status.prefix=Status= +#tcp.status.suffix=. +# +# status.properties = property file to convert codes to messages +#tcp.status.properties=mytestfiles/tcpstatus.properties + +# The length prefix used by LengthPrefixedBinaryTCPClientImpl implementation +# defaults to 2 bytes. +#tcp.binarylength.prefix.length=2 + +#--------------------------------------------------------------------------- +# Summariser - Generate Summary Results - configuration (mainly applies to non-GUI mode) +#--------------------------------------------------------------------------- +# +# Comment the following property to disable the default non-GUI summariser +# [or change the value to rename it] +# (applies to non-GUI mode only) +summariser.name=summary +# +# interval between summaries (in seconds) default 30 seconds +#summariser.interval=30 +# +# Write messages to log file +#summariser.log=true +# +# Write messages to System.out +#summariser.out=true + +# Ignore SampleResults generated by TransactionControllers +# defaults to true +#summariser.ignore_transaction_controller_sample_result=true + + +#--------------------------------------------------------------------------- +# Aggregate Report and Aggregate Graph - configuration +#--------------------------------------------------------------------------- +# +# Percentiles to display in reports +# Can be float value between 0 and 100 +# First percentile to display, defaults to 90% +#aggregate_rpt_pct1=90 +# Second percentile to display, defaults to 95% +#aggregate_rpt_pct2=95 +# Second percentile to display, defaults to 99% +#aggregate_rpt_pct3=99 + +#--------------------------------------------------------------------------- +# BackendListener - configuration +#--------------------------------------------------------------------------- +# +# Backend metrics window mode (fixed=fixed-size window, timed=time boxed) +#backend_metrics_window_mode=fixed +# Backend metrics sliding window size for Percentiles, Min, Max +#backend_metrics_window=100 + +# Backend metrics sliding window size for Percentiles, Min, Max +# when backend_metrics_window_mode is timed +# Setting this value too high can lead to OOM +#backend_metrics_large_window=5000 + +######################## +# Graphite Backend +######################## +# Send interval in second +# Defaults to 1 second +#backend_graphite.send_interval=1 + +######################## +# Influx Backend +######################## + +# Send interval in second +# Defaults to 5 seconds +#backend_influxdb.send_interval=5 +#Influxdb timeouts +#backend_influxdb.connection_timeout=1000 +#backend_influxdb.socket_timeout=3000 +#backend_influxdb.connection_request_timeout=100 + +#--------------------------------------------------------------------------- +# BeanShell configuration +#--------------------------------------------------------------------------- + +# BeanShell Server properties +# +# Define the port number as non-zero to start the http server on that port +#beanshell.server.port=9000 +# The telnet server will be started on the next port + +# +# Define the server initialisation file +beanshell.server.file=../extras/startup.bsh + +# +# Define a file to be processed at startup +# This is processed using its own interpreter. +#beanshell.init.file= + +# +# Define the intialisation files for BeanShell Sampler, Function and other BeanShell elements +# N.B. Beanshell test elements do not share interpreters. +# Each element in each thread has its own interpreter. +# This is retained between samples. +#beanshell.sampler.init=BeanShellSampler.bshrc +#beanshell.function.init=BeanShellFunction.bshrc +#beanshell.assertion.init=BeanShellAssertion.bshrc +#beanshell.listener.init=etc +#beanshell.postprocessor.init=etc +#beanshell.preprocessor.init=etc +#beanshell.timer.init=etc + +# The file BeanShellListeners.bshrc contains sample definitions +# of Test and Thread Listeners. + +#--------------------------------------------------------------------------- +# JSR-223 function +#--------------------------------------------------------------------------- + +# Path to JSR-223 file containing script to call on JMeter startup +# JMeter will try to guess the engine to use by the extension +# of the name of the file. +# This script can use pre-defined variables: +# log : Logger to log any message +# props : JMeter Property +# OUT : System.OUT +#jsr223.init.file= + +#--------------------------------------------------------------------------- +# Groovy function +#--------------------------------------------------------------------------- + +#Path to Groovy file containing utility functions to make available to __groovy function +#groovy.utilities= + +# Example +#groovy.utilities=jmeter.bin/utility.groovy + +#--------------------------------------------------------------------------- +# MailerModel configuration +#--------------------------------------------------------------------------- + +# Number of successful samples before a message is sent +#mailer.successlimit=2 +# +# Number of failed samples before a message is sent +#mailer.failurelimit=2 + +#--------------------------------------------------------------------------- +# CSVRead configuration +#--------------------------------------------------------------------------- + +# CSVRead delimiter setting (default ",") +# Make sure that there are no trailing spaces or tabs after the delimiter +# characters, or these will be included in the list of valid delimiters +#csvread.delimiter=, +#csvread.delimiter=; +#csvread.delimiter=! +#csvread.delimiter=~ +# The following line has a tab after the = +#csvread.delimiter= + +#--------------------------------------------------------------------------- +# __time() function configuration +# +# The properties below can be used to redefine the default formats +#--------------------------------------------------------------------------- +#time.YMD=yyyyMMdd +#time.HMS=HHmmss +#time.YMDHMS=yyyyMMdd-HHmmss +#time.USER1= +#time.USER2= + +#--------------------------------------------------------------------------- +# CSV DataSet configuration +#--------------------------------------------------------------------------- + +# String to return at EOF (if recycle not used) +#csvdataset.eofstring= +#list in https://docs.oracle.com/javase/8/docs/technotes/guides/intl/encoding.doc.html +csvdataset.file.encoding_list=UTF-8|UTF-16|ISO-8859-15|US-ASCII + + +#--------------------------------------------------------------------------- +# LDAP Sampler configuration +#--------------------------------------------------------------------------- +# Maximum number of search results returned by a search that will be sorted +# to guarantee a stable ordering (if more results then this limit are returned +# then no sorting is done). Set to 0 to turn off all sorting, in which case +# "Equals" response assertions will be very likely to fail against search results. +# +#ldapsampler.max_sorted_results=1000 + +# Number of characters to log for each of three sections (starting matching section, diff section, +# ending matching section where not all sections will appear for all diffs) diff display when an Equals +# assertion fails. So a value of 100 means a maximum of 300 characters of diff text will be displayed +# (+ a number of extra characters like "..." and "[[["/"]]]" which are used to decorate it). +#assertion.equals_section_diff_len=100 +# test written out to log to signify start/end of diff delta +#assertion.equals_diff_delta_start=[[[ +#assertion.equals_diff_delta_end=]]] + +#--------------------------------------------------------------------------- +# Miscellaneous configuration +#--------------------------------------------------------------------------- + +# Size of cache used by CSS Selector Extractor (for JODD implementation only) +# to store parsed CSS Selector expressions. +#cssselector.parser.cache.size=400 + + +# Used to control what happens when you start a test and +# have listeners that could overwrite existing result files +# Possible values: +# ASK : Ask user +# APPEND : Append results to existing file +# DELETE : Delete existing file and start a new file +#resultcollector.action_if_file_exists=ASK + +# If defined, then start the mirror server on the port +#mirror.server.port=8081 + +# ORO PatternCacheLRU size +#oro.patterncache.size=1000 + +#TestBeanGui +# +#propertyEditorSearchPath=null + +# Turn expert mode on/off: expert mode will show expert-mode beans and properties +#jmeter.expertMode=true + +# Max size of bytes stored in memory per SampleResult +# Ensure you don't exceed max capacity of a Java Array and remember +# that the higher it is, the higher JMeter will consume heap +# Defaults to 0, which means no truncation +#httpsampler.max_bytes_to_store_per_request=0 + +# Max size of buffer in bytes used when reading responses +# Defaults to 64k +#httpsampler.max_buffer_size=66560 + +# Maximum redirects to follow in a single sequence (default 20) +#httpsampler.max_redirects=20 +# Maximum frame/iframe nesting depth (default 5) +#httpsampler.max_frame_depth=5 + +# Revert to BUG 51939 behaviour (no separate container for embedded resources) by setting the following false: +#httpsampler.separate.container=true + +# If embedded resources download fails due to missing resources or other reasons, if this property is true +# Parent sample will not be marked as failed +#httpsampler.ignore_failed_embedded_resources=false + +#keep alive time for the parallel download threads (in seconds) +#httpsampler.parallel_download_thread_keepalive_inseconds=60 + +# Don't keep the embedded resources response data : just keep the size and the md5 +# default to false +#httpsampler.embedded_resources_use_md5=false + +# List of extra HTTP methods that should be available in select box +#httpsampler.user_defined_methods=VERSION-CONTROL,REPORT,CHECKOUT,CHECKIN,UNCHECKOUT,MKWORKSPACE,UPDATE,LABEL,MERGE,BASELINE-CONTROL,MKACTIVITY + +# The encoding to be used if none is provided (default ISO-8859-1) +sampleresult.default.encoding=UTF-8 + +# CookieManager behaviour - should cookies with null/empty values be deleted? +# Default is true. Use false to revert to original behaviour +#CookieManager.delete_null_cookies=true + +# CookieManager behaviour - should variable cookies be allowed? +# Default is true. Use false to revert to original behaviour +#CookieManager.allow_variable_cookies=true + +# CookieManager behaviour - should Cookies be stored as variables? +# Default is false +#CookieManager.save.cookies=false + +# CookieManager behaviour - prefix to add to cookie name before storing it as a variable +# Default is COOKIE_; to remove the prefix, define it as one or more spaces +#CookieManager.name.prefix= + +# CookieManager behaviour - check received cookies are valid before storing them? +# Default is true. Use false to revert to previous behaviour +#CookieManager.check.cookies=true + +# Netscape HTTP Cookie file +cookies=cookies + +# Ability to switch to Nashorn as default Javascript Engine used by MsIfController and __javaScript function +# JMeter works as following: +# - JDK >= 8 and javascript.use_rhino=false or not set : Nashorn +# - JDK >= 8 and javascript.use_rhino=true: Rhino +# If you want to use Rhino on JDK8, set this property to true +#javascript.use_rhino=false + +# Number of milliseconds to wait for a thread to stop +#jmeterengine.threadstop.wait=5000 + +#Whether to invoke System.exit(0) in server exit code after stopping RMI +#jmeterengine.remote.system.exit=false + +# Whether to call System.exit(1) on failure to stop threads in non-GUI mode. +# This only takes effect if the test was explicitly requested to stop. +# If this is disabled, it may be necessary to kill the JVM externally +#jmeterengine.stopfail.system.exit=true + +# Whether to force call System.exit(0) at end of test in non-GUI mode, even if +# there were no failures and the test was not explicitly asked to stop. +# Without this, the JVM may never exit if there are other threads spawned by +# the test which never exit. +#jmeterengine.force.system.exit=false + +# How long to pause (in ms) in the daemon thread before reporting that the JVM has failed to exit. +# If the value is <= 0, the JMeter does not start the daemon thread +#jmeter.exit.check.pause=2000 + +# If running non-GUI, then JMeter listens on the following port for a shutdown message. +# To disable, set the port to 1000 or less. +#jmeterengine.nongui.port=4445 +# +# If the initial port is busy, keep trying until this port is reached +# (to disable searching, set the value less than or equal to the .port property) +#jmeterengine.nongui.maxport=4455 + +# How often to check for shutdown during ramp-up (milliseconds) +#jmeterthread.rampup.granularity=1000 + +#Should JMeter expand the tree when loading a test plan? +# default value is false since JMeter 2.7 +#onload.expandtree=false + +#JSyntaxTextArea configuration +#jsyntaxtextarea.wrapstyleword=true +#jsyntaxtextarea.linewrap=true +#jsyntaxtextarea.codefolding=true +# Set 0 to disable undo feature in JSyntaxTextArea +#jsyntaxtextarea.maxundos=50 +# Change the font on the (JSyntax) Text Areas. (Useful for HiDPI screens) +#jsyntaxtextarea.font.family=Hack +#jsyntaxtextarea.font.size=14 + +# Set this to false to disable the use of JSyntaxTextArea for the Console Logger panel +#loggerpanel.usejsyntaxtext=true + +# Maximum size of HTML page that can be displayed; default=10 mbytes +# Set to 0 to disable the size check and display the whole response +#view.results.tree.max_size=10485760 + +# Order of Renderers in View Results Tree +# Note full class names should be used for non jmeter core renderers +# For JMeter core renderers, class names start with . and are automatically +# prefixed with org.apache.jmeter.visualizers +view.results.tree.renderers_order=.RenderAsText,.RenderAsRegexp,.RenderAsBoundaryExtractor,.RenderAsCssJQuery,.RenderAsXPath,org.apache.jmeter.extractor.json.render.RenderAsJsonRenderer,.RenderAsHTML,.RenderAsHTMLFormatted,.RenderAsHTMLWithEmbedded,.RenderAsDocument,.RenderAsJSON,.RenderAsXML + +# Maximum number of results in the results tree +# Set to 0 to store all results (might consume a lot of memory) +#view.results.tree.max_results=500 + +# Maximum size of Document that can be parsed by Tika engine; defaut=10 * 1024 * 1024 (10MB) +# Set to 0 to disable the size check +#document.max_size=0 + +#JMS options +# Enable the following property to stop JMS Point-to-Point Sampler from using +# the properties java.naming.security.[principal|credentials] when creating the queue connection +#JMSSampler.useSecurity.properties=false + +# Set the following value to true in order to skip the delete confirmation dialogue +#confirm.delete.skip=false + +# Used by JSR223 elements +# Size of compiled scripts cache +#jsr223.compiled_scripts_cache_size=100 + +#--------------------------------------------------------------------------- +# Classpath configuration +#--------------------------------------------------------------------------- + +# List of directories (separated by ;) to search for additional JMeter plugin classes, +# for example new GUI elements and samplers. +# Any jar file in such a directory will be automatically included, +# jar files in sub directories are ignored. +# The given value is in addition to any jars found in the lib/ext directory. +# Do not use this for utility or plugin dependency jars. +#search_paths=/app1/lib;/app2/lib + +# List of directories that JMeter will search for utility and plugin dependency classes. +# Use your platform path separator to separate multiple paths. +# Any jar file in such a directory will be automatically included, +# jar files in sub directories are ignored. +# The given value is in addition to any jars found in the lib directory. +# All entries will be added to the class path of the system class loader +# and also to the path of the JMeter internal loader. +# Paths with spaces may cause problems for the JVM +#user.classpath=../classes;../lib + +# List of directories (separated by ;) that JMeter will search for utility +# and plugin dependency classes. +# Any jar file in such a directory will be automatically included, +# jar files in sub directories are ignored. +# The given value is in addition to any jars found in the lib directory +# or given by the user.classpath property. +# All entries will be added to the path of the JMeter internal loader only. +# For plugin dependencies this property should be used instead of user.classpath. +#plugin_dependency_paths=../dependencies/lib;../app1/;../app2/ + +# Classpath finder +# ================ +# The classpath finder currently needs to load every single JMeter class to find +# the classes it needs. +# For non-GUI mode, it's only necessary to scan for Function classes, but all classes +# are still loaded. +# All current Function classes include ".function." in their name, +# and none include ".gui." in the name, so the number of unwanted classes loaded can be +# reduced by checking for these. However, if a valid function class name does not match +# these restrictions, it will not be loaded. If problems are encountered, then comment +# or change the following properties: +classfinder.functions.contain=.functions. +classfinder.functions.notContain=.gui. + + +#--------------------------------------------------------------------------- +# Additional property files to load +#--------------------------------------------------------------------------- + +# Should JMeter automatically load additional JMeter properties? +# File name to look for (comment to disable) +user.properties=user.properties + +# Should JMeter automatically load additional system properties? +# File name to look for (comment to disable) +system.properties=system.properties + +# Comma separated list of files that contain reference to templates and their description +# Path must be relative to JMeter root folder +#template.files=/jmeter.bin/templates/templates.xml + + +#--------------------------------------------------------------------------- +# Thread Group Validation feature +#--------------------------------------------------------------------------- + +# Validation is the name of the feature used to rapidly validate a Thread Group runs fine +# Default implementation is org.apache.jmeter.gui.action.validation.TreeClonerForValidation +# It runs validation without timers, with 1 thread, 1 iteration and Startup Delay set to 0 +# You can implement your own policy that must extend org.apache.jmeter.engine.TreeCloner +# JMeter will instantiate it and use it to create the Tree used to run validation on Thread Group +#testplan_validation.tree_cloner_class=org.apache.jmeter.validation.ComponentTreeClonerForValidation + +# Number of threads to use to validate a Thread Group +#testplan_validation.nb_threads_per_thread_group=1 + +# Ignore BackendListener when validating the thread group of plan +#testplan_validation.ignore_backends=true + +# Ignore timers when validating the thread group of plan +#testplan_validation.ignore_timers=true + +# Number of iterations to use to validate a Thread Group +#testplan_validation.number_iterations=1 + +# Force throuput controllers that work in percentage mode to be a 100% +# Disabled by default +#testplan_validation.tpc_force_100_pct=false + +#--------------------------------------------------------------------------- +# Think Time configuration +#--------------------------------------------------------------------------- + +# +# Apply a factor on computed pauses by the following Timers: +# - Gaussian Random Timer +# - Uniform Random Timer +# - Poisson Random Timer +# +#timer.factor=1.0f + +# Default implementation that create the Timer structure to add to Test Plan +# Implementation of interface org.apache.jmeter.gui.action.thinktime.ThinkTimeCreator +#think_time_creator.impl=org.apache.jmeter.thinktime.DefaultThinkTimeCreator + +# Default Timer GUI class added to Test Plan by DefaultThinkTimeCreator +#think_time_creator.default_timer_implementation=org.apache.jmeter.timers.gui.UniformRandomTimerGui + +# Default constant pause of Timer +#think_time_creator.default_constant_pause=1000 + +# Default range pause of Timer +#think_time_creator.default_range=100 + + +# Change this parameter if you want to override the APDEX satisfaction threshold. +jmeter.reportgenerator.apdex_satisfied_threshold=500 + +# Change this parameter if you want to override the APDEX tolerance threshold. +jmeter.reportgenerator.apdex_tolerated_threshold=1500 + +# Timeout in milliseconds for Report generation when using Tools > Generate HTML report +#generate_report_ui.generation_timeout=120000 +#--------------------------------------------------------------------------- +# Naming Policy configuration +#--------------------------------------------------------------------------- + +# Prefix used when naming elements +#naming_policy.prefix= +# Suffix used when naming elements +#naming_policy.suffix= + +# Implementation of interface org.apache.jmeter.gui.action.TreeNodeNamingPolicy +#naming_policy.impl=org.apache.jmeter.gui.action.impl.DefaultTreeNodeNamingPolicy + +#--------------------------------------------------------------------------- +# Help Documentation +#--------------------------------------------------------------------------- + +# Switch that allows using Local documentation opened in JMeter GUI +# By default we use Online documentation opened in Browser +#help.local=false + +#--------------------------------------------------------------------------- +# Documentation generation +#--------------------------------------------------------------------------- + +# Path to XSL file used to generate Schematic View of Test Plan +# When empty, JMeter will use the embedded one in src/core/org/apache/jmeter/gui/action/schematic.xsl +#docgeneration.schematic_xsl= diff --git a/src/main/resources/jmeter/bin/saveservice.properties b/src/main/resources/jmeter/bin/saveservice.properties new file mode 100644 index 0000000..fd64d6b --- /dev/null +++ b/src/main/resources/jmeter/bin/saveservice.properties @@ -0,0 +1,420 @@ +#--------------------------------------------------------- +# SAVESERVICE PROPERTIES - JMETER INTERNAL USE ONLY +#--------------------------------------------------------- +## 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. +# This file is used to define how XStream (de-)serializes classnames +# in JMX test plan files. +# FOR JMETER INTERNAL USE ONLY +#--------------------------------------------------------- +# N.B. To ensure backward compatibility, please do NOT change or delete any entries +# New entries can be added as necessary. +# +# Note that keys starting with an underscore are special, +# and are not used as aliases. +# +# Please keep the entries in alphabetical order within the sections +# to reduce the likelihood of duplicates +# +# version number of this file is now computed by a sha1 sum, so no need for +# an explicit _file_version property anymore. +# +# For this sha1 sum we ignore every newline character. It can be computed +# by the following command: +# +# cat jmeter.bin/saveservice.properties | perl -ne 'chomp; print' | sha1sum +# +# Be aware, that every change in this file will change the sha1 sum! +# +# Conversion version (for JMX output files) +# Must be updated if the file has been changed since the previous release +# Format is: +# Save service version=JMeter version at which change occurred +# 1.7 = 2.1.1 +# 1.8 = 2.1.2 +# (Some version updates were missed here...) +# 2.0 = 2.3.1 +# 2.1 = 2.3.2 +# (Some version updates were missed here...) +# 2.2 = 2.6 +# 2.3 = 2.7 +# 2.4 = 2.9 +# 2.5 = 2.10 +# 2.6 = 2.11 +# 2.7 = 2.12 +# 2.8 = 2.13 +# 2.9 = 2.14 +# 3.1 = 3.1 +# 3.2 = 3.2 +# 4.0 = 4.0 +# 5.0 = 5.0 +_version=5.0 +# +# +# Character set encoding used to read and write JMeter XML files and CSV results +# +_file_encoding=UTF-8 +# +#--------------------------------------------------------- +# +# The following properties are used to create aliases +# [Must all start with capital letter] +# +AccessLogSampler=org.apache.jmeter.protocol.http.sampler.AccessLogSampler +AjpSampler=org.apache.jmeter.protocol.http.sampler.AjpSampler +AjpSamplerGui=org.apache.jmeter.protocol.http.control.gui.AjpSamplerGui +AnchorModifier=org.apache.jmeter.protocol.http.modifier.AnchorModifier +AnchorModifierGui=org.apache.jmeter.protocol.http.modifier.gui.AnchorModifierGui +Argument=org.apache.jmeter.config.Argument +Arguments=org.apache.jmeter.config.Arguments +ArgumentsPanel=org.apache.jmeter.config.gui.ArgumentsPanel +AssertionGui=org.apache.jmeter.assertions.gui.AssertionGui +AssertionVisualizer=org.apache.jmeter.visualizers.AssertionVisualizer +AuthManager=org.apache.jmeter.protocol.http.control.AuthManager +Authorization=org.apache.jmeter.protocol.http.control.Authorization +AuthPanel=org.apache.jmeter.protocol.http.gui.AuthPanel +BackendListener=org.apache.jmeter.visualizers.backend.BackendListener +BackendListenerGui=org.apache.jmeter.visualizers.backend.BackendListenerGui +BeanShellAssertion=org.apache.jmeter.assertions.BeanShellAssertion +BeanShellAssertionGui=org.apache.jmeter.assertions.gui.BeanShellAssertionGui +BeanShellListener=org.apache.jmeter.visualizers.BeanShellListener +BeanShellPostProcessor=org.apache.jmeter.extractor.BeanShellPostProcessor +BeanShellPreProcessor=org.apache.jmeter.modifiers.BeanShellPreProcessor +BeanShellSampler=org.apache.jmeter.protocol.java.sampler.BeanShellSampler +BeanShellSamplerGui=org.apache.jmeter.protocol.java.control.gui.BeanShellSamplerGui +BeanShellTimer=org.apache.jmeter.timers.BeanShellTimer +BoundaryExtractor=org.apache.jmeter.extractor.BoundaryExtractor +BoundaryExtractorGui=org.apache.jmeter.extractor.gui.BoundaryExtractorGui +BSFAssertion=org.apache.jmeter.assertions.BSFAssertion +BSFListener=org.apache.jmeter.visualizers.BSFListener +BSFPreProcessor=org.apache.jmeter.modifiers.BSFPreProcessor +BSFPostProcessor=org.apache.jmeter.extractor.BSFPostProcessor +BSFSampler=org.apache.jmeter.protocol.java.sampler.BSFSampler +BSFSamplerGui=org.apache.jmeter.protocol.java.control.gui.BSFSamplerGui +BSFTimer=org.apache.jmeter.timers.BSFTimer +CacheManager=org.apache.jmeter.protocol.http.control.CacheManager +CacheManagerGui=org.apache.jmeter.protocol.http.gui.CacheManagerGui +CompareAssertion=org.apache.jmeter.assertions.CompareAssertion +ComparisonVisualizer=org.apache.jmeter.visualizers.ComparisonVisualizer +ConfigTestElement=org.apache.jmeter.config.ConfigTestElement +ConstantThroughputTimer=org.apache.jmeter.timers.ConstantThroughputTimer +ConstantTimer=org.apache.jmeter.timers.ConstantTimer +ConstantTimerGui=org.apache.jmeter.timers.gui.ConstantTimerGui +Cookie=org.apache.jmeter.protocol.http.control.Cookie +CookieManager=org.apache.jmeter.protocol.http.control.CookieManager +CookiePanel=org.apache.jmeter.protocol.http.gui.CookiePanel +CounterConfig=org.apache.jmeter.modifiers.CounterConfig +CriticalSectionController=org.apache.jmeter.control.CriticalSectionController +CriticalSectionControllerGui=org.apache.jmeter.control.gui.CriticalSectionControllerGui +CounterConfigGui=org.apache.jmeter.modifiers.gui.CounterConfigGui +CSVDataSet=org.apache.jmeter.config.CSVDataSet +DebugPostProcessor=org.apache.jmeter.extractor.DebugPostProcessor +DebugSampler=org.apache.jmeter.sampler.DebugSampler +# removed in 3.1, class was deleted in r1763837 +DistributionGraphVisualizer=org.apache.jmeter.visualizers.DistributionGraphVisualizer +DNSCacheManager=org.apache.jmeter.protocol.http.control.DNSCacheManager +DNSCachePanel=org.apache.jmeter.protocol.http.gui.DNSCachePanel +DubboSample=io.github.ningyu.jmeter.plugin.dubbo.sample.DubboSample +DubboSampleGui=io.github.ningyu.jmeter.plugin.dubbo.gui.DubboSampleGui +DubboDefaultConfigGui=io.github.ningyu.jmeter.plugin.dubbo.gui.DubboDefaultConfigGui +DurationAssertion=org.apache.jmeter.assertions.DurationAssertion +DurationAssertionGui=org.apache.jmeter.assertions.gui.DurationAssertionGui +PreciseThroughputTimer=org.apache.jmeter.timers.poissonarrivals.PreciseThroughputTimer +# Should really have been defined as floatProp to agree with other properties +# No point changing this now +FloatProperty=org.apache.jmeter.testelement.property.FloatProperty +ForeachController=org.apache.jmeter.control.ForeachController +ForeachControlPanel=org.apache.jmeter.control.gui.ForeachControlPanel +FtpConfigGui=org.apache.jmeter.protocol.ftp.config.gui.FtpConfigGui +FTPSampler=org.apache.jmeter.protocol.ftp.sampler.FTPSampler +FtpTestSamplerGui=org.apache.jmeter.protocol.ftp.control.gui.FtpTestSamplerGui +GaussianRandomTimer=org.apache.jmeter.timers.GaussianRandomTimer +GaussianRandomTimerGui=org.apache.jmeter.timers.gui.GaussianRandomTimerGui +GenericController=org.apache.jmeter.control.GenericController +GraphAccumVisualizer=org.apache.jmeter.visualizers.GraphAccumVisualizer +GraphVisualizer=org.apache.jmeter.visualizers.GraphVisualizer +Header=org.apache.jmeter.protocol.http.control.Header +HeaderManager=org.apache.jmeter.protocol.http.control.HeaderManager +HeaderPanel=org.apache.jmeter.protocol.http.gui.HeaderPanel +HTMLAssertion=org.apache.jmeter.assertions.HTMLAssertion +HTMLAssertionGui=org.apache.jmeter.assertions.gui.HTMLAssertionGui +HTTPArgument=org.apache.jmeter.protocol.http.util.HTTPArgument +HTTPArgumentsPanel=org.apache.jmeter.protocol.http.gui.HTTPArgumentsPanel +HTTPFileArg=org.apache.jmeter.protocol.http.util.HTTPFileArg +HTTPFileArgs=org.apache.jmeter.protocol.http.util.HTTPFileArgs +HttpDefaultsGui=org.apache.jmeter.protocol.http.config.gui.HttpDefaultsGui +HtmlExtractor=org.apache.jmeter.extractor.HtmlExtractor +HtmlExtractorGui=org.apache.jmeter.extractor.gui.HtmlExtractorGui +# removed in r1039684, probably not released. Not present in r322831 or since. +#HttpGenericSampler=org.apache.jmeter.protocol.http.sampler.HttpGenericSampler +# removed in r1039684, probably not released. Not present in r322831 or since. +#HttpGenericSamplerGui=org.apache.jmeter.protocol.http.control.gui.HttpGenericSamplerGui +HttpMirrorControl=org.apache.jmeter.protocol.http.control.HttpMirrorControl +HttpMirrorControlGui=org.apache.jmeter.protocol.http.control.gui.HttpMirrorControlGui +# r397955 - removed test class. Keep as commented entry for info only. +#HTTPNullSampler=org.apache.jmeter.protocol.http.sampler.HTTPNullSampler +# Merge previous 2 HTTP samplers into one +HTTPSampler_=org.apache.jmeter.protocol.http.sampler.HTTPSampler +HTTPSampler2_=org.apache.jmeter.protocol.http.sampler.HTTPSampler2 +HTTPSamplerProxy,HTTPSampler,HTTPSampler2=org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy +# Merge GUIs +HttpTestSampleGui,HttpTestSampleGui2=org.apache.jmeter.protocol.http.control.gui.HttpTestSampleGui +#HttpTestSampleGui2=org.apache.jmeter.protocol.http.control.gui.HttpTestSampleGui2 +IfController=org.apache.jmeter.control.IfController +IfControllerPanel=org.apache.jmeter.control.gui.IfControllerPanel +IncludeController=org.apache.jmeter.control.IncludeController +IncludeControllerGui=org.apache.jmeter.control.gui.IncludeControllerGui +InterleaveControl=org.apache.jmeter.control.InterleaveControl +InterleaveControlGui=org.apache.jmeter.control.gui.InterleaveControlGui +JavaConfig=org.apache.jmeter.protocol.java.config.JavaConfig +JavaConfigGui=org.apache.jmeter.protocol.java.config.gui.JavaConfigGui +JavaSampler=org.apache.jmeter.protocol.java.sampler.JavaSampler +JavaTest=org.apache.jmeter.protocol.java.test.JavaTest +JavaTestSamplerGui=org.apache.jmeter.protocol.java.control.gui.JavaTestSamplerGui +JDBCDataSource=org.apache.jmeter.protocol.jdbc.config.DataSourceElement +JDBCPostProcessor=org.apache.jmeter.protocol.jdbc.processor.JDBCPostProcessor +JDBCPreProcessor=org.apache.jmeter.protocol.jdbc.processor.JDBCPreProcessor +JDBCSampler=org.apache.jmeter.protocol.jdbc.sampler.JDBCSampler +JMESPathAssertion=org.apache.jmeter.assertions.jmespath.JMESPathAssertion +JMESPathAssertionGui=org.apache.jmeter.assertions.jmespath.gui.JMESPathAssertionGui +JMESPathExtractor=org.apache.jmeter.extractor.json.jmespath.JMESPathExtractor +JMESPathExtractorGui=org.apache.jmeter.extractor.json.jmespath.gui.JMESPathExtractorGui +# Renamed to JMSSamplerGui; keep original entry for backwards compatibility +JMSConfigGui=org.apache.jmeter.protocol.jms.control.gui.JMSConfigGui +JMSProperties=org.apache.jmeter.protocol.jms.sampler.JMSProperties +JMSProperty=org.apache.jmeter.protocol.jms.sampler.JMSProperty +JMSPublisherGui=org.apache.jmeter.protocol.jms.control.gui.JMSPublisherGui +JMSSampler=org.apache.jmeter.protocol.jms.sampler.JMSSampler +JMSSamplerGui=org.apache.jmeter.protocol.jms.control.gui.JMSSamplerGui +JMSSubscriberGui=org.apache.jmeter.protocol.jms.control.gui.JMSSubscriberGui +JSONPathAssertion=org.apache.jmeter.assertions.JSONPathAssertion +JSONPathAssertionGui=org.apache.jmeter.assertions.gui.JSONPathAssertionGui +JSONPostProcessor=org.apache.jmeter.extractor.json.jsonpath.JSONPostProcessor +JSONPostProcessorGui=org.apache.jmeter.extractor.json.jsonpath.gui.JSONPostProcessorGui +# Removed in r545311 as Jndi no longer present; keep for compat. +JndiDefaultsGui=org.apache.jmeter.protocol.jms.control.gui.JndiDefaultsGui +JSR223Assertion=org.apache.jmeter.assertions.JSR223Assertion +JSR223Listener=org.apache.jmeter.visualizers.JSR223Listener +JSR223PostProcessor=org.apache.jmeter.extractor.JSR223PostProcessor +JSR223PreProcessor=org.apache.jmeter.modifiers.JSR223PreProcessor +JSR223Sampler=org.apache.jmeter.protocol.java.sampler.JSR223Sampler +JSR223Timer=org.apache.jmeter.timers.JSR223Timer +JUnitSampler=org.apache.jmeter.protocol.java.sampler.JUnitSampler +JUnitTestSamplerGui=org.apache.jmeter.protocol.java.control.gui.JUnitTestSamplerGui +KeystoreConfig=org.apache.jmeter.config.KeystoreConfig +LDAPArgument=org.apache.jmeter.protocol.ldap.config.gui.LDAPArgument +LDAPArguments=org.apache.jmeter.protocol.ldap.config.gui.LDAPArguments +LDAPArgumentsPanel=org.apache.jmeter.protocol.ldap.config.gui.LDAPArgumentsPanel +LdapConfigGui=org.apache.jmeter.protocol.ldap.config.gui.LdapConfigGui +LdapExtConfigGui=org.apache.jmeter.protocol.ldap.config.gui.LdapExtConfigGui +LDAPExtSampler=org.apache.jmeter.protocol.ldap.sampler.LDAPExtSampler +LdapExtTestSamplerGui=org.apache.jmeter.protocol.ldap.control.gui.LdapExtTestSamplerGui +LDAPSampler=org.apache.jmeter.protocol.ldap.sampler.LDAPSampler +LdapTestSamplerGui=org.apache.jmeter.protocol.ldap.control.gui.LdapTestSamplerGui +LogicControllerGui=org.apache.jmeter.control.gui.LogicControllerGui +LoginConfig=org.apache.jmeter.config.LoginConfig +LoginConfigGui=org.apache.jmeter.config.gui.LoginConfigGui +LoopController=org.apache.jmeter.control.LoopController +LoopControlPanel=org.apache.jmeter.control.gui.LoopControlPanel +MailerModel=org.apache.jmeter.reporters.MailerModel +MailerResultCollector=org.apache.jmeter.reporters.MailerResultCollector +MailerVisualizer=org.apache.jmeter.visualizers.MailerVisualizer +MailReaderSampler=org.apache.jmeter.protocol.mail.sampler.MailReaderSampler +MailReaderSamplerGui=org.apache.jmeter.protocol.mail.sampler.gui.MailReaderSamplerGui +MD5HexAssertion=org.apache.jmeter.assertions.MD5HexAssertion +MD5HexAssertionGUI=org.apache.jmeter.assertions.gui.MD5HexAssertionGUI +ModuleController=org.apache.jmeter.control.ModuleController +ModuleControllerGui=org.apache.jmeter.control.gui.ModuleControllerGui +MongoScriptSampler=org.apache.jmeter.protocol.mongodb.sampler.MongoScriptSampler +MongoSourceElement=org.apache.jmeter.protocol.mongodb.config.MongoSourceElement +# removed in 3.2, class was deleted in r +MonitorHealthVisualizer=org.apache.jmeter.visualizers.MonitorHealthVisualizer +NamePanel=org.apache.jmeter.gui.NamePanel +BoltSampler=org.apache.jmeter.protocol.bolt.sampler.BoltSampler +BoltConnectionElement=org.apache.jmeter.protocol.bolt.config.BoltConnectionElement +ObsoleteGui=org.apache.jmeter.config.gui.ObsoleteGui +OnceOnlyController=org.apache.jmeter.control.OnceOnlyController +OnceOnlyControllerGui=org.apache.jmeter.control.gui.OnceOnlyControllerGui +# removed in 3.0, class was deleted in r1722962 +ParamMask=org.apache.jmeter.protocol.http.modifier.ParamMask +# removed in 3.0, class was deleted in r1722757 +ParamModifier=org.apache.jmeter.protocol.http.modifier.ParamModifier +# removed in 3.0, class was deleted in r1722757 +ParamModifierGui=org.apache.jmeter.protocol.http.modifier.gui.ParamModifierGui +PoissonRandomTimer=org.apache.jmeter.timers.PoissonRandomTimer +PoissonRandomTimerGui=org.apache.jmeter.timers.gui.PoissonRandomTimerGui +PropertyControlGui=org.apache.jmeter.visualizers.PropertyControlGui +ProxyControl=org.apache.jmeter.protocol.http.proxy.ProxyControl +ProxyControlGui=org.apache.jmeter.protocol.http.proxy.gui.ProxyControlGui +PublisherSampler=org.apache.jmeter.protocol.jms.sampler.PublisherSampler +RandomControlGui=org.apache.jmeter.control.gui.RandomControlGui +RandomController=org.apache.jmeter.control.RandomController +RandomOrderController=org.apache.jmeter.control.RandomOrderController +RandomOrderControllerGui=org.apache.jmeter.control.gui.RandomOrderControllerGui +RandomVariableConfig=org.apache.jmeter.config.RandomVariableConfig +RecordController=org.apache.jmeter.protocol.http.control.gui.RecordController +RecordingController=org.apache.jmeter.protocol.http.control.RecordingController +# removed in r1039684, class was deleted in r580452 +ReflectionThreadGroup=org.apache.jmeter.threads.ReflectionThreadGroup +RegexExtractor=org.apache.jmeter.extractor.RegexExtractor +RegexExtractorGui=org.apache.jmeter.extractor.gui.RegexExtractorGui +RegExUserParameters=org.apache.jmeter.protocol.http.modifier.RegExUserParameters +RegExUserParametersGui=org.apache.jmeter.protocol.http.modifier.gui.RegExUserParametersGui +RemoteListenerWrapper=org.apache.jmeter.samplers.RemoteListenerWrapper +RemoteSampleListenerWrapper=org.apache.jmeter.samplers.RemoteSampleListenerWrapper +RemoteTestListenerWrapper=org.apache.jmeter.samplers.RemoteTestListenerWrapper +RemoteThreadsListenerWrapper=org.apache.jmeter.threads.RemoteThreadsListenerWrapper +ResponseAssertion=org.apache.jmeter.assertions.ResponseAssertion +RespTimeGraphVisualizer=org.apache.jmeter.visualizers.RespTimeGraphVisualizer +ResultAction=org.apache.jmeter.reporters.ResultAction +ResultActionGui=org.apache.jmeter.reporters.gui.ResultActionGui +ResultCollector=org.apache.jmeter.reporters.ResultCollector +ResultSaver=org.apache.jmeter.reporters.ResultSaver +ResultSaverGui=org.apache.jmeter.reporters.gui.ResultSaverGui +RunTime=org.apache.jmeter.control.RunTime +RunTimeGui=org.apache.jmeter.control.gui.RunTimeGui +SampleSaveConfiguration=org.apache.jmeter.samplers.SampleSaveConfiguration +SampleTimeout=org.apache.jmeter.modifiers.SampleTimeout +SampleTimeoutGui=org.apache.jmeter.modifiers.gui.SampleTimeoutGui +SimpleConfigGui=org.apache.jmeter.config.gui.SimpleConfigGui +SimpleDataWriter=org.apache.jmeter.visualizers.SimpleDataWriter +SizeAssertion=org.apache.jmeter.assertions.SizeAssertion +SizeAssertionGui=org.apache.jmeter.assertions.gui.SizeAssertionGui +SMIMEAssertion=org.apache.jmeter.assertions.SMIMEAssertionTestElement +SMIMEAssertionGui=org.apache.jmeter.assertions.gui.SMIMEAssertionGui +SmtpSampler=org.apache.jmeter.protocol.smtp.sampler.SmtpSampler +SmtpSamplerGui=org.apache.jmeter.protocol.smtp.sampler.gui.SmtpSamplerGui +# removed in 3.2, class was deleted in r +SoapSampler=org.apache.jmeter.protocol.http.sampler.SoapSampler +# removed in 3.2, class was deleted in r +SoapSamplerGui=org.apache.jmeter.protocol.http.control.gui.SoapSamplerGui +# removed in 3.1, class was deleted in r1763837 +SplineVisualizer=org.apache.jmeter.visualizers.SplineVisualizer +# Originally deleted in r397955 as class is obsolete; needed for compat. +SqlConfigGui=org.apache.jmeter.protocol.jdbc.config.gui.SqlConfigGui +StaticHost=org.apache.jmeter.protocol.http.control.StaticHost +StatGraphVisualizer=org.apache.jmeter.visualizers.StatGraphVisualizer +StatVisualizer=org.apache.jmeter.visualizers.StatVisualizer +SubscriberSampler=org.apache.jmeter.protocol.jms.sampler.SubscriberSampler +SubstitutionElement=org.apache.jmeter.assertions.SubstitutionElement +Summariser=org.apache.jmeter.reporters.Summariser +SummariserGui=org.apache.jmeter.reporters.gui.SummariserGui +SummaryReport=org.apache.jmeter.visualizers.SummaryReport +SwitchController=org.apache.jmeter.control.SwitchController +SwitchControllerGui=org.apache.jmeter.control.gui.SwitchControllerGui +SyncTimer=org.apache.jmeter.timers.SyncTimer +SystemSampler=org.apache.jmeter.protocol.system.SystemSampler +SystemSamplerGui=org.apache.jmeter.protocol.system.gui.SystemSamplerGui +TableVisualizer=org.apache.jmeter.visualizers.TableVisualizer +TCPConfigGui=org.apache.jmeter.protocol.tcp.config.gui.TCPConfigGui +TCPSampler=org.apache.jmeter.protocol.tcp.sampler.TCPSampler +TCPSamplerGui=org.apache.jmeter.protocol.tcp.control.gui.TCPSamplerGui +TestAction=org.apache.jmeter.sampler.TestAction +TestActionGui=org.apache.jmeter.sampler.gui.TestActionGui +TestBeanGUI=org.apache.jmeter.testbeans.gui.TestBeanGUI +TestFragmentController=org.apache.jmeter.control.TestFragmentController +TestFragmentControllerGui=org.apache.jmeter.control.gui.TestFragmentControllerGui +TestPlan=org.apache.jmeter.testelement.TestPlan +TestPlanGui=org.apache.jmeter.control.gui.TestPlanGui +ThreadGroup=org.apache.jmeter.threads.ThreadGroup +ThreadGroupGui=org.apache.jmeter.threads.gui.ThreadGroupGui +PostThreadGroup=org.apache.jmeter.threads.PostThreadGroup +PostThreadGroupGui=org.apache.jmeter.threads.gui.PostThreadGroupGui +SetupThreadGroup=org.apache.jmeter.threads.SetupThreadGroup +SetupThreadGroupGui=org.apache.jmeter.threads.gui.SetupThreadGroupGui +ThroughputController=org.apache.jmeter.control.ThroughputController +ThroughputControllerGui=org.apache.jmeter.control.gui.ThroughputControllerGui +TransactionController=org.apache.jmeter.control.TransactionController +TransactionControllerGui=org.apache.jmeter.control.gui.TransactionControllerGui +TransactionSampler=org.apache.jmeter.control.TransactionSampler +UniformRandomTimer=org.apache.jmeter.timers.UniformRandomTimer +UniformRandomTimerGui=org.apache.jmeter.timers.gui.UniformRandomTimerGui +URLRewritingModifier=org.apache.jmeter.protocol.http.modifier.URLRewritingModifier +URLRewritingModifierGui=org.apache.jmeter.protocol.http.modifier.gui.URLRewritingModifierGui +UserParameterModifier=org.apache.jmeter.protocol.http.modifier.UserParameterModifier +UserParameterModifierGui=org.apache.jmeter.protocol.http.modifier.gui.UserParameterModifierGui +UserParameters=org.apache.jmeter.modifiers.UserParameters +UserParametersGui=org.apache.jmeter.modifiers.gui.UserParametersGui +ViewResultsFullVisualizer=org.apache.jmeter.visualizers.ViewResultsFullVisualizer +# removed in 3.0, class was deleted in r1722757 +WebServiceSampler=org.apache.jmeter.protocol.http.sampler.WebServiceSampler +# removed in 3.0, class was deleted in r1722757 +WebServiceSamplerGui=org.apache.jmeter.protocol.http.control.gui.WebServiceSamplerGui +WhileController=org.apache.jmeter.control.WhileController +WhileControllerGui=org.apache.jmeter.control.gui.WhileControllerGui +WorkBench=org.apache.jmeter.testelement.WorkBench +WorkBenchGui=org.apache.jmeter.control.gui.WorkBenchGui +XMLAssertion=org.apache.jmeter.assertions.XMLAssertion +XMLAssertionGui=org.apache.jmeter.assertions.gui.XMLAssertionGui +XMLSchemaAssertion=org.apache.jmeter.assertions.XMLSchemaAssertion +XMLSchemaAssertionGUI=org.apache.jmeter.assertions.gui.XMLSchemaAssertionGUI +XPathAssertion=org.apache.jmeter.assertions.XPathAssertion +XPathAssertionGui=org.apache.jmeter.assertions.gui.XPathAssertionGui +XPath2Assertion=org.apache.jmeter.assertions.XPath2Assertion +XPath2AssertionGui=org.apache.jmeter.assertions.gui.XPath2AssertionGui +XPathExtractor=org.apache.jmeter.extractor.XPathExtractor +XPathExtractorGui=org.apache.jmeter.extractor.gui.XPathExtractorGui +XPath2Extractor=org.apache.jmeter.extractor.XPath2Extractor +XPath2ExtractorGui=org.apache.jmeter.extractor.gui.XPath2ExtractorGui +# Properties - all start with lower case letter and end with Prop +# +boolProp=org.apache.jmeter.testelement.property.BooleanProperty +collectionProp=org.apache.jmeter.testelement.property.CollectionProperty +doubleProp=org.apache.jmeter.testelement.property.DoubleProperty +elementProp=org.apache.jmeter.testelement.property.TestElementProperty +# see above - already defined as FloatProperty +#floatProp=org.apache.jmeter.testelement.property.FloatProperty +intProp=org.apache.jmeter.testelement.property.IntegerProperty +longProp=org.apache.jmeter.testelement.property.LongProperty +mapProp=org.apache.jmeter.testelement.property.MapProperty +objProp=org.apache.jmeter.testelement.property.ObjectProperty +stringProp=org.apache.jmeter.testelement.property.StringProperty +# +# Other - must start with a lower case letter (and not end with Prop) +# (otherwise they could clash with the initial set of aliases) +# +hashTree=org.apache.jorphan.collections.ListedHashTree +jmeterTestPlan=org.apache.jmeter.save.ScriptWrapper +sample=org.apache.jmeter.samplers.SampleResult +httpSample=org.apache.jmeter.protocol.http.sampler.HTTPSampleResult +statSample=org.apache.jmeter.samplers.StatisticalSampleResult +testResults=org.apache.jmeter.save.TestResultWrapper +assertionResult=org.apache.jmeter.assertions.AssertionResult +# removed in 3.2, class was deleted in r +monitorStats=org.apache.jmeter.visualizers.MonitorStats +sampleEvent=org.apache.jmeter.samplers.SampleEvent +# +# Converters to register. Must start line with '_' +# If the converter is a collection of subitems, set equal to "collection" +# If the converter needs to know the class mappings but is not a collection of +# subitems, set it equal to "mapping" +_org.apache.jmeter.protocol.http.sampler.HTTPSamplerBaseConverter=collection +_org.apache.jmeter.protocol.http.util.HTTPResultConverter=collection +_org.apache.jmeter.save.converters.BooleanPropertyConverter= +_org.apache.jmeter.save.converters.IntegerPropertyConverter= +_org.apache.jmeter.save.converters.LongPropertyConverter= +_org.apache.jmeter.save.converters.MultiPropertyConverter=collection +_org.apache.jmeter.save.converters.SampleEventConverter= +_org.apache.jmeter.save.converters.SampleResultConverter=collection +_org.apache.jmeter.save.converters.SampleSaveConfigurationConverter=collection +_org.apache.jmeter.save.converters.StringPropertyConverter= +_org.apache.jmeter.save.converters.HashTreeConverter=collection +_org.apache.jmeter.save.converters.TestElementConverter=collection +_org.apache.jmeter.save.converters.TestElementPropertyConverter=collection +_org.apache.jmeter.save.converters.TestResultWrapperConverter=collection +_org.apache.jmeter.save.ScriptWrapperConverter=mapping +# +# Remember to update the _version entry +# \ No newline at end of file diff --git a/src/main/resources/jmeter/bin/upgrade.properties b/src/main/resources/jmeter/bin/upgrade.properties new file mode 100644 index 0000000..03742c0 --- /dev/null +++ b/src/main/resources/jmeter/bin/upgrade.properties @@ -0,0 +1,123 @@ +# Class, property and value upgrade equivalences. + +## 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. + +# +# Format is as follows -- +# for renamed test element & GUI classes: +# old.class.Name=new.class.Name +# old.class.Name|guiClassName=new.class.Name +# (e.g. for ConfigTestElement) +# +# for renamed / deleted properties: +# class.Name/Old.propertyName=newPropertyName +# if newPropertyName is omitted, then property is deleted +# +# for renamed values: +# old.class.Name.old.propertyName/oldValue=newValue +# + +org.apache.jmeter.protocol.http.config.gui.UrlConfigGui=org.apache.jmeter.protocol.http.config.gui.HttpDefaultsGui +org.apache.jmeter.assertions.Assertion=org.apache.jmeter.assertions.ResponseAssertion +org.apache.jmeter.protocol.http.sampler.HTTPSamplerFull=org.apache.jmeter.protocol.http.sampler.HTTPSampler +org.apache.jmeter.control.gui.RecordController=org.apache.jmeter.protocol.http.control.gui.RecordController + +org.apache.jmeter.timers.gui.ConstantThroughputTimerGui=org.apache.jmeter.testbeans.gui.TestBeanGUI +org.apache.jmeter.timers.ConstantThroughputTimer/ConstantThroughputTimer.throughput=throughput + +org.apache.jmeter.protocol.jdbc.control.gui.JdbcTestSampleGui=org.apache.jmeter.testbeans.gui.TestBeanGUI +org.apache.jmeter.protocol.jdbc.sampler.JDBCSampler/JDBCSampler.query=query +#org.apache.jmeter.protocol.jdbc.sampler.JDBCSampler.JDBCSampler.dataSource/NULL= + +# Convert DBconfig +org.apache.jmeter.protocol.jdbc.config.gui.DbConfigGui=org.apache.jmeter.testbeans.gui.TestBeanGUI +org.apache.jmeter.config.ConfigTestElement|org.apache.jmeter.protocol.jdbc.config.gui.DbConfigGui=org.apache.jmeter.protocol.jdbc.config.DataSourceElement +org.apache.jmeter.protocol.jdbc.config.DataSourceElement/JDBCSampler.url=dbUrl +org.apache.jmeter.protocol.jdbc.config.DataSourceElement/JDBCSampler.driver=driver +org.apache.jmeter.protocol.jdbc.config.DataSourceElement/JDBCSampler.query=query +org.apache.jmeter.protocol.jdbc.config.DataSourceElement/ConfigTestElement.username=username +org.apache.jmeter.protocol.jdbc.config.DataSourceElement/ConfigTestElement.password=password + +# Convert PoolConfig +org.apache.jmeter.protocol.jdbc.config.gui.PoolConfigGui=org.apache.jmeter.testbeans.gui.TestBeanGUI +org.apache.jmeter.config.ConfigTestElement|org.apache.jmeter.protocol.jdbc.config.gui.PoolConfigGui=org.apache.jmeter.protocol.jdbc.config.DataSourceElement +org.apache.jmeter.protocol.jdbc.config.DataSourceElement/JDBCSampler.connections= +org.apache.jmeter.protocol.jdbc.config.DataSourceElement/JDBCSampler.connPoolClass= +org.apache.jmeter.protocol.jdbc.config.DataSourceElement/JDBCSampler.maxuse=poolMax + +# SQL Config +org.apache.jmeter.config.ConfigTestElement/JDBCSampler.query=query +org.apache.jmeter.protocol.http.control.Header/TestElement.name=Header.name + +# Upgrade AccessLogSampler +org.apache.jmeter.protocol.http.control.gui.AccessLogSamplerGui=org.apache.jmeter.testbeans.gui.TestBeanGUI +org.apache.jmeter.protocol.http.sampler.AccessLogSampler/AccessLogSampler.log_file=logFile +org.apache.jmeter.protocol.http.sampler.AccessLogSampler/HTTPSampler.port=portString +#Is the following used now? +#org.apache.jmeter.protocol.http.sampler.AccessLogSampler/AccessLogSampler.generator_class_name= +#Looks to be a new field +#filterClassName +org.apache.jmeter.protocol.http.sampler.AccessLogSampler/HTTPSampler.domain=domain +org.apache.jmeter.protocol.http.sampler.AccessLogSampler/AccessLogSampler.parser_class_name=parserClassName +org.apache.jmeter.protocol.http.sampler.AccessLogSampler/HTTPSampler.image_parser=imageParsing + +# Renamed class +org.apache.jmeter.protocol.jms.control.gui.JMSConfigGui=org.apache.jmeter.protocol.jms.control.gui.JMSSamplerGui + +# These classes have been deleted; there's no defined replacement +org.apache.jmeter.protocol.jdbc.config.gui.SqlConfigGui=org.apache.jmeter.config.gui.ObsoleteGui +org.apache.jmeter.protocol.jms.control.gui.JndiDefaultsGui=org.apache.jmeter.config.gui.ObsoleteGui +# Should probably map to something other than ObsoleteGui... +org.apache.jmeter.threads.ReflectionThreadGroup=org.apache.jmeter.config.gui.ObsoleteGui + +# Convert BSFSamplerGui +org.apache.jmeter.protocol.java.control.gui.BSFSamplerGui=org.apache.jmeter.testbeans.gui.TestBeanGUI +org.apache.jmeter.protocol.java.sampler.BSFSampler/BSFSampler.filename=filename +org.apache.jmeter.protocol.java.sampler.BSFSampler/BSFSampler.language=scriptLanguage +org.apache.jmeter.protocol.java.sampler.BSFSampler/BSFSampler.parameters=parameters +org.apache.jmeter.protocol.java.sampler.BSFSampler/BSFSampler.query=script + +# Obsolete Http user Parameters modifier test element +# Note: ConfigTestElement is the test element associated with ObsoleteGui +org.apache.jmeter.protocol.http.modifier.UserParameterModifier=org.apache.jmeter.config.ConfigTestElement +org.apache.jmeter.protocol.http.modifier.gui.UserParameterModifierGui=org.apache.jmeter.config.gui.ObsoleteGui + +# Obsolete Graph Full Results listener +org.apache.jmeter.visualizers.GraphAccumVisualizer=org.apache.jmeter.config.gui.ObsoleteGui +# removed in 3.0, class was deleted in r1722757 +org.apache.jmeter.protocol.http.sampler.WebServiceSampler=org.apache.jmeter.config.ConfigTestElement +# removed in 3.0, class was deleted in r1722757 +org.apache.jmeter.protocol.http.control.gui.WebServiceSamplerGui=org.apache.jmeter.config.gui.ObsoleteGui +# removed in 3.0, class was deleted in r1722757 +org.apache.jmeter.protocol.http.modifier.ParamModifier=org.apache.jmeter.config.ConfigTestElement +# removed in 3.0, class was deleted in r1722962 +org.apache.jmeter.protocol.http.modifier.ParamMask=org.apache.jmeter.config.ConfigTestElement +# removed in 3.0, class was deleted in r1722757 +org.apache.jmeter.protocol.http.modifier.gui.ParamModifierGui=org.apache.jmeter.config.gui.ObsoleteGui + +# removed in 3.1, class was deleted in r1774947 +org.apache.jmeter.visualizers.SplineVisualizer=org.apache.jmeter.config.gui.ObsoleteGui +# removed in 3.1 class was deleted in r1763837 +org.apache.jmeter.visualizers.DistributionGraphVisualizer=org.apache.jmeter.config.gui.ObsoleteGui + +# removed in 3.2 class was deleted in r1771608 +org.apache.jmeter.visualizers.MonitorStats=org.apache.jmeter.config.ConfigTestElement +org.apache.jmeter.visualizers.MonitorHealthVisualizer=org.apache.jmeter.config.gui.ObsoleteGui + +# removed in 3.2 class was deleted in r1783280 +org.apache.jmeter.protocol.http.sampler.HTTPSampler2=org.apache.jmeter.config.ConfigTestElement +org.apache.jmeter.protocol.http.sampler.SoapSampler=org.apache.jmeter.config.ConfigTestElement +org.apache.jmeter.protocol.http.control.gui.SoapSamplerGui=org.apache.jmeter.config.gui.ObsoleteGui \ No newline at end of file diff --git a/src/main/resources/jmeter/bin/user.properties b/src/main/resources/jmeter/bin/user.properties new file mode 100644 index 0000000..d6a2625 --- /dev/null +++ b/src/main/resources/jmeter/bin/user.properties @@ -0,0 +1,147 @@ +# Sample user.properties file +# +## 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. + +#--------------------------------------------------------------------------- +# Classpath configuration +#--------------------------------------------------------------------------- +# +# List of paths (separated by ;) to search for additional JMeter plugin classes, +# for example new GUI elements and samplers. +# A path item can either be a jar file or a directory. +# Any jar file in such a directory will be automatically included, +# jar files in sub directories are ignored. +# The given value is in addition to any jars found in the lib/ext directory. +# Do not use this for utility or plugin dependency jars. +#search_paths=/app1/lib;/app2/lib + +# List of paths that JMeter will search for utility and plugin dependency classes. +# Use your platform path separator (java.io.File.pathSeparatorChar in Java) to separate multiple paths. +# A path item can either be a jar file or a directory. +# Any jar file in such a directory will be automatically included, +# jar files in sub directories are ignored. +# The given value is in addition to any jars found in the lib directory. +# All entries will be added to the class path of the system class loader +# and also to the path of the JMeter internal loader. +# Paths with spaces may cause problems for the JVM +#Example for windows (; separator) +#user.classpath=../classes;../lib;../app1/jar1.jar;../app2/jar2.jar +#Example for linux (:separator) +#user.classpath=../classes:../lib:../app1/jar1.jar:../app2/jar2.jar + +# List of paths (separated by ;) that JMeter will search for utility +# and plugin dependency classes. +# A path item can either be a jar file or a directory. +# Any jar file in such a directory will be automatically included, +# jar files in sub directories are ignored. +# The given value is in addition to any jars found in the lib directory +# or given by the user.classpath property. +# All entries will be added to the path of the JMeter internal loader only. +# For plugin dependencies using plugin_dependency_paths should be preferred over +# user.classpath. +#plugin_dependency_paths=../dependencies/lib;../app1/jar1.jar;../app2/jar2.jar + +#--------------------------------------------------------------------------- +# Reporting configuration +#--------------------------------------------------------------------------- + +# Configure this property to change the report title +#jmeter.reportgenerator.report_title=Apache JMeter Dashboard + +# Used to generate a report based on a date range if needed +# Default date format (from SimpleDateFormat Java API and Locale.ENGLISH) +#jmeter.reportgenerator.date_format=yyyyMMddHHmmss +# Date range start date using date_format property +#jmeter.reportgenerator.start_date= +# Date range end date using date_format property +#jmeter.reportgenerator.end_date= + +# Change this parameter if you want to change the granularity of over time graphs. +#jmeter.reportgenerator.overall_granularity=60000 + +# Change this parameter if you want to change the granularity of Response time distribution +# Set to 100 ms by default +#jmeter.reportgenerator.graph.responseTimeDistribution.property.set_granularity=100 + +# Change this parameter if you want to keep only some samples. +# Regular Expression which Indicates which samples to keep for graphs and statistics generation. +# Empty value means no filtering +#jmeter.reportgenerator.sample_filter= + +# Change this parameter if you want to override the APDEX satisfaction threshold. +#jmeter.reportgenerator.apdex_satisfied_threshold=500 + +# Change this parameter if you want to override the APDEX tolerance threshold. +#jmeter.reportgenerator.apdex_tolerated_threshold=1500 + +# Indicates which graph series are filtered (regular expression) +# In the below example we filter on Search and Order samples +# Note that the end of the pattern should always include (-success|-failure)?$ +# TransactionsPerSecondGraphConsumer suffixes transactions with "-success" or "-failure" depending +# on the result +#jmeter.reportgenerator.exporter.html.series_filter=^(Search|Order)(-success|-failure)?$ + +# Indicates whether only controller samples are displayed on graphs that support it. +#jmeter.reportgenerator.exporter.html.show_controllers_only=false + +# This property is used by menu item "Export transactions for report" +# It is used to select which transactions by default will be exported +#jmeter.reportgenerator.exported_transactions_pattern=[a-zA-Z0-9_\\-{}\\$\\.]*[-_][0-9]* + + +## Custom graph definition +#jmeter.reportgenerator.graph.custom_mm_hit.classname=org.apache.jmeter.report.processor.graph.impl.CustomGraphConsumer +#jmeter.reportgenerator.graph.custom_mm_hit.title=Graph Title +#jmeter.reportgenerator.graph.custom_mm_hit.property.set_Y_Axis=Response Time (ms) +#jmeter.reportgenerator.graph.custom_mm_hit.property.set_X_Axis=Over Time +#jmeter.reportgenerator.graph.custom_mm_hit.property.set_granularity=${jmeter.reportgenerator.overall_granularity} +#jmeter.reportgenerator.graph.custom_mm_hit.property.setSampleVariableName=VarName +#jmeter.reportgenerator.graph.custom_mm_hit.property.setContentMessage=Message for graph point label + +######################################################################## +################## DISTRIBUTED TESTING CONFIGURATION ################## +######################################################################## +# Type of keystore : JKS +# +#server.rmi.ssl.keystore.type=JKS +# +# Keystore file that contains private key +# +#server.rmi.ssl.keystore.file=rmi_keystore.jks +# +# Password of Keystore +# +#server.rmi.ssl.keystore.password=changeit +# +# Key alias +# +#server.rmi.ssl.keystore.alias=rmi +# +# Type of truststore : JKS +# +#server.rmi.ssl.truststore.type=JKS +# +# Keystore file that contains certificate +# +#server.rmi.ssl.truststore.file=rmi_keystore.jks +# +# Password of Trust store +# +#server.rmi.ssl.truststore.password=changeit +# +# Set this if you don't want to use SSL for RMI +# +#server.rmi.ssl.disable=false \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index d5eae80..455a999 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -153,8 +153,8 @@ - - + + \ No newline at end of file