Merge branch 'v1.8' into pr@master@fix_合并1.8

# Conflicts:
#	frontend/src/business/components/api/automation/report/ApiReportDetail.vue
#	frontend/src/business/components/api/automation/report/components/RequestResult.vue
#	frontend/src/business/components/api/definition/components/case/ApiCaseList.vue
#	frontend/src/business/components/api/definition/components/list/ApiCaseSimpleList.vue
#	frontend/src/business/components/common/components/MsModuleMinder.vue
#	frontend/src/business/components/track/review/view/components/TestReviewTestCaseList.vue
This commit is contained in:
fit2-zhao 2021-04-15 14:47:03 +08:00
commit 58593b4337
45 changed files with 1645 additions and 301 deletions

View File

@ -66,6 +66,7 @@ public class ApiAutomationController {
public void update(@RequestPart("request") SaveApiScenarioRequest request, @RequestPart(value = "files") List<MultipartFile> bodyFiles) {
apiAutomationService.update(request, bodyFiles);
}
@GetMapping("/delete/{id}")
public void delete(@PathVariable String id) {
apiAutomationService.delete(id);
@ -139,7 +140,7 @@ public class ApiAutomationController {
}
@PostMapping(value = "/run/batch")
public String runBatcah(@RequestBody RunScenarioRequest request) {
public String runBatch(@RequestBody RunScenarioRequest request) {
request.setExecuteType(ExecuteType.Saved.name());
request.setTriggerMode(ApiRunMode.SCENARIO.name());
request.setRunMode(ApiRunMode.SCENARIO.name());
@ -174,7 +175,7 @@ public class ApiAutomationController {
}
@PostMapping("/relevance/review")
public void testCaseReviewRelevance(@RequestBody ApiCaseRelevanceRequest request){
public void testCaseReviewRelevance(@RequestBody ApiCaseRelevanceRequest request) {
apiAutomationService.relevanceReview(request);
}

View File

@ -0,0 +1,14 @@
package io.metersphere.api.dto;
import io.metersphere.api.dto.automation.RunModeConfig;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class RunRequest {
private String testId;
private String runMode;
private String jmx;
private RunModeConfig config;
}

View File

@ -0,0 +1,11 @@
package io.metersphere.api.dto.automation;
import lombok.Data;
@Data
public class RunModeConfig {
private String mode;
private String reportType;
private String reportName;
private boolean onSampleError;
}

View File

@ -21,7 +21,7 @@ public class RunScenarioRequest extends ApiScenarioWithBLOBs {
private String runMode;
//测试情景和测试计划的关联ID
/**测试情景和测试计划的关联ID*/
private String planScenarioId;
private List<String> planCaseIds;
@ -30,8 +30,9 @@ public class RunScenarioRequest extends ApiScenarioWithBLOBs {
private String reportUserID;
private Map<String,String> scenarioTestPlanIdMap;
private Map<String, String> scenarioTestPlanIdMap;
private ApiScenarioRequest condition;
private RunModeConfig config;
}

View File

@ -18,6 +18,7 @@ import java.util.List;
@JSONType(typeName = "TestPlan")
public class MsTestPlan extends MsTestElement {
private String type = "TestPlan";
private boolean serializeThreadgroups = false;
@Override
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, ParameterConfig config) {
@ -35,7 +36,7 @@ public class MsTestPlan extends MsTestElement {
testPlan.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("TestPlanGui"));
testPlan.setEnabled(true);
testPlan.setFunctionalMode(false);
testPlan.setSerialized(true);
testPlan.setSerialized(serializeThreadgroups);
testPlan.setTearDownOnShutdown(true);
testPlan.setUserDefinedVariables(new Arguments());
return testPlan;

View File

@ -19,6 +19,7 @@ import java.util.List;
public class MsThreadGroup extends MsTestElement {
private String type = "ThreadGroup";
private boolean enableCookieShare;
private boolean onSampleError;
@Override
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, ParameterConfig config) {
@ -59,6 +60,9 @@ public class MsThreadGroup extends MsTestElement {
threadGroup.setDuration(0);
threadGroup.setProperty(ThreadGroup.ON_SAMPLE_ERROR, ThreadGroup.ON_SAMPLE_ERROR_CONTINUE);
threadGroup.setScheduler(false);
if (onSampleError) {
threadGroup.setProperty("ThreadGroup.on_sample_error", "stoptest");
}
threadGroup.setSamplerController(loopController);
return threadGroup;
}

View File

@ -76,8 +76,12 @@ public class MsIfController extends MsTestElement {
public String getCondition() {
String variable = "\"" + this.variable + "\"";
String operator = this.operator;
String value = "\"" + this.value + "\"";
String value;
if (StringUtils.equals(operator, "<") || StringUtils.equals(operator, ">")) {
value = this.value;
} else {
value = "\"" + this.value + "\"";
}
if (StringUtils.contains(operator, "~")) {
value = "\".*" + this.value + ".*\"";
}

View File

@ -56,7 +56,7 @@ public class MsLoopController extends MsTestElement {
}
final HashTree groupTree = controller(tree);
if (CollectionUtils.isNotEmpty(config.getVariables())) {
this.addCsvDataSet(groupTree, config.getVariables(),config);
this.addCsvDataSet(groupTree, config.getVariables(), config);
this.addCounter(groupTree, config.getVariables());
this.addRandom(groupTree, config.getVariables());
}
@ -111,7 +111,12 @@ public class MsLoopController extends MsTestElement {
private String getCondition() {
String variable = "\"" + this.whileController.getVariable() + "\"";
String operator = this.whileController.getOperator();
String value = "\"" + this.whileController.getValue() + "\"";
String value;
if (StringUtils.equals(operator, "<") || StringUtils.equals(operator, ">")) {
value = this.whileController.getValue();
} else {
value = "\"" + this.whileController.getValue() + "\"";
}
if (StringUtils.contains(operator, "~")) {
value = "\".*" + this.whileController.getValue() + ".*\"";

View File

@ -7,8 +7,18 @@ import io.metersphere.api.dto.definition.request.ParameterConfig;
import io.metersphere.api.dto.definition.request.auth.MsAuthManager;
import io.metersphere.api.dto.definition.request.dns.MsDNSCacheManager;
import io.metersphere.api.dto.scenario.Body;
import io.metersphere.api.dto.scenario.HttpConfig;
import io.metersphere.api.dto.scenario.HttpConfigCondition;
import io.metersphere.api.dto.scenario.KeyValue;
import io.metersphere.api.service.ApiDefinitionService;
import io.metersphere.api.service.ApiModuleService;
import io.metersphere.api.service.ApiTestEnvironmentService;
import io.metersphere.base.domain.ApiDefinition;
import io.metersphere.base.domain.ApiTestEnvironmentWithBLOBs;
import io.metersphere.commons.constants.ConditionType;
import io.metersphere.commons.constants.MsTestElementConstants;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.CommonBeanFactory;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.commons.utils.ScriptEngineUtils;
import lombok.Data;
@ -128,7 +138,7 @@ public class MsHTTPSamplerProxy extends MsTestElement {
}
// 数据兼容处理
if(config.getConfig() != null && StringUtils.isNotEmpty(this.getProjectId()) && config.getConfig().containsKey(this.getProjectId())){
if (config.getConfig() != null && StringUtils.isNotEmpty(this.getProjectId()) && config.getConfig().containsKey(this.getProjectId())) {
// 1.8 之后 当前正常数据
} else if (config.getConfig() != null && config.getConfig().containsKey(getParentProjectId())) {
// 1.8 前后 混合数据
@ -155,12 +165,15 @@ public class MsHTTPSamplerProxy extends MsTestElement {
}
try {
if (config.isEffective(this.getProjectId())) {
String url = config.getConfig().get(this.getProjectId()).getHttpConfig().getProtocol() + "://" + config.getConfig().get(this.getProjectId()).getHttpConfig().getSocket();
HttpConfig httpConfig = getHttpConfig(config.getConfig().get(this.getProjectId()).getHttpConfig());
if (httpConfig == null) {
MSException.throwException("未匹配到环境,请检查环境配置");
}
String url = httpConfig.getProtocol() + "://" + httpConfig.getSocket();
// 补充如果是完整URL 则用自身URL
boolean isUrl = false;
if (StringUtils.isNotEmpty(this.getUrl()) && isURL(this.getUrl())) {
boolean isUrl;
if (isUrl = (StringUtils.isNotEmpty(this.getUrl()) && isURL(this.getUrl()))) {
url = this.getUrl();
isUrl = true;
}
if (isUrl) {
if (StringUtils.isNotEmpty(this.getPort()) && this.getPort().startsWith("${")) {
@ -177,15 +190,15 @@ public class MsHTTPSamplerProxy extends MsTestElement {
sampler.setProtocol(urlObject.getProtocol());
sampler.setPath(urlObject.getPath());
} else {
sampler.setDomain(config.getConfig().get(this.getProjectId()).getHttpConfig().getDomain());
url = config.getConfig().get(this.getProjectId()).getHttpConfig().getProtocol() + "://" + config.getConfig().get(this.getProjectId()).getHttpConfig().getSocket();
sampler.setDomain(httpConfig.getDomain());
url = httpConfig.getProtocol() + "://" + httpConfig.getSocket();
URL urlObject = new URL(url);
String envPath = StringUtils.equals(urlObject.getPath(), "/") ? "" : urlObject.getPath();
if (StringUtils.isNotBlank(this.getPath())) {
envPath += this.getPath();
}
sampler.setPort(config.getConfig().get(this.getProjectId()).getHttpConfig().getPort());
sampler.setProtocol(config.getConfig().get(this.getProjectId()).getHttpConfig().getProtocol());
sampler.setPort(httpConfig.getPort());
sampler.setProtocol(httpConfig.getProtocol());
sampler.setPath(envPath);
}
String envPath = sampler.getPath();
@ -233,14 +246,7 @@ public class MsHTTPSamplerProxy extends MsTestElement {
}
} catch (Exception e) {
LogUtil.error(e);
}
// REST参数
if (CollectionUtils.isNotEmpty(this.getRest())) {
sampler.setArguments(httpArguments(this.getRest()));
}
// 请求参数
if (CollectionUtils.isNotEmpty(this.getArguments())) {
sampler.setArguments(httpArguments(this.getArguments()));
MSException.throwException(e.getMessage());
}
// 请求体
if (!StringUtils.equals(this.getMethod(), "GET")) {
@ -298,7 +304,7 @@ public class MsHTTPSamplerProxy extends MsTestElement {
private String getParentProjectId() {
MsTestElement parent = this.getParent();
while(parent != null) {
while (parent != null) {
if (StringUtils.isNotBlank(parent.getProjectId())) {
return parent.getProjectId();
}
@ -378,7 +384,7 @@ public class MsHTTPSamplerProxy extends MsTestElement {
}
}
public boolean isURL(String str) {
private boolean isURL(String str) {
try {
new URL(str);
return true;
@ -391,6 +397,37 @@ public class MsHTTPSamplerProxy extends MsTestElement {
}
}
/**
* 按照环境规则匹配环境
*
* @param httpConfig
* @return
*/
private HttpConfig getHttpConfig(HttpConfig httpConfig) {
if (CollectionUtils.isNotEmpty(httpConfig.getConditions())) {
for (HttpConfigCondition item : httpConfig.getConditions()) {
if (item.getType().equals(ConditionType.NONE.name())) {
return httpConfig.initHttpConfig(item);
} else if (item.getType().equals(ConditionType.PATH.name())) {
HttpConfig config = httpConfig.getPathCondition(this.getPath());
if (config != null) {
return config;
}
} else if (item.getType().equals(ConditionType.MODULE.name())) {
ApiDefinitionService apiDefinitionService = CommonBeanFactory.getBean(ApiDefinitionService.class);
ApiDefinition apiDefinition = apiDefinitionService.get(this.getId());
if (apiDefinition != null) {
HttpConfig config = httpConfig.getModuleCondition(apiDefinition.getModuleId());
if (config != null) {
return config;
}
}
}
}
}
return httpConfig;
}
private boolean isRest() {
return this.getRest().stream().filter(KeyValue::isEnable).filter(KeyValue::isValid).toArray().length > 0;
}

View File

@ -1,14 +1,59 @@
package io.metersphere.api.dto.scenario;
import io.metersphere.commons.constants.ConditionType;
import io.metersphere.commons.utils.BeanUtils;
import lombok.Data;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import java.beans.Beans;
import java.util.List;
import java.util.stream.Collectors;
@Data
public class HttpConfig {
private String socket;
private String domain;
private String protocol = "https";
private String defaultCondition;
private int port;
private List<HttpConfigCondition> conditions;
private List<KeyValue> headers;
public boolean isNode(String type) {
return StringUtils.equals(defaultCondition, type);
}
public HttpConfig initHttpConfig(HttpConfigCondition configCondition) {
HttpConfig config = new HttpConfig();
BeanUtils.copyBean(config, configCondition);
return config;
}
public HttpConfig getPathCondition(String path) {
List<HttpConfigCondition> conditions = this.getConditions().stream().filter(condition -> ConditionType.PATH.name().equals(condition.getType()) && CollectionUtils.isNotEmpty(condition.getDetails())).collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(conditions)) {
for (HttpConfigCondition item : conditions) {
List<KeyValue> details = item.getDetails().stream().filter(detail -> (detail.getValue().equals("contains") && StringUtils.contains(path, detail.getName())) || (detail.getValue().equals("equals") && StringUtils.equals(path, detail.getName()))).collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(details)) {
return initHttpConfig(item);
}
}
}
return null;
}
public HttpConfig getModuleCondition(String moduleId) {
List<HttpConfigCondition> conditions = this.getConditions().stream().filter(condition -> ConditionType.MODULE.name().equals(condition.getType()) && CollectionUtils.isNotEmpty(condition.getDetails())).collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(conditions)) {
for (HttpConfigCondition item : conditions) {
List<KeyValue> details = item.getDetails().stream().filter(detail -> StringUtils.contains(detail.getValue(), moduleId)).collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(details)) {
return initHttpConfig(item);
}
}
}
return null;
}
}

View File

@ -0,0 +1,15 @@
package io.metersphere.api.dto.scenario;
import lombok.Data;
import java.util.List;
@Data
public class HttpConfigCondition {
private String type;
private List<KeyValue> details;
private String protocol;
private String socket;
private String domain;
private int port;
}

View File

@ -38,6 +38,8 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl
public final static String TEST_ID = "ms.test.id";
public final static String TEST_REPORT_NAME = "ms.test.report.name";
private final static String THREAD_SPLIT = " ";
private final static String ID_SPLIT = "-";
@ -66,6 +68,8 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl
private String testId;
private String debugReportId;
// 只有合并报告是这个有值
private String reportName;
//获得控制台内容
private PrintStream oldPrintStream = System.out;
@ -132,7 +136,7 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl
TestResult testResult = new TestResult();
testResult.setTestId(testId);
testResult.setTotal(queue.size());
testResult.setReportName(this.reportName);
// 一个脚本里可能包含多个场景(ThreadGroup)所以要区分开key: 场景Id
final Map<String, ScenarioResult> scenarios = new LinkedHashMap<>();
queue.forEach(result -> {
@ -216,7 +220,7 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl
} else {
apiDefinitionExecResultService.saveApiResult(testResult, ApiRunMode.API_PLAN.name());
}
} else if (StringUtils.equalsAny(this.runMode, ApiRunMode.SCENARIO.name(), ApiRunMode.SCENARIO_PLAN.name(), ApiRunMode.SCHEDULE_SCENARIO_PLAN.name(),ApiRunMode.SCHEDULE_SCENARIO.name())) {
} else if (StringUtils.equalsAny(this.runMode, ApiRunMode.SCENARIO.name(), ApiRunMode.SCENARIO_PLAN.name(), ApiRunMode.SCHEDULE_SCENARIO_PLAN.name(), ApiRunMode.SCHEDULE_SCENARIO.name())) {
// 执行报告不需要存储由用户确认后在存储
testResult.setTestId(testId);
ApiScenarioReport scenarioReport = apiScenarioReportService.complete(testResult, this.runMode);
@ -426,6 +430,7 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl
private void setParam(BackendListenerContext context) {
this.testId = context.getParameter(TEST_ID);
this.reportName = context.getParameter(TEST_REPORT_NAME);
this.runMode = context.getParameter("runMode");
this.debugReportId = context.getParameter("debugReportId");
if (StringUtils.isBlank(this.runMode)) {

View File

@ -1,30 +1,64 @@
package io.metersphere.api.jmeter;
import com.alibaba.fastjson.JSON;
import io.metersphere.api.dto.RunRequest;
import io.metersphere.api.dto.automation.RunModeConfig;
import io.metersphere.api.dto.definition.request.MsTestPlan;
import io.metersphere.api.dto.scenario.request.BodyFile;
import io.metersphere.base.domain.JarConfig;
import io.metersphere.base.domain.TestResource;
import io.metersphere.commons.constants.ApiRunMode;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.CommonBeanFactory;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.commons.utils.UrlTestUtils;
import io.metersphere.config.JmeterProperties;
import io.metersphere.dto.BaseSystemConfigDTO;
import io.metersphere.dto.NodeDTO;
import io.metersphere.i18n.Translator;
import io.metersphere.service.JarConfigService;
import io.metersphere.service.SystemParameterService;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.jmeter.config.Arguments;
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.jmeter.save.SaveService;
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.core.io.ByteArrayResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
@Service
public class JMeterService {
private static final String BASE_URL = "http://%s:%d";
@Resource
private JmeterProperties jmeterProperties;
@Resource
ResourcePoolCalculation resourcePoolCalculation;
@Resource
private RestTemplate restTemplate;
@PostConstruct
public void init() {
@ -88,6 +122,25 @@ public class JMeterService {
testPlan.add(testPlan.getArray()[0], backendListener);
}
private void addBackendListener(String testId, String debugReportId, String runMode, HashTree testPlan, RunModeConfig config) {
BackendListener backendListener = new BackendListener();
backendListener.setName(testId);
Arguments arguments = new Arguments();
if (config != null && config.getMode().equals("serial") && config.getReportType().equals("setReport")) {
arguments.addArgument(APIBackendListenerClient.TEST_REPORT_NAME, config.getReportName());
}
arguments.addArgument(APIBackendListenerClient.TEST_ID, testId);
if (StringUtils.isNotBlank(runMode)) {
arguments.addArgument("runMode", runMode);
}
if (StringUtils.isNotBlank(debugReportId)) {
arguments.addArgument("debugReportId", debugReportId);
}
backendListener.setArguments(arguments);
backendListener.setClassname(APIBackendListenerClient.class.getCanonicalName());
testPlan.add(testPlan.getArray()[0], backendListener);
}
public void runDefinition(String testId, HashTree testPlan, String debugReportId, String runMode) {
try {
init();
@ -99,4 +152,172 @@ public class JMeterService {
MSException.throwException(Translator.get("api_load_script_error"));
}
}
public void runSerial(String testId, HashTree testPlan, String debugReportId, String runMode, RunModeConfig config) {
try {
init();
addBackendListener(testId, debugReportId, runMode, testPlan, config);
LocalRunner runner = new LocalRunner(testPlan);
runner.run();
} catch (Exception e) {
LogUtil.error(e.getMessage(), e);
MSException.throwException(Translator.get("api_load_script_error"));
}
}
/**
* 获取当前jmx 涉及到的文件
*
* @param tree
*/
public void getFiles(HashTree tree, List<BodyFile> 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);
}
}
}
private byte[] fileToByte(File tradeFile) {
byte[] buffer = null;
try (FileInputStream fis = new FileInputStream(tradeFile);
ByteArrayOutputStream bos = new ByteArrayOutputStream();) {
byte[] b = new byte[1024];
int n;
while ((n = fis.read(b)) != -1) {
bos.write(b, 0, n);
}
buffer = bos.toByteArray();
} catch (Exception e) {
}
return buffer;
}
private List<Object> getJar() {
List<Object> jarFiles = new LinkedList<>();
// jar
JarConfigService jarConfigService = CommonBeanFactory.getBean(JarConfigService.class);
List<JarConfig> jars = jarConfigService.list();
jars.forEach(jarConfig -> {
try {
String path = jarConfig.getPath();
File file = new File(path);
if (file.isDirectory() && !path.endsWith("/")) {
file = new File(path + "/");
}
FileSystemResource resource = new FileSystemResource(file);
ByteArrayResource byteArrayResource = new ByteArrayResource(this.fileToByte(file)) {
@Override
public String getFilename() throws IllegalStateException {
return resource.getFilename();
}
};
jarFiles.add(byteArrayResource);
} catch (Exception e) {
LogUtil.error(e.getMessage(), e);
}
});
return jarFiles;
}
private List<Object> getMultipartFiles(HashTree hashTree) {
List<Object> multipartFiles = new LinkedList<>();
// 获取附件
List<BodyFile> files = new LinkedList<>();
getFiles(hashTree, files);
if (CollectionUtils.isNotEmpty(files)) {
for (BodyFile bodyFile : files) {
File file = new File(bodyFile.getName());
if (file != null && !file.exists()) {
FileSystemResource resource = new FileSystemResource(file);
ByteArrayResource byteArrayResource = new ByteArrayResource(this.fileToByte(file)) {
@Override
public String getFilename() throws IllegalStateException {
return resource.getFilename();
}
};
multipartFiles.add(byteArrayResource);
}
}
}
return multipartFiles;
}
public void runTest(String testId, HashTree hashTree, String runMode, RunModeConfig config) {
// 获取JMX使用到的附件
List<Object> multipartFiles = getMultipartFiles(hashTree);
// 获取JAR
List<Object> jarFiles = getJar();
// 获取可以执行的资源池
TestResource testResource = resourcePoolCalculation.getPool();
String configuration = testResource.getConfiguration();
NodeDTO node = JSON.parseObject(configuration, NodeDTO.class);
String nodeIp = node.getIp();
Integer port = node.getPort();
BaseSystemConfigDTO baseInfo = CommonBeanFactory.getBean(SystemParameterService.class).getBaseInfo();
// 占位符
String metersphereUrl = "http://localhost:8081";
if (baseInfo != null) {
metersphereUrl = baseInfo.getUrl();
}
// 检查地址是否正确
String jmeterPingUrl = metersphereUrl + "/jmeter/ping";
// docker 不能从 localhost 中下载文件
if (StringUtils.contains(metersphereUrl, "http://localhost")
|| !UrlTestUtils.testUrlWithTimeOut(jmeterPingUrl, 1000)) {
MSException.throwException(Translator.get("run_load_test_file_init_error"));
}
String uri = String.format(BASE_URL + "/jmeter/api/run", nodeIp, port);
try {
RunRequest runRequest = new RunRequest();
runRequest.setTestId(testId);
runRequest.setRunMode(runMode);
runRequest.setConfig(config);
runRequest.setJmx(new MsTestPlan().getJmx(hashTree));
MultiValueMap<String, Object> postParameters = new LinkedMultiValueMap<>();
postParameters.put("files", multipartFiles);
postParameters.put("jarFiles", jarFiles);
postParameters.add("request", JSON.toJSONString(runRequest));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN));
HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(postParameters, headers);
String result = restTemplate.postForObject(uri, request, String.class);
if (result == null) {
MSException.throwException(Translator.get("start_engine_fail"));
}
} catch (MSException e) {
throw e;
} catch (Exception e) {
e.printStackTrace();
MSException.throwException("Please check node-controller status.");
}
}
}

View File

@ -2,6 +2,7 @@ package io.metersphere.api.jmeter;
import com.alibaba.fastjson.JSON;
import io.github.ningyu.jmeter.plugin.dubbo.sample.DubboSample;
import org.apache.commons.collections.CollectionUtils;
import org.apache.jmeter.extractor.JSR223PostProcessor;
import org.apache.jmeter.extractor.RegexExtractor;
import org.apache.jmeter.extractor.XPath2Extractor;
@ -31,32 +32,36 @@ public class JMeterVars {
* @param vars
* @param extract
*/
public static void addVars(Integer testId, JMeterVariables vars, String extract) {
JMeterVariables vs = new JMeterVariables();
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<String> extracts = Arrays.asList(extract.split(";"));
Optional.ofNullable(extracts).orElse(new ArrayList<>()).forEach(item -> {
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<Object> data = new ArrayList<>();
for (int i = 1; i < nrv + 1; i++) {
data.add(vars.get(item + "_" + i));
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<Object> 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);
}
String array = JSON.toJSONString(data);
vars.put(item, array);
}
vs.put(item, vars.get(item) == null ? "" : vars.get(item));
}
vs.put(item, vars.get(item) == null ? "" : vars.get(item));
});
vs.remove("TESTSTART.MS"); // 标示变量移除
vs.remove("TESTSTART.MS"); // 标示变量移除
}
}
variables.put(testId, vs);

View File

@ -0,0 +1,213 @@
package io.metersphere.api.jmeter;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.metersphere.api.service.*;
import io.metersphere.base.domain.ApiDefinitionExecResult;
import io.metersphere.base.domain.ApiScenarioReport;
import io.metersphere.base.domain.ApiTestReport;
import io.metersphere.commons.constants.*;
import io.metersphere.commons.utils.CommonBeanFactory;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.dto.BaseSystemConfigDTO;
import io.metersphere.i18n.Translator;
import io.metersphere.notice.sender.NoticeModel;
import io.metersphere.notice.service.NoticeSendService;
import io.metersphere.service.SystemParameterService;
import io.metersphere.track.service.TestPlanReportService;
import io.metersphere.track.service.TestPlanTestCaseService;
import org.apache.commons.lang3.StringUtils;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class MsKafkaListener {
public static final String TOPICS = "ms-api-exec-topic";
@KafkaListener(topics = TOPICS)
public void consume(ConsumerRecord<?, String> record) {
LogUtil.info("接收到执行结果开始存储");
this.save(record.value());
LogUtil.info("执行内容存储结束");
}
@Resource
private APITestService apiTestService;
@Resource
private APIReportService apiReportService;
@Resource
private ApiDefinitionService apiDefinitionService;
@Resource
private ApiDefinitionExecResultService apiDefinitionExecResultService;
@Resource
private TestPlanReportService testPlanReportService;
@Resource
private ApiScenarioReportService apiScenarioReportService;
private TestResult formatResult(String result) {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
try {
// 多态JSON普通转换会丢失内容需要通过 ObjectMapper 获取
if (StringUtils.isNotEmpty(result)) {
TestResult element = mapper.readValue(result, new TypeReference<TestResult>() {
});
return element;
}
} catch (Exception e) {
e.printStackTrace();
LogUtil.error(e.getMessage());
}
return null;
}
private void save(String execResult) {
TestResult testResult = this.formatResult(execResult);
ApiTestReport report = null;
String reportUrl = null;
// 这部分后续优化只留 DEFINITION SCENARIO 两部分
if (StringUtils.equals(testResult.getRunMode(), ApiRunMode.DEFINITION.name())) {
// 调试操作不需要存储结果
apiDefinitionService.addResult(testResult);
if (!testResult.isDebug()) {
apiDefinitionExecResultService.saveApiResult(testResult, ApiRunMode.DEFINITION.name());
}
} else if (StringUtils.equals(testResult.getRunMode(), ApiRunMode.JENKINS.name())) {
apiDefinitionService.addResult(testResult);
apiDefinitionExecResultService.saveApiResult(testResult, ApiRunMode.DEFINITION.name());
} else if (StringUtils.equals(testResult.getRunMode(), ApiRunMode.JENKINS_API_PLAN.name())) {
apiDefinitionService.addResult(testResult);
apiDefinitionExecResultService.saveApiResult(testResult, ApiRunMode.API_PLAN.name());
ApiDefinitionExecResult result = apiDefinitionService.getResultByJenkins(testResult.getTestId(), ApiRunMode.API_PLAN.name());
if (result != null) {
report = new ApiTestReport();
report.setStatus(result.getStatus());
report.setId(result.getId());
report.setTriggerMode(ApiRunMode.API.name());
report.setName(apiDefinitionService.getApiCaseInfo(testResult.getTestId()).getName());
}
} else if (StringUtils.equalsAny(testResult.getRunMode(), ApiRunMode.API_PLAN.name(), ApiRunMode.SCHEDULE_API_PLAN.name())) {
apiDefinitionService.addResult(testResult);
//测试计划定时任务-接口执行逻辑的话需要同步测试计划的报告数据
if (StringUtils.equalsAny(testResult.getRunMode(), ApiRunMode.SCHEDULE_API_PLAN.name(), ApiRunMode.JENKINS_API_PLAN.name())) {
apiDefinitionExecResultService.saveApiResultByScheduleTask(testResult, ApiRunMode.SCHEDULE_API_PLAN.name());
List<String> testPlanReportIdList = new ArrayList<>();
testPlanReportIdList.add(testResult.getTestId());
// 更新每个测试计划的状态
for (String testPlanReportId : testPlanReportIdList) {
testPlanReportService.checkTestPlanStatus(testPlanReportId);
}
testPlanReportService.updateReport(testPlanReportIdList, ApiRunMode.SCHEDULE_API_PLAN.name(), ReportTriggerMode.SCHEDULE.name());
} else {
apiDefinitionExecResultService.saveApiResult(testResult, ApiRunMode.API_PLAN.name());
}
} else if (StringUtils.equalsAny(testResult.getRunMode(), ApiRunMode.SCENARIO.name(), ApiRunMode.SCENARIO_PLAN.name(), ApiRunMode.SCHEDULE_SCENARIO_PLAN.name(), ApiRunMode.SCHEDULE_SCENARIO.name())) {
// 执行报告不需要存储由用户确认后在存储
testResult.setTestId(testResult.getTestId());
ApiScenarioReport scenarioReport = apiScenarioReportService.complete(testResult, testResult.getRunMode());
report = new ApiTestReport();
report.setStatus(scenarioReport.getStatus());
report.setId(scenarioReport.getId());
report.setTriggerMode(scenarioReport.getTriggerMode());
report.setName(scenarioReport.getName());
SystemParameterService systemParameterService = CommonBeanFactory.getBean(SystemParameterService.class);
assert systemParameterService != null;
BaseSystemConfigDTO baseSystemConfigDTO = systemParameterService.getBaseInfo();
reportUrl = baseSystemConfigDTO.getUrl() + "/#/api/automation/report";
testResult.setTestId(scenarioReport.getScenarioId());
} else {
apiTestService.changeStatus(testResult.getTestId(), APITestStatus.Completed);
report = apiReportService.getRunningReport(testResult.getTestId());
apiReportService.complete(testResult, report);
}
TestPlanTestCaseService testPlanTestCaseService = CommonBeanFactory.getBean(TestPlanTestCaseService.class);
List<String> ids = testPlanTestCaseService.getTestPlanTestCaseIds(testResult.getTestId());
if (ids.size() > 0) {
try {
if (StringUtils.equals(APITestStatus.Success.name(), report.getStatus())) {
testPlanTestCaseService.updateTestCaseStates(ids, TestPlanTestCaseStatus.Pass.name());
} else {
testPlanTestCaseService.updateTestCaseStates(ids, TestPlanTestCaseStatus.Failure.name());
}
} catch (Exception e) {
}
}
if (report != null && StringUtils.equals(ReportTriggerMode.API.name(), report.getTriggerMode()) || StringUtils.equals(ReportTriggerMode.SCHEDULE.name(), report.getTriggerMode())) {
sendTask(report, reportUrl, testResult);
}
}
private static void sendTask(ApiTestReport report, String reportUrl, TestResult testResult) {
if (report == null) {
return;
}
SystemParameterService systemParameterService = CommonBeanFactory.getBean(SystemParameterService.class);
NoticeSendService noticeSendService = CommonBeanFactory.getBean(NoticeSendService.class);
assert systemParameterService != null;
assert noticeSendService != null;
BaseSystemConfigDTO baseSystemConfigDTO = systemParameterService.getBaseInfo();
String url = baseSystemConfigDTO.getUrl() + "/#/api/report/view/" + report.getId();
String url2 = baseSystemConfigDTO.getUrl() + "/#/api/automation/report/view/" + report.getId();
String successContext = "";
String failedContext = "";
String subject = "";
String event = "";
if (StringUtils.equals(ReportTriggerMode.API.name(), report.getTriggerMode())) {
successContext = "接口测试 API任务通知:'" + report.getName() + "'执行成功" + "\n" + "【接口定义暂无报告链接】" + "\n" + "请点击下面链接进入测试报告页面" + "\n" + "(旧版)接口测)路径" + url + "\n" + "(新版)接口测试路径" + url2;
failedContext = "接口测试 API任务通知:'" + report.getName() + "'执行失败" + "\n" + "【接口定义暂无报告链接】" + "\n" + "请点击下面链接进入测试报告页面" + "\n" + "(旧版)接口测试路径" + url + "\n" + "(新版)接口测试路径" + url2;
subject = Translator.get("task_notification_jenkins");
}
if (StringUtils.equals(ReportTriggerMode.SCHEDULE.name(), report.getTriggerMode())) {
successContext = "接口测试定时任务通知:'" + report.getName() + "'执行成功" + "\n" + "【接口定义暂无报告链接】" + "\n" + "请点击下面链接进入测试报告页面" + "\n" + "(旧版)接口测试路径" + url + "\n" + "(新版)接口测试路径" + url2;
failedContext = "接口测试定时任务通知:'" + report.getName() + "'执行失败" + "\n" + "【接口定义暂无报告链接】" + "\n" + "请点击下面链接进入测试报告页面" + "\n" + "(旧版)接口测试路径" + url + "\n" + "(新版)接口测试路径" + url2;
subject = Translator.get("task_notification");
}
if (StringUtils.equals("Success", report.getStatus())) {
event = NoticeConstants.Event.EXECUTE_SUCCESSFUL;
}
if (StringUtils.equals("success", report.getStatus())) {
event = NoticeConstants.Event.EXECUTE_SUCCESSFUL;
}
if (StringUtils.equals("Error", report.getStatus())) {
event = NoticeConstants.Event.EXECUTE_FAILED;
}
if (StringUtils.equals("error", report.getStatus())) {
event = NoticeConstants.Event.EXECUTE_FAILED;
}
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("testName", report.getName());
paramMap.put("id", report.getId());
paramMap.put("type", "api");
paramMap.put("url", baseSystemConfigDTO.getUrl());
paramMap.put("status", report.getStatus());
NoticeModel noticeModel = NoticeModel.builder()
.successContext(successContext)
.successMailTemplate("ApiSuccessfulNotification")
.failedContext(failedContext)
.failedMailTemplate("ApiFailedNotification")
.testId(testResult.getTestId())
.status(report.getStatus())
.event(event)
.subject(subject)
.paramMap(paramMap)
.build();
noticeSendService.send(report.getTriggerMode(), noticeModel);
}
}

View File

@ -0,0 +1,45 @@
package io.metersphere.api.jmeter;
import io.metersphere.base.domain.TestResource;
import io.metersphere.base.domain.TestResourceExample;
import io.metersphere.base.domain.TestResourcePool;
import io.metersphere.base.domain.TestResourcePoolExample;
import io.metersphere.base.mapper.TestResourceMapper;
import io.metersphere.base.mapper.TestResourcePoolMapper;
import io.metersphere.commons.exception.MSException;
import io.metersphere.i18n.Translator;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class ResourcePoolCalculation {
@Resource
TestResourcePoolMapper testResourcePoolMapper;
@Resource
TestResourceMapper testResourceMapper;
public TestResource getPool() {
// 获取可以执行的资源池
TestResourcePoolExample example = new TestResourcePoolExample();
example.createCriteria().andStatusEqualTo("VALID").andTypeEqualTo("NODE").andNameEqualTo("赵勇资源池");
List<TestResourcePool> pools = testResourcePoolMapper.selectByExample(example);
// 暂时随机获取一个正常状态NODE
TestResource testResource = null;
if (CollectionUtils.isNotEmpty(pools)) {
List<String> poolIds = pools.stream().map(pool -> pool.getId()).collect(Collectors.toList());
TestResourceExample resourceExample = new TestResourceExample();
resourceExample.createCriteria().andTestResourcePoolIdIn(poolIds);
List<TestResource> testResources = testResourceMapper.selectByExampleWithBLOBs(resourceExample);
int index = (int) (Math.random() * testResources.size());
testResource = testResources.get(index);
}
if (testResource == null) {
MSException.throwException(Translator.get("run_load_test_file_init_error"));
}
return testResource;
}
}

View File

@ -1,17 +1,23 @@
package io.metersphere.api.jmeter;
import io.metersphere.commons.utils.BeanUtils;
import lombok.Data;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
@Data
public class TestResult {
private String testId;
private String reportName;
private boolean isDebug;
private String runMode;
private int success = 0;
private int error = 0;

View File

@ -387,13 +387,14 @@ public class ApiAutomationService {
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
try {
JSONObject element = JSON.parseObject(definition);
List<MsTestElement> hashTree = mapper.readValue(element.getString("hashTree"), new TypeReference<LinkedList<MsTestElement>>(){});
List<MsTestElement> hashTree = mapper.readValue(element.getString("hashTree"), new TypeReference<LinkedList<MsTestElement>>() {
});
for (int i = 0; i < hashTree.size(); i++) {
MsTestElement tr = hashTree.get(i);
String referenced = tr.getReferenced();
if (StringUtils.equals(MsTestElementConstants.REF.name(), referenced)) {
if (StringUtils.equals(tr.getType(), "HTTPSamplerProxy")) {
MsHTTPSamplerProxy http = (MsHTTPSamplerProxy)tr;
MsHTTPSamplerProxy http = (MsHTTPSamplerProxy) tr;
String refType = tr.getRefType();
if (StringUtils.equals(refType, "CASE")) {
http.setUrl(null);
@ -421,14 +422,15 @@ public class ApiAutomationService {
env.getProjectIds().add(apiScenario.getProjectId());
String scenarioDefinition = apiScenario.getScenarioDefinition();
JSONObject element1 = JSON.parseObject(scenarioDefinition);
LinkedList<MsTestElement> hashTree1 = mapper.readValue(element1.getString("hashTree"), new TypeReference<LinkedList<MsTestElement>>(){});
LinkedList<MsTestElement> hashTree1 = mapper.readValue(element1.getString("hashTree"), new TypeReference<LinkedList<MsTestElement>>() {
});
tr.setHashTree(hashTree1);
}
}
} else {
if (StringUtils.equals(tr.getType(), "HTTPSamplerProxy")) {
// 校验是否是全路径
MsHTTPSamplerProxy httpSamplerProxy = (MsHTTPSamplerProxy)tr;
MsHTTPSamplerProxy httpSamplerProxy = (MsHTTPSamplerProxy) tr;
if (httpSamplerProxy.isEnable()) {
if (StringUtils.isBlank(httpSamplerProxy.getUrl()) || !isURL(httpSamplerProxy.getUrl())) {
env.getProjectIds().add(httpSamplerProxy.getProjectId());
@ -464,7 +466,7 @@ public class ApiAutomationService {
String referenced = tr.getReferenced();
if (StringUtils.equals(MsTestElementConstants.REF.name(), referenced)) {
if (StringUtils.equals(tr.getType(), "HTTPSamplerProxy")) {
MsHTTPSamplerProxy http = (MsHTTPSamplerProxy)tr;
MsHTTPSamplerProxy http = (MsHTTPSamplerProxy) tr;
String refType = tr.getRefType();
if (StringUtils.equals(refType, "CASE")) {
http.setUrl(null);
@ -486,20 +488,21 @@ public class ApiAutomationService {
ApiDefinition apiDefinition = apiDefinitionService.get(tr.getId());
env.getProjectIds().add(apiDefinition.getProjectId());
}
} else if (StringUtils.equals(tr.getType(), "scenario")) {
} else if (StringUtils.equals(tr.getType(), "scenario")) {
if (tr.isEnable()) {
ApiScenarioWithBLOBs apiScenario = getApiScenario(tr.getId());
env.getProjectIds().add(apiScenario.getProjectId());
String scenarioDefinition = apiScenario.getScenarioDefinition();
JSONObject element1 = JSON.parseObject(scenarioDefinition);
LinkedList<MsTestElement> hashTree1 = mapper.readValue(element1.getString("hashTree"), new TypeReference<LinkedList<MsTestElement>>(){});
LinkedList<MsTestElement> hashTree1 = mapper.readValue(element1.getString("hashTree"), new TypeReference<LinkedList<MsTestElement>>() {
});
tr.setHashTree(hashTree1);
}
}
} else {
if (StringUtils.equals(tr.getType(), "HTTPSamplerProxy")) {
// 校验是否是全路径
MsHTTPSamplerProxy httpSamplerProxy = (MsHTTPSamplerProxy)tr;
MsHTTPSamplerProxy httpSamplerProxy = (MsHTTPSamplerProxy) tr;
if (httpSamplerProxy.isEnable()) {
if (StringUtils.isBlank(httpSamplerProxy.getUrl()) || !isURL(httpSamplerProxy.getUrl())) {
env.setFullUrl(false);
@ -549,7 +552,7 @@ public class ApiAutomationService {
}
public byte[] loadFileAsBytes(FileOperationRequest fileOperationRequest) {
File file = new File(FileUtils.BODY_FILE_DIR + fileOperationRequest.getId() + "_" + fileOperationRequest.getName());
File file = new File(FileUtils.BODY_FILE_DIR + "/" + fileOperationRequest.getId() + "_" + fileOperationRequest.getName());
try (FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream(1000);) {
byte[] b = new byte[1000];
@ -589,6 +592,7 @@ public class ApiAutomationService {
report.setProjectId(projectId);
report.setScenarioName(scenarioName);
report.setScenarioId(scenarioId);
apiScenarioReportMapper.insert(report);
return report;
}
@ -684,12 +688,13 @@ public class ApiAutomationService {
}
/**
* 场景测试执行
* 场景测试并行执行
* 这种方法性能有问题 2021/04/12
*
* @param request
* @return
*/
public String run(RunScenarioRequest request) {
public String abandonedRun(RunScenarioRequest request) {
ServiceUtils.getSelectAllIds(request, request.getCondition(),
(query) -> extApiScenarioMapper.selectIdsByQuery((ApiScenarioRequest) query));
@ -753,6 +758,123 @@ public class ApiAutomationService {
return request.getId();
}
/**
* 生成HashTree
*
* @param apiScenarios 场景
* @param request 请求参数
* @param reportIds 报告ID
* @return hashTree
*/
private HashTree generateHashTree(List<ApiScenarioWithBLOBs> apiScenarios, RunScenarioRequest request, List<String> reportIds) {
HashTree jmeterHashTree = new ListedHashTree();
MsTestPlan testPlan = new MsTestPlan();
testPlan.setSerializeThreadgroups(request.getConfig() != null && request.getConfig().getMode().equals("serial"));
testPlan.setHashTree(new LinkedList<>());
try {
boolean isFirst = true;
for (ApiScenarioWithBLOBs item : apiScenarios) {
if (item.getStepTotal() == null || item.getStepTotal() == 0) {
// 只有一个场景且没有测试步骤则提示
if (apiScenarios.size() == 1) {
MSException.throwException((item.getName() + "" + Translator.get("automation_exec_info")));
}
LogUtil.warn(item.getName() + "" + Translator.get("automation_exec_info"));
continue;
}
MsThreadGroup group = new MsThreadGroup();
group.setLabel(item.getName());
group.setName(UUID.randomUUID().toString());
if (request.getConfig() != null) {
group.setOnSampleError(request.getConfig().isOnSampleError());
}
// 批量执行的结果直接存储为报告
if (isFirst) {
group.setName(request.getId());
isFirst = false;
}
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
JSONObject element = JSON.parseObject(item.getScenarioDefinition());
MsScenario scenario = JSONObject.parseObject(item.getScenarioDefinition(), MsScenario.class);
// 多态JSON普通转换会丢失内容需要通过 ObjectMapper 获取
if (element != null && StringUtils.isNotEmpty(element.getString("hashTree"))) {
LinkedList<MsTestElement> elements = mapper.readValue(element.getString("hashTree"),
new TypeReference<LinkedList<MsTestElement>>() {
});
scenario.setHashTree(elements);
}
if (StringUtils.isNotEmpty(element.getString("variables"))) {
LinkedList<ScenarioVariable> variables = mapper.readValue(element.getString("variables"),
new TypeReference<LinkedList<ScenarioVariable>>() {
});
scenario.setVariables(variables);
}
group.setEnableCookieShare(scenario.isEnableCookieShare());
LinkedList<MsTestElement> scenarios = new LinkedList<>();
scenarios.add(scenario);
// 创建场景报告
if (reportIds != null) {
//如果是测试计划页面触发的执行方式生成报告时createScenarioReport第二个参数需要特殊处理
if (StringUtils.equals(request.getRunMode(), ApiRunMode.SCENARIO_PLAN.name())) {
String testPlanScenarioId = item.getId();
if (request.getScenarioTestPlanIdMap() != null && request.getScenarioTestPlanIdMap().containsKey(item.getId())) {
testPlanScenarioId = request.getScenarioTestPlanIdMap().get(item.getId());
// 获取场景用例单独的执行环境
TestPlanApiScenario planApiScenario = testPlanApiScenarioMapper.selectByPrimaryKey(testPlanScenarioId);
String environment = planApiScenario.getEnvironment();
if (StringUtils.isNotBlank(environment)) {
scenario.setEnvironmentMap(JSON.parseObject(environment, Map.class));
}
}
createScenarioReport(group.getName(), testPlanScenarioId, item.getName(), request.getTriggerMode() == null ? ReportTriggerMode.MANUAL.name() : request.getTriggerMode(),
request.getExecuteType(), item.getProjectId(), request.getReportUserID());
} else {
createScenarioReport(group.getName(), item.getId(), item.getName(), request.getTriggerMode() == null ? ReportTriggerMode.MANUAL.name() : request.getTriggerMode(),
request.getExecuteType(), item.getProjectId(), request.getReportUserID());
}
reportIds.add(group.getName());
}
group.setHashTree(scenarios);
testPlan.getHashTree().add(group);
}
} catch (Exception ex) {
MSException.throwException(ex.getMessage());
}
testPlan.toHashTree(jmeterHashTree, testPlan.getHashTree(), new ParameterConfig());
return jmeterHashTree;
}
/**
* 场景串行
*
* @param request
* @return
*/
public String run(RunScenarioRequest request) {
ServiceUtils.getSelectAllIds(request, request.getCondition(),
(query) -> extApiScenarioMapper.selectIdsByQuery((ApiScenarioRequest) query));
List<String> ids = request.getIds();
//检查是否有正在执行中的情景
this.checkScenarioIsRunning(ids);
List<ApiScenarioWithBLOBs> apiScenarios = extApiScenarioMapper.selectIds(ids);
String runMode = ApiRunMode.SCENARIO.name();
if (StringUtils.isNotBlank(request.getRunMode()) && StringUtils.equals(request.getRunMode(), ApiRunMode.SCENARIO_PLAN.name())) {
runMode = ApiRunMode.SCENARIO_PLAN.name();
}
if (StringUtils.isNotBlank(request.getRunMode()) && StringUtils.equals(request.getRunMode(), ApiRunMode.DEFINITION.name())) {
runMode = ApiRunMode.DEFINITION.name();
}
// 调用执行方法
List<String> reportIds = new LinkedList<>();
HashTree hashTree = generateHashTree(apiScenarios, request, reportIds);
jMeterService.runSerial(JSON.toJSONString(reportIds), hashTree, request.getReportId(), runMode, request.getConfig());
return request.getId();
}
public void checkScenarioIsRunning(List<String> ids) {
List<ApiScenarioReport> lastReportStatusByIds = apiReportService.selectLastReportByIds(ids);
for (ApiScenarioReport report : lastReportStatusByIds) {
@ -813,9 +935,10 @@ public class ApiAutomationService {
MSException.throwException(e.getMessage());
}
// 调用执行方法
APIScenarioReportResult reportResult = createScenarioReport(request.getId(), request.getScenarioId(), request.getScenarioName(), ReportTriggerMode.MANUAL.name(), request.getExecuteType(), request.getProjectId(),
createScenarioReport(request.getId(), request.getScenarioId(), request.getScenarioName(), ReportTriggerMode.MANUAL.name(), request.getExecuteType(), request.getProjectId(),
SessionUtils.getUserId());
apiScenarioReportMapper.insert(reportResult);
// 调用执行方法
// jMeterService.runTest(request.getId(), hashTree, ApiRunMode.SCENARIO.name(), null);
// 调用执行方法
jMeterService.runDefinition(request.getId(), hashTree, request.getReportId(), ApiRunMode.SCENARIO.name());
return request.getId();
@ -994,9 +1117,9 @@ public class ApiAutomationService {
if (StringUtils.equals(request.getGroup(), ScheduleGroup.TEST_PLAN_TEST.name())) {
scheduleService.addOrUpdateCronJob(
request, TestPlanTestJob.getJobKey(request.getResourceId()), TestPlanTestJob.getTriggerKey(request.getResourceId()), TestPlanTestJob.class);
}else if(StringUtils.equals(request.getGroup(), ScheduleGroup.SWAGGER_IMPORT.name())){
} else if (StringUtils.equals(request.getGroup(), ScheduleGroup.SWAGGER_IMPORT.name())) {
scheduleService.addOrUpdateCronJob(request, SwaggerUrlImportJob.getJobKey(request.getResourceId()), SwaggerUrlImportJob.getTriggerKey(request.getResourceId()), SwaggerUrlImportJob.class);
} else{
} else {
scheduleService.addOrUpdateCronJob(
request, ApiScenarioTestJob.getJobKey(request.getResourceId()), ApiScenarioTestJob.getTriggerKey(request.getResourceId()), ApiScenarioTestJob.class);
}
@ -1248,7 +1371,7 @@ public class ApiAutomationService {
*/
Map<String, List<String>> urlMap = new HashMap<>();
List<String> allApiIdList = new ArrayList<>();
Map<String,List<String>> caseIdMap = new HashMap<>();
Map<String, List<String>> caseIdMap = new HashMap<>();
for (ApiDefinition model : allEffectiveApiList) {
String url = model.getPath();
String id = model.getId();
@ -1261,7 +1384,7 @@ public class ApiAutomationService {
urlMap.put(url, list);
}
}
for (ApiTestCase model : allEffectiveApiCaseList){
for (ApiTestCase model : allEffectiveApiCaseList) {
String caseId = model.getId();
String apiId = model.getApiDefinitionId();
if (urlMap.containsKey(caseId)) {
@ -1278,20 +1401,20 @@ public class ApiAutomationService {
}
List<String> urlList = new ArrayList<>();
List<String> idList= new ArrayList<>();
List<String> idList = new ArrayList<>();
for (ApiScenarioWithBLOBs model : allScenarioInfoList) {
String scenarioDefiniton = model.getScenarioDefinition();
this.addUrlAndIdToList(scenarioDefiniton,urlList,idList);
this.addUrlAndIdToList(scenarioDefiniton, urlList, idList);
}
List<String> containsApiIdList = new ArrayList<>();
for (String url : urlList) {
List<String> apiIdList = urlMap.get(url);
if(apiIdList!=null ){
if (apiIdList != null) {
for (String api : apiIdList) {
if(!containsApiIdList.contains(api)){
if (!containsApiIdList.contains(api)) {
containsApiIdList.add(api);
}
}
@ -1300,16 +1423,16 @@ public class ApiAutomationService {
for (String id : idList) {
List<String> apiIdList = caseIdMap.get(id);
if(apiIdList!=null ){
if (apiIdList != null) {
for (String api : apiIdList) {
if(!containsApiIdList.contains(api)){
if (!containsApiIdList.contains(api)) {
containsApiIdList.add(api);
}
}
}
if(allApiIdList.contains(id)){
if(!containsApiIdList.contains(id)){
if (allApiIdList.contains(id)) {
if (!containsApiIdList.contains(id)) {
containsApiIdList.add(id);
}
}
@ -1322,25 +1445,25 @@ public class ApiAutomationService {
private void addUrlAndIdToList(String scenarioDefiniton, List<String> urlList, List<String> idList) {
try {
JSONObject scenarioObj = JSONObject.parseObject(scenarioDefiniton);
if(scenarioObj.containsKey("hashTree")){
if (scenarioObj.containsKey("hashTree")) {
JSONArray hashArr = scenarioObj.getJSONArray("hashTree");
for (int i = 0; i < hashArr.size(); i++) {
JSONObject elementObj = hashArr.getJSONObject(i);
if(elementObj.containsKey("id")){
if (elementObj.containsKey("id")) {
String id = elementObj.getString("id");
idList.add(id);
}
if(elementObj.containsKey("url")){
if (elementObj.containsKey("url")) {
String url = elementObj.getString("url");
urlList.add(url);
}
if(elementObj.containsKey("path")){
if (elementObj.containsKey("path")) {
String path = elementObj.getString("path");
urlList.add(path);
}
}
}
}catch (Exception e){
} catch (Exception e) {
}
}

View File

@ -530,7 +530,6 @@ public class ApiDefinitionService {
}
HashTree hashTree = request.getTestElement().generateHashTree(config);
String runMode = ApiRunMode.DEFINITION.name();
if (StringUtils.isNotBlank(request.getType()) && StringUtils.equals(request.getType(), ApiRunMode.API_PLAN.name())) {
runMode = ApiRunMode.API_PLAN.name();

View File

@ -16,6 +16,7 @@ import io.metersphere.base.mapper.ApiScenarioReportMapper;
import io.metersphere.base.mapper.TestPlanApiScenarioMapper;
import io.metersphere.base.mapper.ext.ExtApiScenarioReportMapper;
import io.metersphere.base.mapper.ext.ExtTestPlanScenarioCaseMapper;
import io.metersphere.commons.constants.APITestStatus;
import io.metersphere.commons.constants.ApiRunMode;
import io.metersphere.commons.constants.ReportTriggerMode;
import io.metersphere.commons.exception.MSException;
@ -33,10 +34,7 @@ import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;
@Service
@ -59,12 +57,12 @@ public class ApiScenarioReportService {
// 更新场景
if (result != null) {
if (StringUtils.equals(runMode, ApiRunMode.SCENARIO_PLAN.name())) {
return updatePlanCase(result);
return updatePlanCase(result,runMode);
} else if (StringUtils.equals(runMode, ApiRunMode.SCHEDULE_SCENARIO_PLAN.name())) {
return updateSchedulePlanCase(result);
return updateSchedulePlanCase(result,runMode);
} else {
updateScenarioStatus(result.getTestId());
return updateScenario(result);
return updateScenario(result, runMode);
}
}
return null;
@ -92,6 +90,30 @@ public class ApiScenarioReportService {
}
}
public APIScenarioReportResult createScenarioReport(String scenarioIds, String reportName, String status, String scenarioNames, String triggerMode, String projectId, String userID) {
APIScenarioReportResult report = new APIScenarioReportResult();
if (triggerMode.equals(ApiRunMode.SCENARIO.name()) || triggerMode.equals(ApiRunMode.DEFINITION.name())) {
triggerMode = ReportTriggerMode.MANUAL.name();
}
report.setId(UUID.randomUUID().toString());
report.setName(reportName);
report.setCreateTime(System.currentTimeMillis());
report.setUpdateTime(System.currentTimeMillis());
report.setStatus(status);
if (StringUtils.isNotEmpty(userID)) {
report.setUserId(userID);
} else {
report.setUserId(SessionUtils.getUserId());
}
report.setTriggerMode(triggerMode);
report.setExecuteType(ExecuteType.Saved.name());
report.setProjectId(projectId);
report.setScenarioName(scenarioNames);
report.setScenarioId(scenarioIds);
apiScenarioReportMapper.insert(report);
return report;
}
public ApiScenarioReport editReport(ScenarioResult test) {
ApiScenarioReport report = apiScenarioReportMapper.selectByPrimaryKey(test.getName());
report.setId(report.getId());
@ -122,6 +144,17 @@ public class ApiScenarioReportService {
return report;
}
private TestResult createTestResult(TestResult result) {
TestResult testResult = new TestResult();
testResult.setTestId(result.getTestId());
testResult.setTotal(result.getTotal());
testResult.setError(result.getError());
testResult.setPassAssertions(result.getPassAssertions());
testResult.setSuccess(result.getSuccess());
testResult.setTotalAssertions(result.getTotalAssertions());
return testResult;
}
private TestResult createTestResult(String testId, ScenarioResult scenarioResult) {
TestResult testResult = new TestResult();
testResult.setTestId(testId);
@ -133,10 +166,15 @@ public class ApiScenarioReportService {
return testResult;
}
public ApiScenarioReport updatePlanCase(TestResult result) {
public ApiScenarioReport updatePlanCase(TestResult result,String runMode) {
// TestPlanApiScenario testPlanApiScenario = testPlanApiScenarioMapper.selectByPrimaryKey(result.getTestId());
List<ScenarioResult> scenarioResultList = result.getScenarios();
ApiScenarioReport returnReport = null;
StringBuilder scenarioIds = new StringBuilder();
StringBuilder scenarioNames = new StringBuilder();
String projectId = null;
String userId = null;
TestResult fullResult = createTestResult(result);
for (ScenarioResult scenarioResult :
scenarioResultList) {
ApiScenarioReport report = editReport(scenarioResult);
@ -151,6 +189,12 @@ public class ApiScenarioReportService {
detail.setProjectId(report.getProjectId());
apiScenarioReportDetailMapper.insert(detail);
fullResult.addScenario(scenarioResult);
projectId = report.getProjectId();
userId = report.getUserId();
scenarioIds.append(scenarioResult.getName()).append(",");
scenarioNames.append(report.getName()).append(",");
TestPlanApiScenario testPlanApiScenario = testPlanApiScenarioMapper.selectByPrimaryKey(report.getScenarioId());
if (testPlanApiScenario != null) {
report.setScenarioId(testPlanApiScenario.getApiScenarioId());
@ -168,15 +212,20 @@ public class ApiScenarioReportService {
}
returnReport = report;
}
margeReport(result, scenarioIds, scenarioNames, runMode, projectId, userId);
return returnReport;
}
public ApiScenarioReport updateSchedulePlanCase(TestResult result) {
public ApiScenarioReport updateSchedulePlanCase(TestResult result,String runMode) {
ApiScenarioReport lastReport = null;
List<ScenarioResult> scenarioResultList = result.getScenarios();
List<String> testPlanReportIdList = new ArrayList<>();
StringBuilder scenarioIds = new StringBuilder();
StringBuilder scenarioNames = new StringBuilder();
String projectId = null;
String userId = null;
TestResult fullResult = createTestResult(result);
for (ScenarioResult scenarioResult : scenarioResultList) {
// 存储场景报告
ApiScenarioReport report = editReport(scenarioResult);
@ -224,8 +273,16 @@ public class ApiScenarioReportService {
testPlanApiScenario.setUpdateTime(System.currentTimeMillis());
testPlanApiScenarioMapper.updateByPrimaryKeySelective(testPlanApiScenario);
fullResult.addScenario(scenarioResult);
projectId = report.getProjectId();
userId = report.getUserId();
scenarioIds.append(scenarioResult.getName()).append(",");
scenarioNames.append(report.getName()).append(",");
lastReport = report;
}
// 合并报告
margeReport(result, scenarioIds, scenarioNames, runMode, projectId, userId);
TestPlanReportService testPlanReportService = CommonBeanFactory.getBean(TestPlanReportService.class);
testPlanReportService.updateReport(testPlanReportIdList, ApiRunMode.SCHEDULE_SCENARIO_PLAN.name(), ReportTriggerMode.SCHEDULE.name());
@ -266,16 +323,38 @@ public class ApiScenarioReportService {
}
}
public ApiScenarioReport updateScenario(TestResult result) {
private void margeReport(TestResult result, StringBuilder scenarioIds, StringBuilder scenarioNames, String runMode, String projectId, String userId) {
// 合并生成一份报告
if (StringUtils.isNotEmpty(result.getReportName())) {
ApiScenarioReport report = createScenarioReport(scenarioIds.toString(), result.getReportName(), result.getError() > 0 ? "Error" : "Success", scenarioNames.toString().substring(0, scenarioNames.toString().length() - 1), runMode, projectId, userId);
ApiScenarioReportDetail detail = new ApiScenarioReportDetail();
detail.setContent(JSON.toJSONString(result).getBytes(StandardCharsets.UTF_8));
detail.setReportId(report.getId());
detail.setProjectId(report.getProjectId());
apiScenarioReportDetailMapper.insert(detail);
}
}
public ApiScenarioReport updateScenario(TestResult result, String runMode) {
ApiScenarioReport lastReport = null;
StringBuilder scenarioIds = new StringBuilder();
StringBuilder scenarioNames = new StringBuilder();
String projectId = null;
String userId = null;
TestResult fullResult = createTestResult(result);
for (ScenarioResult item : result.getScenarios()) {
// 更新报告状态
ApiScenarioReport report = editReport(item);
// 报告详情内容
ApiScenarioReportDetail detail = new ApiScenarioReportDetail();
TestResult newResult = createTestResult(result.getTestId(), item);
item.setName(report.getScenarioName());
newResult.addScenario(item);
fullResult.addScenario(item);
projectId = report.getProjectId();
userId = report.getUserId();
scenarioIds.append(item.getName()).append(",");
scenarioNames.append(report.getName()).append(",");
// 报告详情内容
ApiScenarioReportDetail detail = new ApiScenarioReportDetail();
detail.setContent(JSON.toJSONString(newResult).getBytes(StandardCharsets.UTF_8));
detail.setReportId(report.getId());
detail.setProjectId(report.getProjectId());
@ -295,6 +374,8 @@ public class ApiScenarioReportService {
}
lastReport = report;
}
// 合并生成一份报告
margeReport(result, scenarioIds, scenarioNames, runMode, projectId, userId);
return lastReport;
}

View File

@ -100,7 +100,7 @@
<select id="list" resultMap="BaseResultMap">
SELECT r.name AS test_name,
r.name, r.description, r.id, r.project_id, r.create_time, r.update_time, r.status, r.trigger_mode,s.name as scenario_name,
r.name, r.description, r.id, r.project_id, r.create_time, r.update_time, r.status, r.trigger_mode,IfNULL(s.name,r.scenario_name) as scenario_name,
project.name AS project_name, user.name AS user_name
FROM api_scenario_report r
LEFT JOIN api_scenario s on r.scenario_id = s.id

View File

@ -0,0 +1,5 @@
package io.metersphere.commons.constants;
public enum ConditionType {
NONE, PATH, MODULE
}

View File

@ -0,0 +1,5 @@
-- api_scenario_report modify column length
ALTER TABLE api_scenario_report MODIFY COLUMN name VARCHAR(3000);
-- api_scenario_report modify column length
ALTER TABLE api_scenario_report MODIFY COLUMN scenario_id VARCHAR(3000);

View File

@ -40,7 +40,7 @@
import MsApiReportExport from "./ApiReportExport";
import MsApiReportViewHeader from "./ApiReportViewHeader";
import {RequestFactory} from "../../definition/model/ApiTestModel";
import {windowPrint} from "@/common/js/utils";
import {windowPrint,getUUID} from "@/common/js/utils";
export default {
name: "MsApiReport",
@ -102,14 +102,12 @@
formatResult(res) {
let resMap = new Map;
let array = [];
let i = 0;
if (res && res.scenarios) {
res.scenarios.forEach(item => {
if (item && item.requestResults) {
item.requestResults.forEach(req => {
resMap.set(req.id, req);
req.index = i;
i++;
req.name = item.name + "^@~@^" + req.name+ "UUID="+getUUID();
array.push(req);
})
}

View File

@ -157,7 +157,7 @@
</batch-edit>
<batch-move @refresh="search" @moveSave="moveSave" ref="testBatchMove"/>
<ms-run-mode @handleRunBatch="handleRunBatch" ref="runMode"/>
</div>
</template>
@ -180,6 +180,8 @@ import BatchEdit from "../../../track/case/components/BatchEdit";
import {API_SCENARIO_LIST, PROJECT_NAME, WORKSPACE_ID} from "../../../../../common/js/constants";
import EnvironmentSelect from "../../definition/components/environment/EnvironmentSelect";
import BatchMove from "../../../track/case/components/BatchMove";
import MsRunMode from "./common/RunMode";
import {
_filter,
_handleSelect,
@ -213,7 +215,8 @@ export default {
MsApiReportDetail,
MsScenarioExtendButtons,
MsTestPlanList,
MsTableOperatorButton
MsTableOperatorButton,
MsRunMode
},
props: {
referenced: {
@ -606,9 +609,13 @@ export default {
param.condition = this.condition;
},
handleBatchExecute() {
this.$refs.runMode.open();
},
handleRunBatch(config){
this.infoDb = false;
let url = "/api/automation/run/batch";
let run = {};
let run = {config: config};
run.id = getUUID();
this.buildBatchParam(run);
this.$post(url, run, response => {

View File

@ -126,7 +126,7 @@
<el-col :span="4">
<el-button :disabled="scenarioDefinition.length < 1" size="mini" type="primary" v-prevent-re-click @click="runDebug">{{$t('api_test.request.debug')}}</el-button>
<el-tooltip class="item" effect="dark" :content="$t('commons.refresh')" placement="right-start">
<el-button :disabled="scenarioDefinition.length < 1" size="mini" icon ="el-icon-refresh" v-prevent-re-click @click="getApiScenario"></el-button>
<el-button :disabled="scenarioDefinition.length < 1" size="mini" icon="el-icon-refresh" v-prevent-re-click @click="getApiScenario"></el-button>
</el-tooltip>
<font-awesome-icon class="alt-ico" :icon="['fa', 'expand-alt']" size="lg" @click="fullScreen"/>
</el-col>
@ -134,13 +134,19 @@
</div>
<!-- 场景步骤内容 -->
<div>
<el-button class="el-icon-files ms-open-btn ms-open-btn-left" size="mini" v-prevent-re-click @click="openExpansion">
{{$t('api_test.automation.open_expansion')}}
</el-button>
<el-button class=" el-icon-notebook-1 ms-open-btn" size="mini" @click="closeExpansion">
{{$t('api_test.automation.close_expansion')}}
</el-button>
<el-tree node-key="resourceId" :props="props" :data="scenarioDefinition" class="ms-tree"
:default-expanded-keys="expandedNode"
:expand-on-click-node="false"
highlight-current
@node-expand="nodeExpand"
@node-collapse="nodeCollapse"
:allow-drop="allowDrop" @node-drag-end="allowDrag" @node-click="nodeClick" v-if="!loading" draggable>
:allow-drop="allowDrop" @node-drag-end="allowDrag" @node-click="nodeClick" v-if="!loading" draggable ref="stepTree">
<span class="custom-tree-node father" slot-scope="{ node, data}" style="width: 96%">
<!-- 步骤组件-->
<ms-component-config :type="data.type" :scenario="data" :response="response" :currentScenario="currentScenario"
@ -206,7 +212,8 @@
<ms-drawer :visible="drawer" :size="100" @close="close" direction="right" :show-full-screen="false" :is-show-close="false" style="overflow: hidden">
<template v-slot:header>
<scenario-header :currentScenario="currentScenario" :projectEnvMap="projectEnvMap" :projectIds.sync="projectIds" :projectList="projectList" :scenarioDefinition="scenarioDefinition" :enableCookieShare="enableCookieShare"
:isFullUrl.sync="isFullUrl" @closePage="close" @unFullScreen="unFullScreen" @showAllBtn="showAllBtn" @runDebug="runDebug" @setProjectEnvMap="setProjectEnvMap" @showScenarioParameters="showScenarioParameters" @setCookieShare="setCookieShare" ref="maximizeHeader"/>
:isFullUrl.sync="isFullUrl" @closePage="close" @unFullScreen="unFullScreen" @showAllBtn="showAllBtn" @runDebug="runDebug" @setProjectEnvMap="setProjectEnvMap" @showScenarioParameters="showScenarioParameters" @setCookieShare="setCookieShare"
ref="maximizeHeader"/>
</template>
<maximize-scenario :scenario-definition="scenarioDefinition" :envMap="projectEnvMap" :moduleOptions="moduleOptions"
@ -325,7 +332,8 @@
projectList: [],
debugResult: new Map,
drawer: false,
isFullUrl: true
isFullUrl: true,
expandedStatus: false,
}
},
created() {
@ -597,6 +605,7 @@
recursiveSorting(arr, scenarioProjectId) {
for (let i in arr) {
arr[i].index = Number(i) + 1;
arr[i].active = this.expandedStatus;
if (arr[i].type === ELEMENT_TYPE.LoopController && arr[i].loopType === "LOOP_COUNT" && arr[i].hashTree && arr[i].hashTree.length > 1) {
arr[i].countController.proceed = true;
}
@ -623,6 +632,7 @@
for (let i in this.scenarioDefinition) {
//
this.scenarioDefinition[i].index = Number(i) + 1;
this.scenarioDefinition[i].active = this.expandedStatus;
//
if (this.scenarioDefinition[i].type === ELEMENT_TYPE.LoopController && this.scenarioDefinition[i].hashTree
&& this.scenarioDefinition[i].hashTree.length > 1) {
@ -1149,6 +1159,44 @@
this.getEnv(JSON.stringify(definition)).then(() => {
this.$refs.envPopover.openEnvSelect();
})
},
shrinkTreeNode() {
//
for (let i in this.scenarioDefinition) {
if (this.scenarioDefinition[i]) {
if (this.expandedStatus) {
this.expandedNode.push(this.scenarioDefinition[i].resourceId);
}
this.scenarioDefinition[i].active = this.expandedStatus;
if (this.scenarioDefinition[i].hashTree && this.scenarioDefinition[i].hashTree.length > 0) {
this.changeNodeStatus(this.scenarioDefinition[i].hashTree);
}
}
}
},
changeNodeStatus(nodes) {
for (let i in nodes) {
if (nodes[i]) {
if (this.expandedStatus) {
this.expandedNode.push(nodes[i].resourceId);
}
nodes[i].active = this.expandedStatus;
if (nodes[i].hashTree != undefined && nodes[i].hashTree.length > 0) {
this.changeNodeStatus(nodes[i].hashTree);
}
}
}
},
openExpansion() {
this.expandedNode = [];
this.expandedStatus = true;
this.shrinkTreeNode();
},
closeExpansion() {
this.expandedStatus = false;
this.expandedNode = [];
this.shrinkTreeNode();
this.reload();
}
}
}
@ -1309,4 +1357,15 @@
white-space: nowrap;
width: 200px;
}
.ms-open-btn {
margin: 5px 5px 0px;
font-size: 10px;
background-color: #F2F9EE;
color: #67C23A;
}
.ms-open-btn-left {
margin-left: 30px;
}
</style>

View File

@ -0,0 +1,77 @@
<template>
<el-dialog
destroy-on-close
:title="$t('load_test.runtime_config')"
width="350px"
:visible.sync="runModeVisible"
>
<div>
<span class="ms-mode-span">{{ $t("run_mode.title") }}</span>
<el-radio-group v-model="runConfig.mode">
<el-radio label="serial">{{ $t("run_mode.serial") }}</el-radio>
<el-radio label="parallel">{{ $t("run_mode.parallel") }}</el-radio>
</el-radio-group>
</div>
<div class="ms-mode-div" v-if="runConfig.mode === 'serial'">
<span class="ms-mode-span">{{ $t("run_mode.other_config") }}</span>
<el-radio-group v-model="runConfig.reportType">
<el-radio label="iddReport">{{ $t("run_mode.idd_report") }}</el-radio>
<el-radio label="setReport">{{ $t("run_mode.set_report") }}</el-radio>
</el-radio-group>
</div>
<div class="ms-mode-div" v-if="runConfig.reportType === 'setReport'">
<span class="ms-mode-span">{{ $t("run_mode.report_name") }}</span>
<el-input
v-model="runConfig.reportName"
:placeholder="$t('commons.input_content')"
size="small"
style="width: 200px"
/>
</div>
<template v-slot:footer>
<ms-dialog-footer @cancel="close" @confirm="handleRunBatch"/>
</template>
</el-dialog>
</template>
<script>
import MsDialogFooter from "@/business/components/common/components/MsDialogFooter";
export default {
name: "RunMode",
components: {MsDialogFooter},
data() {
return {
runModeVisible: false,
runConfig: {mode: "serial", reportType: "iddReport", reportName: ""},
};
},
methods: {
open() {
this.runModeVisible = true;
},
close() {
this.runConfig = {mode: "serial", reportType: "iddReport", reportName: ""};
this.runModeVisible = false;
},
handleRunBatch() {
if (this.runConfig.mode === 'serial' && this.runConfig.reportType === 'setReport' && this.runConfig.reportName.trim() === "") {
this.$warning(this.$t('commons.input_name'));
return;
}
this.$emit("handleRunBatch", this.runConfig);
this.close();
},
},
};
</script>
<style scoped>
.ms-mode-span {
margin-right: 10px;
}
.ms-mode-div {
margin-top: 20px;
}
</style>

View File

@ -2,9 +2,15 @@
<div>
<!-- 场景步骤-->
<ms-container>
<ms-aside-container>
<ms-aside-container style="padding-top: 0px">
<!-- 场景步骤内容 -->
<div v-loading="loading">
<el-button class="el-icon-files ms-open-btn ms-open-btn-left" size="mini" @click="openExpansion">
{{$t('api_test.automation.open_expansion')}}
</el-button>
<el-button class="el-icon-notebook-1 ms-open-btn" size="mini" @click="closeExpansion">
{{$t('api_test.automation.close_expansion')}}
</el-button>
<el-tree node-key="resourceId"
:props="props"
:data="scenarioDefinition"
@ -226,6 +232,7 @@
projectEnvMap: new Map,
projectList: [],
debugResult: new Map,
expandedStatus: false,
}
},
created() {
@ -940,6 +947,44 @@
//
this.debugResult = result;
this.sort()
},
shrinkTreeNode() {
//
for (let i in this.scenarioDefinition) {
if (this.scenarioDefinition[i]) {
if (this.expandedStatus) {
this.expandedNode.push(this.scenarioDefinition[i].resourceId);
}
this.scenarioDefinition[i].active = this.expandedStatus;
if (this.scenarioDefinition[i].hashTree && this.scenarioDefinition[i].hashTree.length > 0) {
this.changeNodeStatus(this.scenarioDefinition[i].hashTree);
}
}
}
},
changeNodeStatus(nodes) {
for (let i in nodes) {
if (nodes[i]) {
if (this.expandedStatus) {
this.expandedNode.push(nodes[i].resourceId);
}
nodes[i].active = this.expandedStatus;
if (nodes[i].hashTree != undefined && nodes[i].hashTree.length > 0) {
this.changeNodeStatus(nodes[i].hashTree);
}
}
}
},
openExpansion() {
this.expandedNode = [];
this.expandedStatus = true;
this.shrinkTreeNode();
},
closeExpansion() {
this.expandedStatus = false;
this.expandedNode = [];
this.shrinkTreeNode();
this.reload();
}
}
}
@ -1111,4 +1156,15 @@
.father:hover .child {
display: block;
}
.ms-open-btn {
margin: 5px 5px 0px;
font-size: 10px;
background-color: #F2F9EE;
color: #67C23A;
}
.ms-open-btn-left {
margin-left: 30px;
}
</style>

View File

@ -222,7 +222,7 @@
},
created() {
let dataRange = this.$route.params.dataSelectRange;
if (dataRange.length > 0) {
if (dataRange && dataRange.length > 0) {
this.activeDom = 'middle';
}
if (this.activeDom === 'left') {

View File

@ -248,7 +248,6 @@
if (!apiCase.request.hashTree) {
apiCase.request.hashTree = [];
}
});
this.apiCaseList = data;
if (!this.useEnvironment && this.apiCaseList[0] && this.apiCaseList[0].request && this.apiCaseList[0].request.useEnvironment) {

View File

@ -42,7 +42,7 @@
import {Environment} from "../../model/EnvironmentModel";
import MsApiHostTable from "./ApiHostTable";
import MsDatabaseConfig from "../request/database/DatabaseConfig";
import MsEnvironmentHttpConfig from "./EnvironmentHttpConfig";
import MsEnvironmentHttpConfig from "../../../test/components/environment/EnvironmentHttpConfig";
import MsEnvironmentCommonConfig from "./EnvironmentCommonConfig";
import EnvironmentTcpConfig from "./EnvironmentTcpConfig";

View File

@ -3,15 +3,15 @@
<el-row type="flex">
<el-col>
<div style="font-size: 14px;color: #AAAAAA;float: left">{{$t('api_report.response_code')}} :</div>
<div style="font-size: 14px;color:#61C550;margin-top:2px;margin-left:10px;float: left">{{response.responseResult.responseCode ? response.responseResult.responseCode :'0'}}</div>
<div style="font-size: 14px;color:#61C550;margin-top:2px;margin-left:10px;float: left">{{response.responseResult && response.responseResult.responseCode ? response.responseResult.responseCode :'0'}}</div>
</el-col>
<el-col>
<div style="font-size: 14px;color: #AAAAAA;float: left">{{$t('api_report.response_time')}} :</div>
<div style="font-size: 14px;color:#61C550;margin-top:2px;margin-left:10px;float: left">{{response.responseResult.responseTime?response.responseResult.responseTime:0}} ms</div>
<div style="font-size: 14px;color:#61C550;margin-top:2px;margin-left:10px;float: left">{{response.responseResult && response.responseResult.responseTime?response.responseResult.responseTime:0}} ms</div>
</el-col>
<el-col>
<div style="font-size: 14px;color: #AAAAAA;float: left">{{$t('api_report.response_size')}} :</div>
<div style="font-size: 14px;color:#61C550; margin-top:2px;margin-left:10px;float: left">{{response.responseResult.responseSize?response.responseResult.responseSize:0}} bytes</div>
<div style="font-size: 14px;color:#61C550; margin-top:2px;margin-left:10px;float: left">{{response.responseResult && response.responseResult.responseSize?response.responseResult.responseSize:0}} bytes</div>
</el-col>
</el-row>
</div>

View File

@ -117,8 +117,11 @@
if (!this.response.body) {
this.response.body = "";
}
if(!this.response.responseResult.vars){
this.response.responseResult.vars="";
if (!this.response.responseResult) {
this.response.responseResult = {};
}
if (!this.response.responseResult.vars) {
this.response.responseResult.vars = "";
}
this.reqMessages = this.$t('api_test.request.address') + ":\n" + this.response.url + "\n" +
this.$t('api_test.scenario.headers') + ":\n" + this.response.headers + "\n" + "Cookies :\n" +

View File

@ -145,7 +145,10 @@
})
},
runRefresh(data) {
this.responseData = data;
this.responseData = {type: 'HTTP', responseResult: {responseCode: ""}, subRequestResults: []};
if (data) {
this.responseData = data;
}
this.loading = false;
},
saveAs() {

View File

@ -1,12 +1,12 @@
<template>
<el-dialog :close-on-click-modal="false" :title="$t('api_test.environment.environment_config')"
:visible.sync="visible" class="environment-dialog" width="60%"
@close="close" append-to-body ref="environmentConfig">
@close="close" append-to-body destroy-on-close ref="environmentConfig">
<el-container v-loading="result.loading">
<ms-aside-item :enable-aside-hidden="false" :title="$t('api_test.environment.environment_list')"
:data="environments" :item-operators="environmentOperators" :add-fuc="addEnvironment"
:delete-fuc="deleteEnvironment" @itemSelected="environmentSelected" ref="environmentItems"/>
<environment-edit :environment="currentEnvironment" ref="environmentEdit" @close="close"/>
<environment-edit :project-id="projectId" :environment="currentEnvironment" ref="environmentEdit" @close="close"/>
</el-container>
</el-dialog>
</template>

View File

@ -15,7 +15,7 @@
</el-tab-pane>
<el-tab-pane :label="$t('api_test.environment.http_config')" name="http">
<ms-environment-http-config :http-config="environment.config.httpConfig" ref="httpConfig"/>
<ms-environment-http-config :project-id="projectId" :http-config="environment.config.httpConfig" ref="httpConfig"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.environment.database_config')" name="sql">
<ms-database-config :configs="environment.config.databaseConfigs"/>
@ -52,9 +52,11 @@
MsTcpConfig,
MsEnvironmentCommonConfig,
MsEnvironmentHttpConfig,
MsDatabaseConfig, MsApiHostTable, MsDialogFooter, MsApiKeyValue, MsApiScenarioVariables},
MsDatabaseConfig, MsApiHostTable, MsDialogFooter, MsApiKeyValue, MsApiScenarioVariables
},
props: {
environment: new Environment(),
projectId: String,
},
data() {

View File

@ -1,86 +1,282 @@
<template>
<el-form :model="httpConfig" :rules="rules" ref="httpConfig">
<span>{{$t('api_test.environment.socket')}}</span>
<el-form-item prop="socket">
<el-input v-model="httpConfig.socket" :placeholder="$t('api_test.request.url_description')" clearable>
<el-form :model="condition" :rules="rules" ref="httpConfig">
<el-form-item prop="socket">
<span class="ms-env-span">{{$t('api_test.environment.socket')}}</span>
<el-input v-model="condition.socket" style="width: 80%" :placeholder="$t('api_test.request.url_description')" clearable size="small">
<template v-slot:prepend>
<el-select v-model="condition.protocol" class="request-protocol-select" size="small">
<el-option label="http://" value="http"/>
<el-option label="https://" value="https"/>
</el-select>
</template>
</el-input>
</el-form-item>
<el-form-item prop="enable">
<span class="ms-env-span">{{$t('api_test.environment.condition_enable')}}</span>
<el-radio-group v-model="condition.type" @change="typeChange">
<el-radio label="NONE">{{ $t('api_test.definition.document.data_set.none') }}</el-radio>
<el-radio label="MODULE">{{$t('test_track.module.module')}}</el-radio>
<el-radio label="PATH">{{$t('api_test.definition.api_path')}}</el-radio>
</el-radio-group>
<el-button type="primary" v-if="!condition.id" style="float: right" size="mini" @click="add">{{$t('commons.add')}}</el-button>
<el-button type="primary" v-else style="float: right" size="mini" @click="update">{{$t('commons.update')}}</el-button>
<div v-if="condition.type === 'MODULE'">
<ms-select-tree size="small" :data="moduleOptions" :default-key="condition.ids" @getValue="setModule" :obj="moduleObj" clearable checkStrictly multiple/>
</div>
<div v-if="condition.type === 'PATH'">
<el-input v-model="pathDetails.name" :placeholder="$t('api_test.value')" clearable size="small">
<template v-slot:prepend>
<el-select v-model="httpConfig.protocol" class="request-protocol-select">
<el-option label="http://" value="http"/>
<el-option label="https://" value="https"/>
<el-select v-model="pathDetails.value" class="request-protocol-select" size="small">
<el-option :label="$t('api_test.request.assertions.contains')" value="contains"/>
<el-option :label="$t('commons.adv_search.operators.equals')" value="equals"/>
</el-select>
</template>
</el-input>
</el-form-item>
<span>{{$t('api_test.request.headers')}}</span>
<ms-api-key-value :items="httpConfig.headers" :isShowEnable="true" :suggestions="headerSuggestions"/>
</el-form>
</div>
</el-form-item>
<div class="ms-border">
<el-table :data="httpConfig.conditions" highlight-current-row @current-change="selectRow">
<el-table-column prop="socket" :label="$t('load_test.domain')" width="180">
<template v-slot:default="{row}">
{{getUrl(row)}}
</template>
</el-table-column>
<el-table-column prop="type" :label="$t('api_test.environment.condition_enable')" show-overflow-tooltip min-width="120px">
<template v-slot:default="{row}">
{{getName(row)}}
</template>
</el-table-column>
<el-table-column prop="details" show-overflow-tooltip min-width="120px" :label="$t('api_test.value')">
<template v-slot:default="{row}">
{{getDetails(row)}}
</template>
</el-table-column>
<el-table-column prop="createTime" show-overflow-tooltip min-width="120px" :label="$t('commons.create_time')">
<template v-slot:default="{row}">
<span>{{ row.time | timestampFormatDate }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('commons.operating')" width="100px">
<template v-slot:default="{row}">
<ms-table-operator-button :tip="$t('api_test.automation.copy')" icon="el-icon-document-copy" @exec="copy(row)"/>
<ms-table-operator-button :tip="$t('api_test.automation.remove')" icon="el-icon-delete" @exec="remove(row)" type="danger" v-tester/>
</template>
</el-table-column>
</el-table>
</div>
<span>{{$t('api_test.request.headers')}}</span>
<ms-api-key-value :items="httpConfig.headers" :isShowEnable="true" :suggestions="headerSuggestions"/>
</el-form>
</template>
<script>
import {HttpConfig} from "../../model/EnvironmentModel";
import MsApiKeyValue from "../ApiKeyValue";
import {REQUEST_HEADERS} from "../../../../../../common/js/constants";
import {HttpConfig} from "../../model/EnvironmentModel";
import MsApiKeyValue from "../ApiKeyValue";
import {REQUEST_HEADERS} from "../../../../../../common/js/constants";
import MsSelectTree from "../../../../common/select-tree/SelectTree";
import MsTableOperatorButton from "@/business/components/common/components/MsTableOperatorButton";
import {getUUID} from "@/common/js/utils";
import {KeyValue} from "../../../definition/model/ApiTestModel";
import Vue from "vue";
export default {
name: "MsEnvironmentHttpConfig",
components: {MsApiKeyValue},
props: {
httpConfig: new HttpConfig(),
},
data() {
let socketValidator = (rule, value, callback) => {
if (!this.validateSocket(value)) {
callback(new Error(this.$t('commons.formatErr')));
return false;
} else {
callback();
return true;
}
}
return {
headerSuggestions: REQUEST_HEADERS,
rules: {
socket: [{required: false, validator: socketValidator, trigger: 'blur'}],
},
}
},
methods: {
validateSocket(socket) {
if (!socket) return true;
let urlStr = this.httpConfig.protocol + '://' + socket;
let url = {};
try {
url = new URL(urlStr);
} catch (e) {
return false;
}
this.httpConfig.domain = decodeURIComponent(url.hostname);
this.httpConfig.port = url.port;
let path = url.pathname === '/' ? '' : url.pathname;
if (url.port) {
this.httpConfig.socket = this.httpConfig.domain + ':' + url.port + path;
} else {
this.httpConfig.socket = this.httpConfig.domain + path;
}
export default {
name: "MsEnvironmentHttpConfig",
components: {MsApiKeyValue, MsSelectTree, MsTableOperatorButton},
props: {
httpConfig: new HttpConfig(),
projectId: String,
},
created() {
this.list();
},
data() {
let socketValidator = (rule, value, callback) => {
if (!this.validateSocket(value)) {
callback(new Error(this.$t("commons.formatErr")));
return false;
} else {
callback();
return true;
},
validate() {
let isValidate = false;
this.$refs['httpConfig'].validate((valid) => {
isValidate = valid;
});
return isValidate;
}
}
}
};
return {
headerSuggestions: REQUEST_HEADERS,
rules: {
socket: [{required: false, validator: socketValidator, trigger: "blur"}],
},
moduleOptions: [],
moduleObj: {
id: "id",
label: "name",
},
pathDetails: new KeyValue({name: "", value: "contains"}),
condition: {type: "NONE", details: [new KeyValue({name: "", value: "contains"})], protocol: "http", socket: "", domain: "", port: 0},
};
},
watch: {
projectId() {
this.list();
},
httpConfig: function (o) {
this.condition = {type: "NONE", details: [new KeyValue({name: "", value: "contains"})], protocol: "http", socket: "", domain: "", port: 0};
},
},
methods: {
getUrl(row) {
return row.protocol + "://" + row.socket;
},
getName(row) {
switch (row.type) {
case "NONE":
return this.$t("api_test.definition.document.data_set.none");
case "MODULE":
return this.$t("test_track.module.module");
case "PATH":
return this.$t("api_test.definition.api_path");
}
},
getDetails(row) {
if (row && row.type === "MODULE") {
if (row.details && row.details instanceof Array) {
let value = "";
row.details.forEach((item) => {
value += item.name + ",";
});
if (value.endsWith(",")) {
value = value.substr(0, value.length - 1);
}
return value;
}
} else if (row && row.type === "PATH" && row.details.length > 0 && row.details[0].name) {
return row.details[0].value === "equals" ? this.$t("commons.adv_search.operators.equals") : this.$t("api_test.request.assertions.contains") + row.details[0].name;
} else {
return "";
}
},
selectRow(row) {
if (row) {
this.httpConfig.socket = row.socket;
this.httpConfig.protocol = row.protocol;
this.httpConfig.port = row.port;
this.condition = row;
if (row.type === "PATH" && row.details.length > 0) {
this.pathDetails = row.details[0];
} else if (row.type === "MODULE" && row.details.length > 0) {
this.condition.ids = [];
row.details.forEach((item) => {
this.condition.ids.push(item.value);
});
}
}
},
typeChange() {
switch (this.condition.type) {
case "NONE":
this.condition.details = [];
break;
case "MODULE":
this.condition.details = [];
break;
case "PATH":
this.pathDetails = new KeyValue({name: "", value: "contains"});
break;
}
},
list() {
let url = "/api/automation/module/list/" + this.projectId;
this.result = this.$get(url, (response) => {
if (response.data !== undefined && response.data !== null) {
this.moduleOptions = response.data;
}
});
},
setModule(id, data) {
if (data && data.length > 0) {
this.condition.details = [];
data.forEach((item) => {
this.condition.details.push(new KeyValue({name: item.name, value: item.id}));
});
}
},
update() {
const index = this.httpConfig.conditions.findIndex((d) => d.id === this.condition.id);
this.validateSocket(this.condition.socket);
let obj = {
id: this.condition.id, type: this.condition.type, domain: this.condition.domain, socket: this.condition.socket,
protocol: this.condition.protocol, details: this.condition.details, port: this.condition.port, time: this.condition.time
};
if (index !== -1) {
Vue.set(this.httpConfig.conditions[index], obj, 1);
this.condition = {type: "NONE", details: [new KeyValue({name: "", value: "contains"})], protocol: "", socket: "", domain: ""};
}
},
add() {
let obj = {
id: getUUID(), type: this.condition.type, socket: this.condition.socket, protocol: this.condition.protocol,
domain: this.condition.domain, port: this.condition.port, time: new Date().getTime()
};
if (this.condition.type === "PATH") {
obj.details = [JSON.parse(JSON.stringify(this.pathDetails))];
} else {
obj.details = this.condition.details ? JSON.parse(JSON.stringify(this.condition.details)) : this.condition.details;
}
this.httpConfig.conditions.unshift(obj);
},
remove(row) {
const index = this.httpConfig.conditions.findIndex((d) => d.id === row.id);
this.httpConfig.conditions.splice(index, 1);
},
copy(row) {
const index = this.httpConfig.conditions.findIndex((d) => d.id === row.id);
let obj = {id: getUUID(), type: row.type, socket: row.socket, details: row.details, protocol: row.protocol, domain: row.domain,};
if (index != -1) {
this.httpConfig.conditions.splice(index + 1, 0, obj);
} else {
this.httpConfig.conditions.push(obj);
}
},
validateSocket(socket) {
if (!socket) return true;
let urlStr = this.condition.protocol + "://" + socket;
let url = {};
try {
url = new URL(urlStr);
} catch (e) {
return false;
}
this.condition.domain = decodeURIComponent(url.hostname);
this.condition.port = url.port;
let path = url.pathname === "/" ? "" : url.pathname;
if (url.port) {
this.condition.socket = this.condition.domain + ":" + url.port + path;
} else {
this.condition.socket = this.condition.domain + path;
}
return true;
},
validate() {
let isValidate = false;
this.$refs["httpConfig"].validate((valid) => {
isValidate = valid;
});
return isValidate;
},
},
};
</script>
<style scoped>
.request-protocol-select {
width: 90px;
}
.ms-env-span {
margin-right: 10px;
}
/deep/ .el-form-item {
margin-bottom: 10px;
}
</style>

View File

@ -10,7 +10,6 @@ export class Environment extends BaseConfig {
this.name = undefined;
this.id = undefined;
this.config = undefined;
this.set(options);
this.sets({}, options);
}
@ -63,15 +62,16 @@ export class CommonConfig extends BaseConfig {
export class HttpConfig extends BaseConfig {
constructor(options = {}) {
super();
this.socket = undefined;
this.domain = undefined;
this.headers = [];
this.protocol = 'https';
this.port = undefined;
this.conditions = [];
this.defaultCondition = "NONE";
this.set(options);
this.sets({headers: KeyValue}, options);
this.sets({conditions: KeyValue}, options);
}
initOptions(options = {}) {

View File

@ -0,0 +1,61 @@
<template>
<el-dialog
destroy-on-close
:title="$t('load_test.runtime_config')"
width="350px"
:visible.sync="runModeVisible"
>
<div>
<span class="ms-mode-span">{{ $t("run_mode.title") }}</span>
<el-radio-group v-model="runConfig.mode">
<el-radio label="serial">{{ $t("run_mode.serial") }}</el-radio>
<el-radio label="parallel">{{ $t("run_mode.parallel") }}</el-radio>
</el-radio-group>
</div>
<div class="ms-mode-div" v-if="runConfig.mode === 'serial'">
<span class="ms-mode-span">{{ $t("run_mode.other_config") }}</span>
<el-checkbox v-model="runConfig.onSampleError">失败停止</el-checkbox>
</div>
<template v-slot:footer>
<ms-dialog-footer @cancel="close" @confirm="handleRunBatch"/>
</template>
</el-dialog>
</template>
<script>
import MsDialogFooter from "@/business/components/common/components/MsDialogFooter";
export default {
name: "MsPlanRunMode",
components: {MsDialogFooter},
data() {
return {
runModeVisible: false,
runConfig: {mode: "serial", reportType: "iddReport", onSampleError: false},
};
},
methods: {
open() {
this.runModeVisible = true;
},
close() {
this.runConfig = {mode: "serial", reportType: "iddReport", onSampleError: false};
this.runModeVisible = false;
},
handleRunBatch() {
this.$emit("handleRunBatch", this.runConfig);
this.close();
},
},
};
</script>
<style scoped>
.ms-mode-span {
margin-right: 10px;
}
.ms-mode-div {
margin-top: 20px;
}
</style>

View File

@ -142,6 +142,7 @@
<batch-edit :dialog-title="$t('test_track.case.batch_edit_case')" :type-arr="typeArr" :value-arr="valueArr"
:select-row="selectRows" ref="batchEdit" @batchEdit="batchEdit"/>
<ms-plan-run-mode @handleRunBatch="handleRunBatch" ref="runMode"/>
</el-card>
</div>
@ -187,6 +188,7 @@ import HeaderCustom from "@/business/components/common/head/HeaderCustom";
import {Test_Plan_Api_Case} from "@/business/components/common/model/JsonData";
import HeaderLabelOperate from "@/business/components/common/head/HeaderLabelOperate";
import MsTableHeaderSelectPopover from "@/business/components/common/components/table/MsTableHeaderSelectPopover";
import MsPlanRunMode from "../../../common/PlanRunMode";
export default {
@ -209,7 +211,8 @@ export default {
MsContainer,
MsBottomContainer,
ShowMoreBtn,
MsTableHeaderSelectPopover
MsTableHeaderSelectPopover,
MsPlanRunMode
},
data() {
return {
@ -461,22 +464,22 @@ export default {
});
}
}
}
});
},
getResult(data) {
if (RESULT_MAP.get(data)) {
return RESULT_MAP.get(data);
} else {
return RESULT_MAP.get("default");
}
});
},
getResult(data) {
if (RESULT_MAP.get(data)) {
return RESULT_MAP.get(data);
} else {
return RESULT_MAP.get("default");
}
},
runRefresh(data) {
this.rowLoading = "";
this.$success(this.$t('schedule.event_success'));
this.initTable();
},
},
runRefresh(data) {
this.rowLoading = "";
this.$success(this.$t('schedule.event_success'));
this.initTable();
},
singleRun(row) {
this.runData = [];
@ -498,6 +501,26 @@ export default {
this.$refs.batchEdit.open(this.selectRows.size);
this.$refs.batchEdit.setSelectRows(this.selectRows);
},
getData() {
return new Promise((resolve) => {
let index = 1;
this.runData = [];
this.selectRows.forEach(row => {
this.$get('/api/testcase/get/' + row.caseId, (response) => {
let apiCase = response.data;
let request = JSON.parse(apiCase.request);
request.name = row.id;
request.id = row.id;
request.useEnvironment = row.environmentId;
this.runData.push(request);
if (this.selectRows.size === index) {
resolve();
}
index++;
});
});
});
},
batchEdit(form) {
let param = {};
//
@ -537,58 +560,44 @@ export default {
}
},
handleBatchExecute() {
if(this.condition != null && this.condition.selectAll){
this.$alert(this.$t('commons.option_cannot_spread_pages'), '', {
confirmButtonText: this.$t('commons.confirm'),
callback: (action) => {
if (action === 'confirm') {
this.selectRows.forEach(row => {
this.$get('/api/testcase/get/' + row.caseId, (response) => {
let apiCase = response.data;
let request = JSON.parse(apiCase.request);
request.name = row.id;
request.id = row.id;
request.useEnvironment = row.environmentId;
let runData = [];
runData.push(request);
this.batchRun(runData, getUUID().substring(0, 8));
});
});
this.$message('任务执行中,请稍后刷新查看结果');
this.search();
}
}
});
}else {
this.selectRows.forEach(row => {
this.$get('/api/testcase/get/' + row.caseId, (response) => {
let apiCase = response.data;
let request = JSON.parse(apiCase.request);
request.name = row.id;
request.id = row.id;
request.useEnvironment = row.environmentId;
let runData = [];
runData.push(request);
this.batchRun(runData, getUUID().substring(0, 8));
});
});
this.$message('任务执行中,请稍后刷新查看结果');
this.search();
}
this.getData().then(() => {
if (this.runData && this.runData.length > 0) {
this.$refs.runMode.open();
}
});
},
batchRun(runData, reportId) {
handleRunBatch(config) {
let testPlan = new TestPlan();
let projectId = this.$store.state.projectId;
let threadGroup = new ThreadGroup();
threadGroup.hashTree = [];
testPlan.hashTree = [threadGroup];
runData.forEach(item => {
threadGroup.hashTree.push(item);
});
let reqObj = {id: reportId, testElement: testPlan, type: 'API_PLAN', reportId: "run", projectId: projectId};
let bodyFiles = getBodyUploadFiles(reqObj, runData);
this.$fileUpload("/api/definition/run", null, bodyFiles, reqObj, response => {
});
if (config.mode === 'serial') {
testPlan.serializeThreadgroups = true;
testPlan.hashTree = [];
this.runData.forEach(item => {
let threadGroup = new ThreadGroup();
threadGroup.onSampleError = config.onSampleError;
threadGroup.hashTree = [];
threadGroup.hashTree.push(item);
testPlan.hashTree.push(threadGroup);
});
let reqObj = {id: getUUID().substring(0, 8), testElement: testPlan, type: 'API_PLAN', reportId: "run", projectId: projectId};
let bodyFiles = getBodyUploadFiles(reqObj, this.runData);
this.$fileUpload("/api/definition/run", null, bodyFiles, reqObj, response => {
});
} else {
testPlan.serializeThreadgroups = false;
let threadGroup = new ThreadGroup();
threadGroup.hashTree = [];
testPlan.hashTree = [threadGroup];
this.runData.forEach(item => {
threadGroup.hashTree.push(item);
});
let reqObj = {id: getUUID().substring(0, 8), testElement: testPlan, type: 'API_PLAN', reportId: "run", projectId: projectId};
let bodyFiles = getBodyUploadFiles(reqObj, this.runData);
this.$fileUpload("/api/definition/run", null, bodyFiles, reqObj, response => {
});
}
this.search();
this.$message('任务执行中,请稍后刷新查看结果');
},
autoCheckStatus() { //
if (!this.planId) {

View File

@ -101,7 +101,7 @@
<!-- 批量编辑 -->
<batch-edit :dialog-title="$t('test_track.case.batch_edit_case')" :type-arr="typeArr" :value-arr="valueArr"
:select-row="selectRows" ref="batchEdit" @batchEdit="batchEdit"/>
<ms-plan-run-mode @handleRunBatch="handleRunBatch" ref="runMode"/>
</div>
</template>
@ -135,6 +135,7 @@ import {TEST_CASE_LIST, TEST_PLAN_SCENARIO_CASE} from "@/common/js/constants";
import {Test_Plan_Scenario_Case, Track_Test_Case} from "@/business/components/common/model/JsonData";
import HeaderLabelOperate from "@/business/components/common/head/HeaderLabelOperate";
import BatchEdit from "@/business/components/track/case/components/BatchEdit";
import MsPlanRunMode from "../../../common/PlanRunMode";
import MsTableHeaderSelectPopover from "@/business/components/common/components/table/MsTableHeaderSelectPopover";
export default {
@ -153,6 +154,7 @@ export default {
MsScenarioExtendButtons,
MsTestPlanList,
BatchEdit,
MsPlanRunMode,
MsTableHeaderSelectPopover
},
props: {
@ -292,49 +294,32 @@ export default {
})
},
handleBatchExecute() {
// if (this.reviewId) {} By.Song Tianyang
// if (this.reviewId) {
// this.selectRows.forEach(row => {
// let param = this.buildExecuteParam(row);
// this.$post("/test/case/review/scenario/case/run", param, response => {
// });
// });
// }
if(this.condition != null && this.condition.selectAll){
this.$alert(this.$t('commons.option_cannot_spread_pages'), '', {
confirmButtonText: this.$t('commons.confirm'),
callback: (action) => {
if (action === 'confirm') {
if (this.planId) {
this.selectRows.forEach(row => {
let param = this.buildExecuteParam(row);
this.$post("/test/plan/scenario/case/run", param, response => {
});
});
}
this.$message('任务执行中,请稍后刷新查看结果');
this.search();
}
}
this.$refs.runMode.open();
},
handleRunBatch(config){
if (this.reviewId) {
let param = {config : config,planCaseIds:[]};
this.selectRows.forEach(row => {
this.buildExecuteParam(param,row);
});
}else {
if (this.planId) {
this.selectRows.forEach(row => {
let param = this.buildExecuteParam(row);
this.$post("/test/plan/scenario/case/run", param, response => {
});
});
}
this.$message('任务执行中,请稍后刷新查看结果');
this.search();
this.$post("/test/case/review/scenario/case/run", param, response => {});
}
if (this.planId) {
let param = {config : config,planCaseIds:[]};
this.selectRows.forEach(row => {
this.buildExecuteParam(param,row);
});
console.log(param)
this.$post("/test/plan/scenario/case/run", param, response => {});
}
this.$message('任务执行中,请稍后刷新查看结果');
this.search();
},
execute(row) {
this.infoDb = false;
let param = this.buildExecuteParam(row);
console.log(param)
let param ={planCaseIds: []};
this.buildExecuteParam(param,row);
if (this.planId) {
this.$post("/test/plan/scenario/case/run", param, response => {
this.runVisible = true;
@ -348,14 +333,11 @@ export default {
});
}
},
buildExecuteParam(row) {
let param = {};
buildExecuteParam(param,row) {
// param.id = row.id;
param.id = getUUID();
param.planScenarioId = row.id;
console.log(row.id)
param.projectId = row.projectId;
param.planCaseIds = [];
param.planCaseIds.push(row.id);
return param;
},

View File

@ -16,6 +16,17 @@
<status-edit ref="statusEdit" :plan-id="reviewId"
:select-ids="new Set(Array.from(this.selectRows).map(row => row.id))" @refresh="initTableData"/>
<el-table
v-loading="result.loading"
class="adjust-table"
border
@select-all="handleSelectAll"
@filter-change="filter"
@sort-change="sort"
@select="handleSelectionChange"
row-key="id"
@row-click="showDetail"
:data="tableData">
<el-table
v-loading="result.loading"
class="test-content adjust-table ms-select-all-fixed"

View File

@ -712,6 +712,8 @@ export default {
}
},
automation: {
open_expansion: "One-click expansion",
close_expansion: "One-click storage",
constant: "constant",
counter: "counter",
random: "random",
@ -775,6 +777,7 @@ export default {
environment: {
name: "Environment Name",
socket: "Socket",
condition_enable: "Activation conditions",
globalVariable: "Global Variable",
environment_list: "Environment List",
environment_config: "Environment Config",
@ -1679,5 +1682,14 @@ export default {
header_display_field: 'Header display field',
fields_to_be_selected: 'Fields to be selected',
selected_fields: 'Selected fields'
},
run_mode: {
title: "Mode",
serial: "Serial",
parallel: "Parallel",
other_config: "Other config",
idd_report: "Report",
set_report: "Set report",
report_name: "Report name",
}
};

View File

@ -713,6 +713,8 @@ export default {
}
},
automation: {
open_expansion: "一键展开",
close_expansion: "一键收起",
constant: "常量",
counter: "计数器",
random: "随机数",
@ -776,6 +778,7 @@ export default {
environment: {
name: "环境名称",
socket: "环境域名",
condition_enable: "启用条件",
globalVariable: "全局变量",
environment_list: "环境列表",
environment_config: "环境配置",
@ -1682,5 +1685,14 @@ export default {
header_display_field: '表头显示字段',
fields_to_be_selected: '待选字段',
selected_fields: '已选字段'
},
run_mode: {
title: "模式",
serial: "串行",
parallel: "并行",
other_config: "其他配置",
idd_report: "独立报告",
set_report: "集合报告",
report_name: "报告名称",
}
};

View File

@ -712,6 +712,8 @@ export default {
}
},
automation: {
open_expansion: "一鍵展開",
close_expansion: "一鍵收起",
constant: "常量",
counter: "計數器",
random: "随机器",
@ -775,6 +777,7 @@ export default {
environment: {
name: "環境名稱",
socket: "環境域名",
condition_enable: "啟用條件",
globalVariable: "全局變量",
environment_list: "環境列表",
environment_config: "環境配置",
@ -1680,6 +1683,14 @@ export default {
header_display_field: '表頭顯示欄位',
fields_to_be_selected: '待選欄位',
selected_fields: '已選欄位'
},
run_mode: {
title: "模式",
serial: "串行",
parallel: "並行",
other_config: "其他配置",
idd_report: "獨立報告",
set_report: "集合報告",
report_name: "報告名稱",
}
};