refactor(接口测试): 开源失败重跑和失败重试及生成测试数据功能

--task=1011041 --user=赵勇 【开源计划】失败重试... https://www.tapd.cn/55049933/s/1327959
--task=1011040 --user=赵勇 【开源计划】自动生成... https://www.tapd.cn/55049933/s/1327961
This commit is contained in:
fit2-zhao 2023-01-17 14:39:58 +08:00 committed by fit2-zhao
parent 2e0b010fe2
commit a0326c34e3
18 changed files with 943 additions and 315 deletions

View File

@ -0,0 +1,72 @@
package io.metersphere.api.dto.definition.request.controller;
import io.metersphere.plugin.core.MsParameter;
import io.metersphere.plugin.core.MsTestElement;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.jmeter.control.WhileController;
import org.apache.jmeter.save.SaveService;
import org.apache.jmeter.testelement.TestElement;
import org.apache.jmeter.visualizers.JSR223Listener;
import org.apache.jorphan.collections.HashTree;
import java.util.List;
import java.util.UUID;
@Data
@EqualsAndHashCode(callSuper = true)
public class MsRetryLoopController extends MsTestElement {
private String type = "RetryLoopController";
private String clazzName = MsRetryLoopController.class.getCanonicalName();
private long retryNum;
private String ms_current_timer = UUID.randomUUID().toString();
@Override
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, MsParameter msParameter) {
final HashTree groupTree = controller(tree);
if (CollectionUtils.isNotEmpty(hashTree)) {
hashTree.forEach(el -> {
// 给所有孩子加一个父亲标志
el.setParent(this);
el.toHashTree(groupTree, el.getHashTree(), msParameter);
});
}
}
private WhileController initWhileController(String condition) {
if (StringUtils.isEmpty(condition)) {
return null;
}
WhileController controller = new WhileController();
controller.setEnabled(this.isEnable());
controller.setName("WhileController");
controller.setProperty(TestElement.TEST_CLASS, WhileController.class.getName());
controller.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("WhileControllerGui"));
controller.setCondition(condition);
return controller;
}
private String script() {
String script = "// 失败重试控制\n" + "try{\n" + "\tint errorCount = prev.getErrorCount();\n" + "\tif(errorCount == 0 && prev.getFirstAssertionFailureMessage() == null ){\n" + "\t vars.put(\"" + ms_current_timer + "\", \"stop\");\n" + "\t}\n" + "\tif(vars.get(\"" + ms_current_timer + "_num\") == null){\n" + "\t\tvars.put(\"" + ms_current_timer + "_num\", \"0\");\n" + "\t}else{\n" + "\t\tint retryNum= Integer.parseInt(vars.get(\"" + ms_current_timer + "_num\"));\n" + "\t\tlog.info(\"重试:\"+ retryNum);\n" + " \tprev.setSampleLabel(\"MsRetry_\"+ (retryNum + 1) + \"_\" + prev.getSampleLabel());\n" + "\t\tretryNum =retryNum +1;\n" + "\t\tvars.put(\"" + ms_current_timer + "_num\",retryNum + \"\");\n" + "\t}\n" + "\tif(vars.get(\"" + ms_current_timer + "_num\").equals( \"" + retryNum + "\")){\n" + "\t\tvars.put(\"" + ms_current_timer + "\", \"stop\");\n" + "\t}\n" + "}catch (Exception e){\n" + "\tvars.put(\"" + ms_current_timer + "\", \"stop\");\n" + "}\n";
return script;
}
private HashTree controller(HashTree tree) {
String whileCondition = "${__jexl3(" + "\"${" + ms_current_timer + "}\" !=\"stop\")}";
HashTree hashTree = tree.add(initWhileController(whileCondition));
// 添加超时处理防止死循环
JSR223Listener postProcessor = new JSR223Listener();
postProcessor.setName("Retry-controller");
postProcessor.setProperty(TestElement.TEST_CLASS, JSR223Listener.class.getName());
postProcessor.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("TestBeanGUI"));
postProcessor.setProperty("scriptLanguage", "beanshell");
postProcessor.setProperty("script", script());
hashTree.add(postProcessor);
return hashTree;
}
}

View File

@ -34,7 +34,7 @@ import io.metersphere.plugin.core.MsTestElement;
import io.metersphere.service.ApiExecutionQueueService; import io.metersphere.service.ApiExecutionQueueService;
import io.metersphere.service.RemakeReportService; import io.metersphere.service.RemakeReportService;
import io.metersphere.utils.LoggerUtil; import io.metersphere.utils.LoggerUtil;
import io.metersphere.xpack.api.service.ApiRetryOnFailureService; import io.metersphere.service.ApiRetryOnFailureService;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.jorphan.collections.HashTree; import org.apache.jorphan.collections.HashTree;
import org.json.JSONObject; import org.json.JSONObject;
@ -59,6 +59,8 @@ public class ApiCaseSerialService {
private RedisTemplate<String, Object> redisTemplate; private RedisTemplate<String, Object> redisTemplate;
@Resource @Resource
private TestPlanApiCaseMapper testPlanApiCaseMapper; private TestPlanApiCaseMapper testPlanApiCaseMapper;
@Resource
private ApiRetryOnFailureService apiRetryOnFailureService;
public void serial(DBTestQueue executionQueue) { public void serial(DBTestQueue executionQueue) {
ApiExecutionQueueDetail queue = executionQueue.getDetail(); ApiExecutionQueueDetail queue = executionQueue.getDetail();
@ -154,7 +156,6 @@ public class ApiCaseSerialService {
String data = element.toString(); String data = element.toString();
if (runRequest.isRetryEnable() && runRequest.getRetryNum() > 0) { if (runRequest.isRetryEnable() && runRequest.getRetryNum() > 0) {
// 失败重试 // 失败重试
ApiRetryOnFailureService apiRetryOnFailureService = CommonBeanFactory.getBean(ApiRetryOnFailureService.class);
String retryData = apiRetryOnFailureService.retry(data, runRequest.getRetryNum(), true); String retryData = apiRetryOnFailureService.retry(data, runRequest.getRetryNum(), true);
data = StringUtils.isNotEmpty(retryData) ? retryData : data; data = StringUtils.isNotEmpty(retryData) ? retryData : data;
// 格式化数据 // 格式化数据

View File

@ -23,7 +23,7 @@ import io.metersphere.service.ApiExecutionQueueService;
import io.metersphere.service.RemakeReportService; import io.metersphere.service.RemakeReportService;
import io.metersphere.utils.LoggerUtil; import io.metersphere.utils.LoggerUtil;
import io.metersphere.vo.BooleanPool; import io.metersphere.vo.BooleanPool;
import io.metersphere.xpack.api.service.ApiRetryOnFailureService; import io.metersphere.service.ApiRetryOnFailureService;
import org.apache.commons.collections.MapUtils; import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.jorphan.collections.HashTree; import org.apache.jorphan.collections.HashTree;

View File

@ -0,0 +1,17 @@
package io.metersphere.controller;
import io.metersphere.service.TestDataGenerator;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(value = "/api/test/data")
public class TestDataController {
@PostMapping("/generator")
public String preview(@RequestBody String jsonSchema) {
return TestDataGenerator.generator(jsonSchema);
}
}

View File

@ -0,0 +1,90 @@
package io.metersphere.service;
import io.metersphere.api.dto.definition.request.controller.MsRetryLoopController;
import io.metersphere.commons.utils.JSON;
import io.metersphere.commons.utils.JSONUtil;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.plugin.core.MsTestElement;
import org.apache.commons.lang3.StringUtils;
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.UUID;
@Service
public class ApiRetryOnFailureService {
public final static List<String> requests = List.of(
"HTTPSamplerProxy",
"DubboSampler",
"JDBCSampler",
"TCPSampler",
"JSR223Processor");
private final static String HASH_TREE_ELEMENT = "hashTree";
private final static String TYPE = "type";
private final static String RESOURCE_ID = "resourceId";
private final static String RETRY = "MsRetry_";
private final static String LOOP = "LoopController";
public String retry(String data, long retryNum, boolean isCase) {
if (StringUtils.isNotEmpty(data)) {
JSONObject element = JSONUtil.parseObject(data);
if (element != null && isCase) {
return formatSampler(element, retryNum).toString();
}
if (element != null && element.has(HASH_TREE_ELEMENT) && !StringUtils.equalsIgnoreCase(element.optString(TYPE), LOOP)) {
JSONArray hashTree = element.getJSONArray(HASH_TREE_ELEMENT);
setRetry(hashTree, retryNum);
}
return element.toString();
}
return null;
}
public MsTestElement retryParse(String data) {
try {
MsRetryLoopController controller = JSON.parseObject(data, MsRetryLoopController.class);
return controller;
} catch (Exception e) {
LogUtil.error(e);
}
return null;
}
public void setRetry(JSONArray hashTree, long retryNum) {
for (int i = 0; i < hashTree.length(); i++) {
JSONObject element = hashTree.getJSONObject(i);
if (StringUtils.equalsIgnoreCase(element.optString(TYPE), LOOP)) {
continue;
}
JSONObject whileObj = formatSampler(element, retryNum);
if (whileObj != null) {
hashTree.put(i, whileObj);
} else if (element.has(HASH_TREE_ELEMENT)) {
JSONArray elementJSONArray = element.getJSONArray(HASH_TREE_ELEMENT);
setRetry(elementJSONArray, retryNum);
}
}
}
private JSONObject formatSampler(JSONObject element, long retryNum) {
if (element.has(TYPE) && requests.contains(element.optString(TYPE))) {
MsRetryLoopController loopController = new MsRetryLoopController();
loopController.setClazzName(MsRetryLoopController.class.getCanonicalName());
loopController.setName(RETRY + element.optString(RESOURCE_ID));
loopController.setRetryNum(retryNum);
loopController.setEnable(true);
loopController.setResourceId(UUID.randomUUID().toString());
JSONObject whileObj = JSONUtil.parseObject(JSON.toJSONString(loopController));
JSONArray hashTree = new JSONArray();
hashTree.put(element);
whileObj.put(HASH_TREE_ELEMENT, hashTree);
return whileObj;
}
return null;
}
}

View File

@ -201,7 +201,7 @@ public class MsHashTreeService {
private JSONObject setRefScenario(JSONObject element) { private JSONObject setRefScenario(JSONObject element) {
boolean enable = element.has(ENABLE) ? element.optBoolean(ENABLE) : true; boolean enable = element.has(ENABLE) ? element.optBoolean(ENABLE) : true;
if (!element.has(MIX_ENABLE)) { if (!element.has(MIX_ENABLE)) {
element.put(MIX_ENABLE, true); element.put(MIX_ENABLE, false);
} }
ApiScenarioDTO scenarioWithBLOBs = extApiScenarioMapper.selectById(element.optString(ID)); ApiScenarioDTO scenarioWithBLOBs = extApiScenarioMapper.selectById(element.optString(ID));
@ -209,7 +209,7 @@ public class MsHashTreeService {
boolean environmentEnable = element.has(ENV_ENABLE) ? element.optBoolean(ENV_ENABLE) : false; boolean environmentEnable = element.has(ENV_ENABLE) ? element.optBoolean(ENV_ENABLE) : false;
boolean variableEnable = element.has(VARIABLE_ENABLE) ? element.optBoolean(VARIABLE_ENABLE) : false; boolean variableEnable = element.has(VARIABLE_ENABLE) ? element.optBoolean(VARIABLE_ENABLE) : false;
boolean mixEnable = element.has(MIX_ENABLE) boolean mixEnable = element.has(MIX_ENABLE)
? element.getBoolean(MIX_ENABLE) : true; ? element.getBoolean(MIX_ENABLE) : false;
if (environmentEnable && StringUtils.isNotEmpty(scenarioWithBLOBs.getEnvironmentJson())) { if (environmentEnable && StringUtils.isNotEmpty(scenarioWithBLOBs.getEnvironmentJson())) {
element.put(ENV_MAP, JSON.parseObject(scenarioWithBLOBs.getEnvironmentJson(), Map.class)); element.put(ENV_MAP, JSON.parseObject(scenarioWithBLOBs.getEnvironmentJson(), Map.class));

View File

@ -0,0 +1,345 @@
package io.metersphere.service;
import com.apifan.common.random.source.DateTimeSource;
import com.apifan.common.random.source.InternetSource;
import com.apifan.common.random.source.NumberSource;
import com.google.gson.*;
import com.mifmif.common.regex.Generex;
import io.metersphere.jmeter.utils.ScriptEngineUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.json.JSONObject;
import java.time.LocalDate;
import java.util.LinkedList;
import java.util.List;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 生成测试数据
*/
public class TestDataGenerator {
private static final String TYPE = "type";
private static final String ALL_OF = "allOf";
private static final String DEFINITIONS = "definitions";
private static final String PROPERTIES = "properties";
private static final String ARRAY = "array";
private static final String OBJECT = "object";
private static final String MS_OBJECT = "MS-OBJECT";
private static final String ITEMS = "items";
private static final String ENUM = "enum";
private static final String STRING = "string";
private static final String DEFAULT = "default";
private static final String MOCK = "mock";
private static final String MAXLENGTH = "maxLength";
private static final String MINLENGTH = "minLength";
private static final String FORMAT = "format";
private static final String PATTERN = "pattern";
private static final String INTEGER = "integer";
private static final String NUMBER = "number";
private static final String BOOLEAN = "boolean";
private static final String MINIMUM = "minimum";
private static final String MAXIMUM = "maximum";
public static void analyzeSchema(String json, JSONObject rootObj) {
Gson gson = new Gson();
JsonElement element = gson.fromJson(json, JsonElement.class);
JsonObject rootElement = element.getAsJsonObject();
analyzeRootSchemaElement(rootElement, rootObj);
}
public static void analyzeRootSchemaElement(JsonObject rootElement, JSONObject rootObj) {
if ((rootElement.has(TYPE) || rootElement.has(ALL_OF)) && rootElement != null) {
analyzeObject(rootElement, rootObj);
}
if (rootElement.has(DEFINITIONS)) {
analyzeDefinitions(rootElement);
}
}
public static void analyzeObject(JsonObject object, JSONObject rootObj) {
if (object.has(ALL_OF)) {
for (JsonElement el : object.get(ALL_OF).getAsJsonArray()) {
JsonObject elObj = el.getAsJsonObject();
if (elObj.has(PROPERTIES)) {
analyzeProperties(rootObj, elObj);
}
}
} else if (object.has(PROPERTIES)) {
analyzeProperties(rootObj, object);
} else if (object.has(TYPE) && object.get(TYPE).getAsString().equals(ARRAY)) {
analyzeProperty(rootObj, MS_OBJECT, object);
} else if (object.has(TYPE) && !object.get(TYPE).getAsString().equals(OBJECT)) {
analyzeProperty(rootObj, object.getAsString(), object);
}
}
private static void analyzeProperties(JSONObject rootObj, JsonObject allOfElementObj) {
JsonObject propertiesObj = allOfElementObj.get(PROPERTIES).getAsJsonObject();
for (Entry<String, JsonElement> entry : propertiesObj.entrySet()) {
String propertyKey = entry.getKey();
JsonObject propertyObj = propertiesObj.get(propertyKey).getAsJsonObject();
analyzeProperty(rootObj, propertyKey, propertyObj);
}
}
public static void analyzeItems(JSONObject concept, String propertyName, JsonObject object) {
// 先设置空值
List<Object> array = new LinkedList<>();
JsonArray jsonArray = new JsonArray();
if (object.has(ITEMS) && object.get(ITEMS).isJsonArray()) {
jsonArray = object.get(ITEMS).getAsJsonArray();
} else {
JsonObject itemsObject = object.get(ITEMS).getAsJsonObject();
array.add(itemsObject);
}
for (JsonElement element : jsonArray) {
JsonObject itemsObject = element.getAsJsonObject();
if (object.has(ITEMS)) {
if (itemsObject.has(ENUM)) {
array.add(analyzeEnumProperty(itemsObject));
} else if (itemsObject.has(TYPE) && itemsObject.get(TYPE).getAsString().equals(STRING)) {
array.add(analyzeString(itemsObject));
} else if (itemsObject.has(PROPERTIES)) {
JSONObject propertyConcept = new JSONObject();
analyzeProperties(propertyConcept, itemsObject);
array.add(propertyConcept);
} else if (itemsObject.has(TYPE) && itemsObject.get(TYPE) instanceof JsonPrimitive) {
JSONObject newJsonObj = new JSONObject();
analyzeProperty(newJsonObj, propertyName + "_item", itemsObject);
array.add(newJsonObj.get(propertyName + "_item"));
}
} else if (object.has(ITEMS) && object.get(ITEMS).isJsonArray()) {
JsonArray itemsObjectArray = object.get(ITEMS).getAsJsonArray();
array.add(itemsObjectArray);
}
}
concept.put(propertyName, array);
}
public static String getMockValue(JsonObject object) {
if (object.has(MOCK)
&& object.get(MOCK).getAsJsonObject() != null
&& object.get(MOCK).getAsJsonObject().get(MOCK) != null
&& StringUtils.isNotBlank(object.get(MOCK).getAsJsonObject().get(MOCK).getAsString())) {
if (StringUtils.startsWithAny(object.get(MOCK).getAsJsonObject().get(MOCK).getAsString(), "@", "${")) {
return ScriptEngineUtils.calculate(object.get(MOCK).getAsJsonObject().get(MOCK).getAsString());
}
}
return null;
}
public static String analyzeString(JsonObject object) {
// 先设置空值
if (object.has(DEFAULT)) {
return object.get(DEFAULT).getAsString();
}
Object mockValue = getMockValue(object);
if (mockValue != null) {
return mockValue.toString();
}
int maxLength = 9;
if (object.has(MAXLENGTH)) {
maxLength = object.get(MAXLENGTH).getAsInt();
}
int minLength = 0;
if (object.has(MINLENGTH)) {
minLength = object.get(MINLENGTH).getAsInt();
}
String value = RandomStringUtils.randomAlphanumeric(minLength, maxLength);
Object enumObj = analyzeEnumProperty(object);
String v = enumObj == null ? "" : String.valueOf(enumObj);
value = StringUtils.isNotBlank(v) ? v : value;
try {
if (object.has(FORMAT)) {
String propertyFormat = object.get(FORMAT).getAsString();
switch (propertyFormat) {
case "date-time":
value = DateTimeSource.getInstance().randomTimestamp(LocalDate.now()) + "";
break;
case "date":
value = DateTimeSource.getInstance().randomDate(LocalDate.now().getYear(), "yyyy-MM-dd");
break;
case "email":
value = InternetSource.getInstance().randomEmail(maxLength);
break;
case "hostname":
value = InternetSource.getInstance().randomDomain(maxLength);
break;
case "ipv4":
value = InternetSource.getInstance().randomPublicIpv4();
break;
case "ipv6":
value = InternetSource.getInstance().randomIpV6();
break;
case "uri":
value = InternetSource.getInstance().randomStaticUrl("jpg");
break;
}
}
if (object.has(PATTERN)) {
String pattern = object.get(PATTERN).getAsString();
if (StringUtils.isNotEmpty(pattern)) {
Generex generex = new Generex(pattern);
value = generex.random();
}
}
return value;
} catch (Exception e) {
return value;
}
}
public static boolean isNumber(Object obj) {
if (ObjectUtils.isEmpty(obj)) {
return false;
}
Pattern pattern = Pattern.compile("-?[0-9]+\\.?[0-9]*");
Matcher isNum = pattern.matcher(obj.toString());
if (!isNum.matches()) {
return false;
}
return true;
}
public static Object analyzeInteger(JsonObject object) {
// 先设置空值
if (object.has(DEFAULT) && isNumber(object.get(DEFAULT))) {
return object.get(DEFAULT).getAsInt();
}
Object mockValue = getMockValue(object);
if (mockValue != null && isNumber(mockValue)) {
return Integer.parseInt(mockValue.toString());
}
int minimum = 1;
int maximum = 101;
if (object.has(MINIMUM)) {
minimum = object.get(MINIMUM).getAsInt() < 0 ? 0 : object.get(MINIMUM).getAsInt();
}
if (object.has(MAXIMUM)) {
maximum = object.get(MAXIMUM).getAsInt();
}
return NumberSource.getInstance().randomInt(minimum, maximum);
}
public static Object analyzeNumber(JsonObject object) {
if (object != null && object.has(DEFAULT) && isNumber(object.has(DEFAULT))) {
return object.get(DEFAULT).getAsFloat();
}
Object mockValue = getMockValue(object);
if (mockValue != null && isNumber(mockValue)) {
return Float.parseFloat(mockValue.toString());
}
float maximum = 200001.0f;
float minimum = 100000.0f;
if (object.has(MINIMUM)) {
float min = object.get(MINIMUM).getAsFloat();
minimum = min < 0 ? 0 : min;
}
if (object.has(MAXIMUM)) {
float max = object.get(MAXIMUM).getAsFloat();
maximum = max > 0 ? max : maximum;
}
return NumberSource.getInstance().randomDouble(minimum, maximum);
}
public static boolean getRandomBoolean() {
return Math.random() < 0.5;
}
public static Boolean analyzeBoolean(JsonObject object) {
if (object.has(DEFAULT)) {
return object.get(DEFAULT).getAsBoolean();
}
return getRandomBoolean();
}
public static void analyzeProperty(JSONObject concept, String propertyName, JsonObject object) {
if (object != null && object.has(TYPE)) {
String propertyObjType = getPropertyObjType(object);
if (object.has(DEFAULT)) {
concept.put(propertyName, object.get(DEFAULT).getAsString());
} else if (object.has(ENUM)) {
concept.put(propertyName, analyzeEnumProperty(object));
} else if (propertyObjType.equals(STRING)) {
concept.put(propertyName, analyzeString(object));
} else if (propertyObjType.equals(INTEGER)) {
concept.put(propertyName, analyzeInteger(object));
} else if (propertyObjType.equals(NUMBER)) {
concept.put(propertyName, analyzeNumber(object));
} else if (propertyObjType.equals(BOOLEAN)) {
concept.put(propertyName, analyzeBoolean(object));
} else if (propertyObjType.equals(ARRAY)) {
analyzeItems(concept, propertyName, object);
} else if (propertyObjType.equals(OBJECT)) {
JSONObject obj = new JSONObject();
concept.put(propertyName, obj);
analyzeObject(object, obj);
}
}
}
public static String getPropertyObjType(JsonObject object) {
if (object.get(TYPE) != null && object.get(TYPE) instanceof JsonPrimitive) {
return object.get(TYPE).getAsString();
} else if (object.get(TYPE) instanceof JsonArray) {
JsonArray typeArray = object.get(TYPE).getAsJsonArray();
return typeArray.get(0).getAsString();
}
return null;
}
public static Object analyzeEnumProperty(JsonObject object) {
Object enumValue = "";
try {
if (object.get(ENUM) != null) {
String enums = object.get(ENUM).getAsString();
if (StringUtils.isNotBlank(enums)) {
String enumArr[] = enums.split("\n");
int index = (int) (Math.random() * enumArr.length);
enumValue = enumArr[index];
}
}
String propertyObjType = getPropertyObjType(object);
if (propertyObjType.equals(INTEGER)) {
enumValue = Integer.parseInt(enumValue.toString());
} else if (propertyObjType.equals(NUMBER)) {
enumValue = Float.parseFloat(enumValue.toString());
}
} catch (Exception e) {
return enumValue;
}
return enumValue;
}
public static void analyzeDefinitions(JsonObject object) {
JsonObject definitionsObj = object.get(DEFINITIONS).getAsJsonObject();
for (Entry<String, JsonElement> entry : definitionsObj.entrySet()) {
String definitionKey = entry.getKey();
JsonObject definitionObj = definitionsObj.get(definitionKey).getAsJsonObject();
JSONObject obj = new JSONObject();
analyzeRootSchemaElement(definitionObj, obj);
}
}
public static String generator(String jsonSchema) {
try {
if (StringUtils.isEmpty(jsonSchema)) {
return null;
}
JSONObject root = new JSONObject();
analyzeSchema(jsonSchema, root);
// 格式化返回
if (root != null && root.has(MS_OBJECT)) {
return root.get(MS_OBJECT).toString();
}
return root.toString();
} catch (Exception ex) {
return jsonSchema;
}
}
}

View File

@ -118,7 +118,6 @@ import {apiProjectByScenarioId, getProjectApplicationConfig} from '../../../api/
import { apiTestReRun } from '../../../api/xpack'; import { apiTestReRun } from '../../../api/xpack';
import { getUUID } from 'metersphere-frontend/src/utils'; import { getUUID } from 'metersphere-frontend/src/utils';
import { getApiScenarioIdByPlanScenarioId } from '@/api/test-plan'; import { getApiScenarioIdByPlanScenarioId } from '@/api/test-plan';
import {getScenarioReport} from '../../../api/scenario-report';
export default { export default {
name: 'MsApiReportViewHeader', name: 'MsApiReportViewHeader',

View File

@ -47,7 +47,7 @@
@click.stop @click.stop
@click="generate" @click="generate"
style="margin-left: 10px" style="margin-left: 10px"
v-if="hasPermission('PROJECT_API_DEFINITION:READ+CREATE_API') && hasLicense()"> v-if="hasPermission('PROJECT_API_DEFINITION:READ+CREATE_API')">
{{ $t('commons.generate_test_data') }} {{ $t('commons.generate_test_data') }}
</el-button> </el-button>
</div> </div>

View File

@ -111,9 +111,7 @@
:headers="request.headers" :headers="request.headers"
:body="request.body" /> :body="request.body" />
</el-tab-pane> </el-tab-pane>
<el-tab-pane <el-tab-pane name="create" v-if="hasPermission('PROJECT_API_DEFINITION:READ+CREATE_API') && definitionTest">
name="create"
v-if="hasPermission('PROJECT_API_DEFINITION:READ+CREATE_API') && hasLicense() && definitionTest">
<template v-slot:label> <template v-slot:label>
<el-button size="mini" type="primary" @click.stop @click="generate" <el-button size="mini" type="primary" @click.stop @click="generate"
>{{ $t('commons.generate_test_data') }} >{{ $t('commons.generate_test_data') }}

View File

@ -59,7 +59,7 @@
@click.stop @click.stop
@click="generate" @click="generate"
style="margin-left: 10px" style="margin-left: 10px"
v-if="hasPermission('PROJECT_API_DEFINITION:READ+CREATE_API') && hasLicense()"> v-if="hasPermission('PROJECT_API_DEFINITION:READ+CREATE_API')">
{{ $t('commons.generate_test_data') }} {{ $t('commons.generate_test_data') }}
</el-button> </el-button>
</el-form-item> </el-form-item>

View File

@ -1,10 +0,0 @@
package io.metersphere.xpack.api.service;
import io.metersphere.plugin.core.MsTestElement;
public interface ApiRetryOnFailureService {
public String retry(String data, long retryNum, boolean isCase);
public MsTestElement retryParse(String retryCase);
}

View File

@ -117,7 +117,7 @@
</div> </div>
<!-- 失败重试 --> <!-- 失败重试 -->
<div class="mode-row" v-if="isHasLicense"> <div class="mode-row">
<el-checkbox <el-checkbox
v-model="runConfig.retryEnable" v-model="runConfig.retryEnable"
class="ms-failure-div-right" class="ms-failure-div-right"
@ -127,7 +127,7 @@
<span v-if="runConfig.retryEnable"> <span v-if="runConfig.retryEnable">
<el-tooltip placement="top" style="margin: 0 4px 0 2px"> <el-tooltip placement="top" style="margin: 0 4px 0 2px">
<div slot="content">{{ $t("run_mode.retry_message") }}</div> <div slot="content">{{ $t("run_mode.retry_message") }}</div>
<i class="el-icon-question" style="cursor: pointer"/> <i class="el-icon-question" style="cursor: pointer" />
</el-tooltip> </el-tooltip>
<span style="margin-left: 10px"> <span style="margin-left: 10px">
{{ $t("run_mode.retry") }} {{ $t("run_mode.retry") }}
@ -146,9 +146,8 @@
</div> </div>
<div class="mode-row" v-if="runConfig.mode === 'serial'"> <div class="mode-row" v-if="runConfig.mode === 'serial'">
<el-checkbox v-model="runConfig.onSampleError">{{ <el-checkbox v-model="runConfig.onSampleError"
$t("api_test.fail_to_stop") >{{ $t("api_test.fail_to_stop") }}
}}
</el-checkbox> </el-checkbox>
</div> </div>
@ -166,44 +165,43 @@
<el-button @click="close">{{ $t("commons.cancel") }}</el-button> <el-button @click="close">{{ $t("commons.cancel") }}</el-button>
<el-dropdown @command="handleCommand" style="margin-left: 5px"> <el-dropdown @command="handleCommand" style="margin-left: 5px">
<el-button type="primary"> <el-button type="primary">
{{ {{ $t("load_test.save_and_run")
$t("load_test.save_and_run")
}}<i class="el-icon-arrow-down el-icon--right"></i> }}<i class="el-icon-arrow-down el-icon--right"></i>
</el-button> </el-button>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
<el-dropdown-item command="run">{{ <el-dropdown-item command="run"
$t("load_test.save_and_run") >{{ $t("load_test.save_and_run") }}
}}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item command="save">{{ <el-dropdown-item command="save"
$t("commons.save") >{{ $t("commons.save") }}
}}
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
</div> </div>
<ms-dialog-footer v-else @cancel="close" @confirm="handleRunBatch"/> <ms-dialog-footer v-else @cancel="close" @confirm="handleRunBatch" />
</template> </template>
</el-dialog> </el-dialog>
</template> </template>
<script> <script>
import MsDialogFooter from "metersphere-frontend/src/components/MsDialogFooter"; import MsDialogFooter from "metersphere-frontend/src/components/MsDialogFooter";
import {hasLicense} from "metersphere-frontend/src/utils/permission"; import { strMapToObj } from "metersphere-frontend/src/utils";
import {strMapToObj} from "metersphere-frontend/src/utils";
import {ENV_TYPE} from "metersphere-frontend/src/utils/constants"; import { ENV_TYPE } from "metersphere-frontend/src/utils/constants";
import {getCurrentProjectID, getOwnerProjects} from "@/business/utils/sdk-utils"; import {
import {getQuotaValidResourcePools} from "@/api/remote/resource-pool"; getCurrentProjectID,
getOwnerProjects,
} from "@/business/utils/sdk-utils";
import { getQuotaValidResourcePools } from "@/api/remote/resource-pool";
import EnvGroupPopover from "@/business/plan/env/EnvGroupPopover"; import EnvGroupPopover from "@/business/plan/env/EnvGroupPopover";
import {getApiCaseEnv} from "@/api/remote/plan/test-plan-api-case"; import { getApiCaseEnv } from "@/api/remote/plan/test-plan-api-case";
import {getApiScenarioEnv, getPlanCaseEnv} from "@/api/remote/plan/test-plan"; import { getApiScenarioEnv, getPlanCaseEnv } from "@/api/remote/plan/test-plan";
import {getSystemBaseSetting} from "metersphere-frontend/src/api/system"; import { getSystemBaseSetting } from "metersphere-frontend/src/api/system";
import {getProjectConfig} from "@/api/project"; import { getProjectConfig } from "@/api/project";
export default { export default {
name: "MsPlanRunModeWithEnv", name: "MsPlanRunModeWithEnv",
components: {EnvGroupPopover, MsDialogFooter}, components: { EnvGroupPopover, MsDialogFooter },
data() { data() {
return { return {
loading: false, loading: false,
@ -227,7 +225,6 @@ export default {
retryNum: 1, retryNum: 1,
browser: "CHROME", browser: "CHROME",
}, },
isHasLicense: hasLicense(),
projectEnvListMap: {}, projectEnvListMap: {},
projectList: [], projectList: [],
projectIds: new Set(), projectIds: new Set(),
@ -274,8 +271,12 @@ export default {
open(testType, runModeConfig) { open(testType, runModeConfig) {
if (runModeConfig) { if (runModeConfig) {
this.runConfig = JSON.parse(runModeConfig); this.runConfig = JSON.parse(runModeConfig);
this.runConfig.onSampleError = this.runConfig.onSampleError === 'true' || this.runConfig.onSampleError === true; this.runConfig.onSampleError =
this.runConfig.runWithinResourcePool = this.runConfig.runWithinResourcePool === 'true' || this.runConfig.runWithinResourcePool === true; this.runConfig.onSampleError === "true" ||
this.runConfig.onSampleError === true;
this.runConfig.runWithinResourcePool =
this.runConfig.runWithinResourcePool === "true" ||
this.runConfig.runWithinResourcePool === true;
} }
this.runModeVisible = true; this.runModeVisible = true;
this.testType = testType; this.testType = testType;
@ -285,21 +286,21 @@ export default {
}, },
query() { query() {
this.loading = true; this.loading = true;
this.result = getSystemBaseSetting().then(response => { this.result = getSystemBaseSetting().then((response) => {
if (!response.data.runMode) { if (!response.data.runMode) {
response.data.runMode = 'LOCAL' response.data.runMode = "LOCAL";
} }
this.runMode = response.data.runMode; this.runMode = response.data.runMode;
if (this.runMode === 'POOL') { if (this.runMode === "POOL") {
this.runConfig.runWithinResourcePool = true; this.runConfig.runWithinResourcePool = true;
this.getProjectApplication(); this.getProjectApplication();
} else { } else {
this.loading = false; this.loading = false;
} }
}) });
}, },
getProjectApplication() { getProjectApplication() {
getProjectConfig(getCurrentProjectID(), "").then(res => { getProjectConfig(getCurrentProjectID(), "").then((res) => {
if (res.data && res.data.poolEnable && res.data.resourcePoolId) { if (res.data && res.data.poolEnable && res.data.resourcePoolId) {
this.runConfig.resourcePoolId = res.data.resourcePoolId; this.runConfig.resourcePoolId = res.data.resourcePoolId;
} }
@ -329,10 +330,9 @@ export default {
this.close(); this.close();
}, },
getResourcePools() { getResourcePools() {
getQuotaValidResourcePools() getQuotaValidResourcePools().then((response) => {
.then((response) => { this.resourcePools = response.data;
this.resourcePools = response.data; });
});
}, },
setProjectEnvMap(projectEnvMap) { setProjectEnvMap(projectEnvMap) {
this.runConfig.envMap = strMapToObj(projectEnvMap); this.runConfig.envMap = strMapToObj(projectEnvMap);
@ -341,10 +341,9 @@ export default {
this.runConfig.environmentGroupId = id; this.runConfig.environmentGroupId = id;
}, },
getWsProjects() { getWsProjects() {
getOwnerProjects() getOwnerProjects().then((res) => {
.then((res) => { this.projectList = res.data;
this.projectList = res.data; });
});
}, },
showPopover() { showPopover() {
this.projectIds.clear(); this.projectIds.clear();
@ -374,7 +373,7 @@ export default {
this.$refs.envPopover.openEnvSelect(); this.$refs.envPopover.openEnvSelect();
}); });
} else if (this.type === "plan") { } else if (this.type === "plan") {
param = {id: this.planId}; param = { id: this.planId };
getPlanCaseEnv(param).then((res) => { getPlanCaseEnv(param).then((res) => {
let data = res.data; let data = res.data;
if (data) { if (data) {

View File

@ -3,32 +3,33 @@
destroy-on-close destroy-on-close
:title="$t('load_test.runtime_config')" :title="$t('load_test.runtime_config')"
width="550px" width="550px"
style="margin-top: -8.65vh;max-height: 87.3vh" style="margin-top: -8.65vh; max-height: 87.3vh"
@close="close" @close="close"
:visible.sync="runModeVisible" :visible.sync="runModeVisible"
> >
<div class="env-container"> <div class="env-container">
<div> <div>
<div>{{ $t("commons.environment") }}</div> <div>{{ $t("commons.environment") }}</div>
<env-select-popover :project-ids="projectIds" <env-select-popover
:project-list="projectList" :project-ids="projectIds"
:project-env-map="projectEnvListMap" :project-list="projectList"
:environment-type.sync="runConfig.environmentType" :project-env-map="projectEnvListMap"
:has-option-group="true" :environment-type.sync="runConfig.environmentType"
:group-id="runConfig.environmentGroupId" :has-option-group="true"
@setProjectEnvMap="setProjectEnvMap" :group-id="runConfig.environmentGroupId"
@setEnvGroup="setEnvGroup" @setProjectEnvMap="setProjectEnvMap"
ref="envSelectPopover" @setEnvGroup="setEnvGroup"
class="mode-row" ref="envSelectPopover"
class="mode-row"
></env-select-popover> ></env-select-popover>
</div> </div>
<div v-if="haveUICase"> <div v-if="haveUICase">
<div>{{ $t("ui.browser") }}</div> <div>{{ $t("ui.browser") }}</div>
<div > <div>
<el-select <el-select
size="mini" size="mini"
v-model="runConfig.browser" v-model="runConfig.browser"
style="width: 100% " style="width: 100%"
class="mode-row" class="mode-row"
> >
<el-option <el-option
@ -42,7 +43,7 @@
</div> </div>
<div> <div>
<div class="mode-row">{{ $t("run_mode.title") }}</div> <div class="mode-row">{{ $t("run_mode.title") }}</div>
<div > <div>
<el-radio-group <el-radio-group
v-model="runConfig.mode" v-model="runConfig.mode"
@change="changeMode" @change="changeMode"
@ -54,13 +55,17 @@
</el-radio-group> </el-radio-group>
</div> </div>
</div> </div>
<div > <div>
<div class="mode-row">{{ $t("run_mode.other_config") }}</div> <div class="mode-row">{{ $t("run_mode.other_config") }}</div>
<div > <div>
<!-- 串行 --> <!-- 串行 -->
<div <div
class="mode-row" class="mode-row"
v-if="runConfig.mode === 'serial' && testType === 'API' && haveOtherExecCase" v-if="
runConfig.mode === 'serial' &&
testType === 'API' &&
haveOtherExecCase
"
> >
<el-checkbox <el-checkbox
v-model="runConfig.runWithinResourcePool" v-model="runConfig.runWithinResourcePool"
@ -68,13 +73,13 @@
class="radio-change" class="radio-change"
:disabled="runMode === 'POOL'" :disabled="runMode === 'POOL'"
> >
{{ $t("run_mode.run_with_resource_pool") }} {{ $t("run_mode.run_with_resource_pool") }} </el-checkbox
</el-checkbox><br/> ><br />
<el-select <el-select
:disabled="!runConfig.runWithinResourcePool" :disabled="!runConfig.runWithinResourcePool"
v-model="runConfig.resourcePoolId" v-model="runConfig.resourcePoolId"
size="mini" size="mini"
style="width:100%; margin-top: 8px" style="width: 100%; margin-top: 8px"
> >
<el-option <el-option
v-for="item in resourcePools" v-for="item in resourcePools"
@ -88,7 +93,11 @@
<!-- 并行 --> <!-- 并行 -->
<div <div
class="mode-row" class="mode-row"
v-if="runConfig.mode === 'parallel' && testType === 'API' && haveOtherExecCase" v-if="
runConfig.mode === 'parallel' &&
testType === 'API' &&
haveOtherExecCase
"
> >
<el-checkbox <el-checkbox
v-model="runConfig.runWithinResourcePool" v-model="runConfig.runWithinResourcePool"
@ -96,13 +105,13 @@
class="radio-change" class="radio-change"
:disabled="runMode === 'POOL'" :disabled="runMode === 'POOL'"
> >
{{ $t("run_mode.run_with_resource_pool") }} {{ $t("run_mode.run_with_resource_pool") }} </el-checkbox
</el-checkbox><br/> ><br />
<el-select <el-select
:disabled="!runConfig.runWithinResourcePool" :disabled="!runConfig.runWithinResourcePool"
v-model="runConfig.resourcePoolId" v-model="runConfig.resourcePoolId"
size="mini" size="mini"
style="width:100%; margin-top: 8px" style="width: 100%; margin-top: 8px"
> >
<el-option <el-option
v-for="item in resourcePools" v-for="item in resourcePools"
@ -116,7 +125,7 @@
</div> </div>
<!-- 失败重试 --> <!-- 失败重试 -->
<div class="mode-row" v-if="isHasLicense"> <div class="mode-row">
<el-checkbox <el-checkbox
v-model="runConfig.retryEnable" v-model="runConfig.retryEnable"
class="radio-change ms-failure-div-right" class="radio-change ms-failure-div-right"
@ -126,8 +135,11 @@
<span v-if="runConfig.retryEnable"> <span v-if="runConfig.retryEnable">
<el-tooltip placement="top" style="margin: 0 4px 0 2px"> <el-tooltip placement="top" style="margin: 0 4px 0 2px">
<div slot="content">{{ $t("run_mode.retry_message") }}</div> <div slot="content">{{ $t("run_mode.retry_message") }}</div>
<i class="el-icon-question" style="cursor: pointer"/> <i
</el-tooltip><br/> class="el-icon-question"
style="cursor: pointer"
/> </el-tooltip
><br />
<span> <span>
{{ $t("run_mode.retry") }} {{ $t("run_mode.retry") }}
<el-input-number <el-input-number
@ -136,7 +148,7 @@
:min="1" :min="1"
:max="10000000" :max="10000000"
size="mini" size="mini"
style="width: 103px;margin-top: 8px" style="width: 103px; margin-top: 8px"
/> />
&nbsp; &nbsp;
{{ $t("run_mode.retry_frequency") }} {{ $t("run_mode.retry_frequency") }}
@ -145,14 +157,16 @@
</div> </div>
<div class="mode-row" v-if="runConfig.mode === 'serial'"> <div class="mode-row" v-if="runConfig.mode === 'serial'">
<el-checkbox v-model="runConfig.onSampleError" class="radio-change">{{ <el-checkbox v-model="runConfig.onSampleError" class="radio-change"
$t("api_test.fail_to_stop") >{{ $t("api_test.fail_to_stop") }}
}}
</el-checkbox> </el-checkbox>
</div> </div>
<div class="mode-row" v-if="haveUICase"> <div class="mode-row" v-if="haveUICase">
<el-checkbox v-model="runConfig.headlessEnabled" class="radio-change"> <el-checkbox
v-model="runConfig.headlessEnabled"
class="radio-change"
>
{{ $t("ui.performance_mode") }} {{ $t("ui.performance_mode") }}
</el-checkbox> </el-checkbox>
</div> </div>
@ -165,55 +179,64 @@
<el-button @click="close">{{ $t("commons.cancel") }}</el-button> <el-button @click="close">{{ $t("commons.cancel") }}</el-button>
<el-dropdown @command="handleCommand" style="margin-left: 5px"> <el-dropdown @command="handleCommand" style="margin-left: 5px">
<el-button type="primary"> <el-button type="primary">
{{ {{ $t("api_test.run")
$t("api_test.run")
}}<i class="el-icon-arrow-down el-icon--right"></i> }}<i class="el-icon-arrow-down el-icon--right"></i>
</el-button> </el-button>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
<el-dropdown-item command="run">{{ <el-dropdown-item command="run"
$t("api_test.run") >{{ $t("api_test.run") }}
}}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item command="runAndSave">{{ <el-dropdown-item command="runAndSave"
$t("load_test.save_and_run") >{{ $t("load_test.save_and_run") }}
}}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item command="save">{{ <el-dropdown-item command="save"
$t("commons.save") >{{ $t("commons.save") }}
}}
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
</div> </div>
<ms-dialog-footer v-else @cancel="close" @confirm="handleRunBatch"/> <ms-dialog-footer v-else @cancel="close" @confirm="handleRunBatch" />
</template> </template>
</el-dialog> </el-dialog>
</template> </template>
<script> <script>
import MsDialogFooter from "metersphere-frontend/src/components/MsDialogFooter"; import MsDialogFooter from "metersphere-frontend/src/components/MsDialogFooter";
import {hasLicense} from "metersphere-frontend/src/utils/permission"; import { strMapToObj } from "metersphere-frontend/src/utils";
import {strMapToObj} from "metersphere-frontend/src/utils";
import MsTag from "metersphere-frontend/src/components/MsTag"; import MsTag from "metersphere-frontend/src/components/MsTag";
import {ENV_TYPE} from "metersphere-frontend/src/utils/constants"; import { ENV_TYPE } from "metersphere-frontend/src/utils/constants";
import {getCurrentProjectID, getOwnerProjects} from "@/business/utils/sdk-utils"; import {
import {getQuotaValidResourcePools} from "@/api/remote/resource-pool"; getCurrentProjectID,
getOwnerProjects,
} from "@/business/utils/sdk-utils";
import { getQuotaValidResourcePools } from "@/api/remote/resource-pool";
import EnvGroupPopover from "@/business/plan/env/EnvGroupPopover"; import EnvGroupPopover from "@/business/plan/env/EnvGroupPopover";
import {getApiCaseEnv} from "@/api/remote/plan/test-plan-api-case"; import { getApiCaseEnv } from "@/api/remote/plan/test-plan-api-case";
import {getApiScenarioEnv, getPlanCaseEnv, getPlanCaseProjectIds} from "@/api/remote/plan/test-plan"; import {
getApiScenarioEnv,
getPlanCaseEnv,
getPlanCaseProjectIds,
} from "@/api/remote/plan/test-plan";
import EnvGroupWithOption from "../env/EnvGroupWithOption"; import EnvGroupWithOption from "../env/EnvGroupWithOption";
import EnvironmentGroup from "@/business/plan/env/EnvironmentGroupList"; import EnvironmentGroup from "@/business/plan/env/EnvironmentGroupList";
import EnvSelectPopover from "@/business/plan/env/EnvSelectPopover"; import EnvSelectPopover from "@/business/plan/env/EnvSelectPopover";
import {getSystemBaseSetting} from "metersphere-frontend/src/api/system"; import { getSystemBaseSetting } from "metersphere-frontend/src/api/system";
import {getProjectConfig} from "@/api/project"; import { getProjectConfig } from "@/api/project";
export default { export default {
name: "MsTestPlanRunModeWithEnv", name: "MsTestPlanRunModeWithEnv",
components: {EnvGroupPopover, MsDialogFooter,MsTag,EnvGroupWithOption,EnvironmentGroup,EnvSelectPopover}, components: {
EnvGroupPopover,
MsDialogFooter,
MsTag,
EnvGroupWithOption,
EnvironmentGroup,
EnvSelectPopover,
},
computed: { computed: {
ENV_TYPE() { ENV_TYPE() {
return ENV_TYPE; return ENV_TYPE;
} },
}, },
data() { data() {
return { return {
@ -221,7 +244,7 @@ export default {
btnStyle: { btnStyle: {
width: "260px", width: "260px",
}, },
result:{loading: false}, result: { loading: false },
runModeVisible: false, runModeVisible: false,
testType: null, testType: null,
resourcePools: [], resourcePools: [],
@ -239,7 +262,6 @@ export default {
retryNum: 1, retryNum: 1,
browser: "CHROME", browser: "CHROME",
}, },
isHasLicense: hasLicense(),
projectList: [], projectList: [],
projectIds: new Set(), projectIds: new Set(),
options: [ options: [
@ -290,8 +312,12 @@ export default {
if (runModeConfig) { if (runModeConfig) {
this.runConfig = JSON.parse(runModeConfig); this.runConfig = JSON.parse(runModeConfig);
this.runConfig.envMap = new Map(); this.runConfig.envMap = new Map();
this.runConfig.onSampleError = this.runConfig.onSampleError === 'true' || this.runConfig.onSampleError === true; this.runConfig.onSampleError =
this.runConfig.runWithinResourcePool = this.runConfig.runWithinResourcePool === 'true' || this.runConfig.runWithinResourcePool === true; this.runConfig.onSampleError === "true" ||
this.runConfig.onSampleError === true;
this.runConfig.runWithinResourcePool =
this.runConfig.runWithinResourcePool === "true" ||
this.runConfig.runWithinResourcePool === true;
} }
this.runModeVisible = true; this.runModeVisible = true;
this.testType = testType; this.testType = testType;
@ -302,21 +328,21 @@ export default {
}, },
query() { query() {
this.loading = true; this.loading = true;
this.result = getSystemBaseSetting().then(response => { this.result = getSystemBaseSetting().then((response) => {
if (!response.data.runMode) { if (!response.data.runMode) {
response.data.runMode = 'LOCAL' response.data.runMode = "LOCAL";
} }
this.runMode = response.data.runMode; this.runMode = response.data.runMode;
if (this.runMode === 'POOL') { if (this.runMode === "POOL") {
this.runConfig.runWithinResourcePool = true; this.runConfig.runWithinResourcePool = true;
this.getProjectApplication(); this.getProjectApplication();
} else { } else {
this.loading = false; this.loading = false;
} }
}) });
}, },
getProjectApplication() { getProjectApplication() {
getProjectConfig(getCurrentProjectID(), "").then(res => { getProjectConfig(getCurrentProjectID(), "").then((res) => {
if (res.data && res.data.poolEnable && res.data.resourcePoolId) { if (res.data && res.data.poolEnable && res.data.resourcePoolId) {
this.runConfig.resourcePoolId = res.data.resourcePoolId; this.runConfig.resourcePoolId = res.data.resourcePoolId;
} }
@ -346,10 +372,9 @@ export default {
this.close(); this.close();
}, },
getResourcePools() { getResourcePools() {
getQuotaValidResourcePools() getQuotaValidResourcePools().then((response) => {
.then((response) => { this.resourcePools = response.data;
this.resourcePools = response.data; });
});
}, },
setProjectEnvMap(projectEnvMap) { setProjectEnvMap(projectEnvMap) {
this.runConfig.envMap = projectEnvMap; this.runConfig.envMap = projectEnvMap;
@ -358,10 +383,9 @@ export default {
this.runConfig.environmentGroupId = id; this.runConfig.environmentGroupId = id;
}, },
getWsProjects() { getWsProjects() {
getOwnerProjects() getOwnerProjects().then((res) => {
.then((res) => { this.projectList = res.data;
this.projectList = res.data; });
});
}, },
showPopover() { showPopover() {
this.projectIds.clear(); this.projectIds.clear();
@ -391,7 +415,7 @@ export default {
this.$refs.envSelectPopover.open(); this.$refs.envSelectPopover.open();
}); });
} else if (this.type === "plan") { } else if (this.type === "plan") {
param = {id: this.planId}; param = { id: this.planId };
getPlanCaseEnv(param).then((res) => { getPlanCaseEnv(param).then((res) => {
let data = res.data; let data = res.data;
if (data) { if (data) {
@ -401,7 +425,7 @@ export default {
} }
} }
if (this.projectIds.size === 0) { if (this.projectIds.size === 0) {
param = {id: this.planId}; param = { id: this.planId };
getPlanCaseProjectIds(param).then((res) => { getPlanCaseProjectIds(param).then((res) => {
let data = res.data; let data = res.data;
if (data) { if (data) {
@ -416,28 +440,27 @@ export default {
} }
}); });
} }
}, },
handleCommand(command) { handleCommand(command) {
if ( if (
this.runConfig.runWithinResourcePool && this.runConfig.runWithinResourcePool &&
this.runConfig.resourcePoolId == null && this.haveOtherExecCase this.runConfig.resourcePoolId == null &&
this.haveOtherExecCase
) { ) {
this.$warning( this.$warning(
this.$t("workspace.env_group.please_select_run_within_resource_pool") this.$t("workspace.env_group.please_select_run_within_resource_pool")
); );
return; return;
} }
this.runConfig.envMap =strMapToObj(this.runConfig.envMap) this.runConfig.envMap = strMapToObj(this.runConfig.envMap);
if (command === "runAndSave") { if (command === "runAndSave") {
this.runConfig.executionWay = "runAndSave"; this.runConfig.executionWay = "runAndSave";
} else if(command === "save"){ } else if (command === "save") {
this.runConfig.executionWay = "save"; this.runConfig.executionWay = "save";
} else { } else {
this.runConfig.executionWay = "run"; this.runConfig.executionWay = "run";
} }
this.handleRunBatch(); this.handleRunBatch();
}, },
}, },
}; };
@ -461,13 +484,12 @@ export default {
.mode-row { .mode-row {
margin-top: 8px; margin-top: 8px;
} }
</style> </style>
<style lang="scss" scoped> <style lang="scss" scoped>
.radio-change:deep(.el-radio__input.is-checked + .el-radio__label) { .radio-change:deep(.el-radio__input.is-checked + .el-radio__label) {
color: #606266 !important; color: #606266 !important;
} }
.radio-change:deep(.el-checkbox__input.is-checked+.el-checkbox__label) { .radio-change:deep(.el-checkbox__input.is-checked + .el-checkbox__label) {
color: #606266 !important; color: #606266 !important;
} }
</style> </style>

View File

@ -1,60 +1,97 @@
<template> <template>
<el-dialog <el-dialog
v-loading="result.loading" v-loading="result.loading"
:close-on-click-modal="false" width="60%" class="schedule-edit" :visible.sync="dialogVisible" :close-on-click-modal="false"
:append-to-body='true' width="60%"
@close="close"> class="schedule-edit"
:visible.sync="dialogVisible"
:append-to-body="true"
@close="close"
>
<template> <template>
<div> <div>
<el-tabs v-model="activeName"> <el-tabs v-model="activeName">
<el-tab-pane :label="$t('schedule.task_config')" name="first"> <el-tab-pane :label="$t('schedule.task_config')" name="first">
<div class="el-step__icon is-text" style="margin-right: 10px;"> <div class="el-step__icon is-text" style="margin-right: 10px">
<div class="el-step__icon-inner">1</div> <div class="el-step__icon-inner">1</div>
</div> </div>
<span>{{ $t('schedule.edit_timer_task') }}</span> <span>{{ $t("schedule.edit_timer_task") }}</span>
<el-form :model="form" :rules="rules" ref="from" style="padding-top: 10px;margin-left: 20px;" <el-form
class="ms-el-form-item__error"> :model="form"
<el-form-item :label="$t('commons.schedule_cron_title')" :rules="rules"
prop="cronValue" style="height: 50px"> ref="from"
style="padding-top: 10px; margin-left: 20px"
class="ms-el-form-item__error"
>
<el-form-item
:label="$t('commons.schedule_cron_title')"
prop="cronValue"
style="height: 50px"
>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="16"> <el-col :span="16">
<el-input :disabled="isReadOnly" v-model="form.cronValue" class="inp" <el-input
:placeholder="$t('schedule.please_input_cron_expression')" size="mini"> :disabled="isReadOnly"
<a :disabled="isReadOnly" type="primary" @click="showCronDialog" slot="suffix" class="head"> v-model="form.cronValue"
{{ $t('schedule.generate_expression') }} class="inp"
:placeholder="$t('schedule.please_input_cron_expression')"
size="mini"
>
<a
:disabled="isReadOnly"
type="primary"
@click="showCronDialog"
slot="suffix"
class="head"
>
{{ $t("schedule.generate_expression") }}
</a> </a>
</el-input> </el-input>
<span>{{ this.$t('commons.schedule_switch') }}</span> <span>{{ this.$t("commons.schedule_switch") }}</span>
<el-tooltip effect="dark" placement="bottom" <el-tooltip
:content="schedule.enable ? $t('commons.close_schedule') : $t('commons.open_schedule')"> effect="dark"
<el-switch v-model="schedule.enable" style="margin-left: 20px"></el-switch> placement="bottom"
:content="
schedule.enable
? $t('commons.close_schedule')
: $t('commons.open_schedule')
"
>
<el-switch
v-model="schedule.enable"
style="margin-left: 20px"
></el-switch>
</el-tooltip> </el-tooltip>
</el-col> </el-col>
<el-col :span="2"> <el-col :span="2">
<el-button :disabled="isReadOnly" type="primary" @click="saveCron" size="mini">{{ <el-button
$t('commons.save') :disabled="isReadOnly"
}} type="primary"
@click="saveCron"
size="mini"
>{{ $t("commons.save") }}
</el-button> </el-button>
</el-col> </el-col>
</el-row> </el-row>
</el-form-item> </el-form-item>
<crontab-result :ex="form.cronValue" ref="crontabResult"/> <crontab-result :ex="form.cronValue" ref="crontabResult" />
</el-form> </el-form>
<div class="el-step__icon is-text" style="margin-right: 10px;"> <div class="el-step__icon is-text" style="margin-right: 10px">
<div class="el-step__icon-inner">2</div> <div class="el-step__icon-inner">2</div>
</div> </div>
<span>{{ $t('load_test.runtime_config') }}</span> <span>{{ $t("load_test.runtime_config") }}</span>
<div class="ms-mode-div"> <div class="ms-mode-div">
<span class="ms-mode-span">{{ $t("run_mode.title") }}</span> <span class="ms-mode-span">{{ $t("run_mode.title") }}</span>
<el-radio-group v-model="runConfig.mode" @change="changeMode"> <el-radio-group v-model="runConfig.mode" @change="changeMode">
<el-radio label="serial">{{ $t("run_mode.serial") }}</el-radio> <el-radio label="serial">{{ $t("run_mode.serial") }}</el-radio>
<el-radio label="parallel">{{ $t("run_mode.parallel") }}</el-radio> <el-radio label="parallel">{{
$t("run_mode.parallel")
}}</el-radio>
</el-radio-group> </el-radio-group>
</div> </div>
<div style="margin-top: 10px;" v-if="haveUICase"> <div style="margin-top: 10px" v-if="haveUICase">
<span class="ms-mode-span">{{ $t("浏览器") }}</span> <span class="ms-mode-span">{{ $t("浏览器") }}</span>
<el-select <el-select
size="mini" size="mini"
@ -72,20 +109,30 @@
<div class="ms-mode-div" v-if="runConfig.mode === 'serial'"> <div class="ms-mode-div" v-if="runConfig.mode === 'serial'">
<el-row> <el-row>
<el-col :span="3"> <el-col :span="3">
<span class="ms-mode-span">{{ $t("run_mode.other_config") }}</span> <span class="ms-mode-span"
>{{ $t("run_mode.other_config") }}</span
>
</el-col> </el-col>
<el-col :span="18"> <el-col :span="18">
<div v-if="testType === 'API'"> <div v-if="testType === 'API'">
<el-checkbox v-model="runConfig.runWithinResourcePool" style="padding-right: 10px;" :disabled="runMode === 'POOL'"> <el-checkbox
{{ $t('run_mode.run_with_resource_pool') }} v-model="runConfig.runWithinResourcePool"
style="padding-right: 10px"
:disabled="runMode === 'POOL'"
>
{{ $t("run_mode.run_with_resource_pool") }}
</el-checkbox> </el-checkbox>
<el-select :disabled="!runConfig.runWithinResourcePool" v-model="runConfig.resourcePoolId" <el-select
size="mini"> :disabled="!runConfig.runWithinResourcePool"
v-model="runConfig.resourcePoolId"
size="mini"
>
<el-option <el-option
v-for="item in resourcePools" v-for="item in resourcePools"
:key="item.id" :key="item.id"
:label="item.name" :label="item.name"
:value="item.id"> :value="item.id"
>
</el-option> </el-option>
</el-select> </el-select>
</div> </div>
@ -95,21 +142,31 @@
<div class="ms-mode-div" v-if="runConfig.mode === 'parallel'"> <div class="ms-mode-div" v-if="runConfig.mode === 'parallel'">
<el-row> <el-row>
<el-col :span="3"> <el-col :span="3">
<span class="ms-mode-span">{{ $t("run_mode.other_config") }}</span> <span class="ms-mode-span"
>{{ $t("run_mode.other_config") }}</span
>
</el-col> </el-col>
<el-col :span="18"> <el-col :span="18">
<div v-if="testType === 'API'"> <div v-if="testType === 'API'">
<el-checkbox v-model="runConfig.runWithinResourcePool" style="padding-right: 10px;" :disabled="runMode === 'POOL'"> <el-checkbox
{{ $t('run_mode.run_with_resource_pool') }} v-model="runConfig.runWithinResourcePool"
style="padding-right: 10px"
:disabled="runMode === 'POOL'"
>
{{ $t("run_mode.run_with_resource_pool") }}
</el-checkbox> </el-checkbox>
<el-select :disabled="!runConfig.runWithinResourcePool" v-model="runConfig.resourcePoolId" <el-select
size="mini"> :disabled="!runConfig.runWithinResourcePool"
v-model="runConfig.resourcePoolId"
size="mini"
>
<el-option <el-option
v-for="item in resourcePools" v-for="item in resourcePools"
:key="item.id" :key="item.id"
:label="item.name" :label="item.name"
:disabled="!item.api" :disabled="!item.api"
:value="item.id"> :value="item.id"
>
</el-option> </el-option>
</el-select> </el-select>
</div> </div>
@ -118,45 +175,55 @@
</div> </div>
<!-- 失败重试 --> <!-- 失败重试 -->
<div class="ms-mode-div" v-if="isHasLicense"> <div class="ms-mode-div">
<el-row> <el-row>
<el-col :span="3"> <el-col :span="3">
<span class="ms-mode-span">&nbsp;</span> <span class="ms-mode-span">&nbsp;</span>
</el-col> </el-col>
<el-col :span="18"> <el-col :span="18">
<el-checkbox v-model="runConfig.retryEnable" class="ms-failure-div-right"> <el-checkbox
{{ $t('run_mode.retry_on_failure') }} v-model="runConfig.retryEnable"
class="ms-failure-div-right"
>
{{ $t("run_mode.retry_on_failure") }}
</el-checkbox> </el-checkbox>
<span v-if="runConfig.retryEnable"> <span v-if="runConfig.retryEnable">
<el-tooltip placement="top"> <el-tooltip placement="top">
<div slot="content">{{ $t('run_mode.retry_message') }}</div> <div slot="content">
<i class="el-icon-question" style="cursor: pointer"/> {{ $t("run_mode.retry_message") }}
</el-tooltip> </div>
<span> <i class="el-icon-question" style="cursor: pointer" />
{{ $t('run_mode.retry') }} </el-tooltip>
<el-input-number :value="runConfig.retryNum" v-model="runConfig.retryNum" :min="1" :max="10000000" <span>
size="mini"/> {{ $t("run_mode.retry") }}
&nbsp; <el-input-number
{{ $t('run_mode.retry_frequency') }} :value="runConfig.retryNum"
</span> v-model="runConfig.retryNum"
:min="1"
:max="10000000"
size="mini"
/>
&nbsp;
{{ $t("run_mode.retry_frequency") }}
</span>
</span> </span>
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
<div class="ms-failure-div" v-if="runConfig.mode === 'serial'" > <div class="ms-failure-div" v-if="runConfig.mode === 'serial'">
<el-row> <el-row>
<el-col :span="18" :offset="3"> <el-col :span="18" :offset="3">
<div> <div>
<el-checkbox v-model="runConfig.onSampleError">{{ $t("api_test.fail_to_stop") }}</el-checkbox> <el-checkbox v-model="runConfig.onSampleError">{{
$t("api_test.fail_to_stop")
}}</el-checkbox>
</div> </div>
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
<div v-if="haveUICase"> <div v-if="haveUICase">
<el-row> <el-row>
<el-col :span="3"> <el-col :span="3"> &nbsp; </el-col>
&nbsp;
</el-col>
<el-col :span="18"> <el-col :span="18">
<div style="margin-top: 10px"> <div style="margin-top: 10px">
<el-checkbox v-model="runConfig.headlessEnabled"> <el-checkbox v-model="runConfig.headlessEnabled">
@ -167,15 +234,25 @@
</el-row> </el-row>
</div> </div>
<el-dialog width="60%" :title="$t('schedule.generate_expression')" :visible.sync="showCron" <el-dialog
:modal="false"> width="60%"
<crontab @hide="showCron=false" @fill="crontabFill" :expression="schedule.value" :title="$t('schedule.generate_expression')"
ref="crontab"/> :visible.sync="showCron"
:modal="false"
>
<crontab
@hide="showCron = false"
@fill="crontabFill"
:expression="schedule.value"
ref="crontab"
/>
</el-dialog> </el-dialog>
</el-tab-pane> </el-tab-pane>
<el-tab-pane :label="$t('schedule.task_notification')" name="second"> <el-tab-pane :label="$t('schedule.task_notification')" name="second">
<ms-schedule-notification :test-id="testId" <ms-schedule-notification
:schedule-receiver-options="scheduleReceiverOptions"/> :test-id="testId"
:schedule-receiver-options="scheduleReceiverOptions"
/>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</div> </div>
@ -184,30 +261,36 @@
</template> </template>
<script> <script>
import {getCurrentProjectID, getCurrentUser, getCurrentWorkspaceId} from "metersphere-frontend/src/utils/token"; import {
import {hasLicense} from "metersphere-frontend/src/utils/permission"; getCurrentProjectID,
import {listenGoBack, removeGoBackListener} from "metersphere-frontend/src/utils"; getCurrentUser,
getCurrentWorkspaceId,
} from "metersphere-frontend/src/utils/token";
import {
listenGoBack,
removeGoBackListener,
} from "metersphere-frontend/src/utils";
import Crontab from "metersphere-frontend/src/components/cron/Crontab"; import Crontab from "metersphere-frontend/src/components/cron/Crontab";
import CrontabResult from "metersphere-frontend/src/components/cron/CrontabResult"; import CrontabResult from "metersphere-frontend/src/components/cron/CrontabResult";
import {cronValidate} from "metersphere-frontend/src/utils/cron"; import { cronValidate } from "metersphere-frontend/src/utils/cron";
import MsScheduleNotification from "./ScheduleNotification"; import MsScheduleNotification from "./ScheduleNotification";
import ScheduleSwitch from "./ScheduleSwitch"; import ScheduleSwitch from "./ScheduleSwitch";
import {ENV_TYPE} from "metersphere-frontend/src/utils/constants"; import { ENV_TYPE } from "metersphere-frontend/src/utils/constants";
import MxNotification from "metersphere-frontend/src/components/MxNoticeTemplate"; import MxNotification from "metersphere-frontend/src/components/MxNoticeTemplate";
import { import {
createSchedule, createSchedule,
getPlanSchedule, getPlanSchedule,
updateSchedule, updateSchedule,
updateScheduleEnableByPrimyKey updateScheduleEnableByPrimyKey,
} from "@/api/remote/plan/test-plan"; } from "@/api/remote/plan/test-plan";
import {saveNotice} from "@/api/notice"; import { saveNotice } from "@/api/notice";
import {getProjectMember} from "@/api/user"; import { getProjectMember } from "@/api/user";
import {getQuotaValidResourcePools} from "@/api/remote/resource-pool"; import { getQuotaValidResourcePools } from "@/api/remote/resource-pool";
import {getProjectConfig} from "@/api/project"; import { getProjectConfig } from "@/api/project";
import {getSystemBaseSetting} from "metersphere-frontend/src/api/system"; import { getSystemBaseSetting } from "metersphere-frontend/src/api/system";
function defaultCustomValidate() { function defaultCustomValidate() {
return {pass: true}; return { pass: true };
} }
export default { export default {
@ -217,47 +300,46 @@ export default {
ScheduleSwitch, ScheduleSwitch,
Crontab, Crontab,
MsScheduleNotification, MsScheduleNotification,
"NoticeTemplate": MxNotification, NoticeTemplate: MxNotification,
}, },
props: { props: {
customValidate: { customValidate: {
type: Function, type: Function,
default: defaultCustomValidate default: defaultCustomValidate,
}, },
isReadOnly: { isReadOnly: {
type: Boolean, type: Boolean,
default: false default: false,
}, },
planCaseIds: [], planCaseIds: [],
type: String, type: String,
//ui ui //ui ui
haveUICase: { haveUICase: {
type: Boolean, type: Boolean,
default: false default: false,
} },
}, },
watch: { watch: {
'schedule.value'() { "schedule.value"() {
this.form.cronValue = this.schedule.value; this.form.cronValue = this.schedule.value;
}, },
'runConfig.runWithinResourcePool'() { "runConfig.runWithinResourcePool"() {
if (!this.runConfig.runWithinResourcePool) { if (!this.runConfig.runWithinResourcePool) {
this.runConfig.resourcePoolId = null; this.runConfig.resourcePoolId = null;
} }
} },
}, },
data() { data() {
const validateCron = (rule, cronValue, callback) => { const validateCron = (rule, cronValue, callback) => {
let customValidate = this.customValidate(this.getIntervalTime()); let customValidate = this.customValidate(this.getIntervalTime());
if (!cronValue) { if (!cronValue) {
callback(new Error(this.$t('commons.input_content'))); callback(new Error(this.$t("commons.input_content")));
} else if (!cronValidate(cronValue)) { } else if (!cronValidate(cronValue)) {
callback(new Error(this.$t('schedule.cron_expression_format_error'))); callback(new Error(this.$t("schedule.cron_expression_format_error")));
} else if (!this.intervalValidate()) { } else if (!this.intervalValidate()) {
callback(new Error(this.$t('schedule.cron_expression_interval_error'))); callback(new Error(this.$t("schedule.cron_expression_interval_error")));
} else if (!customValidate.pass) { } else if (!customValidate.pass) {
callback(new Error(customValidate.info)); callback(new Error(customValidate.info));
} else { } else {
@ -269,7 +351,6 @@ export default {
}; };
return { return {
runMode: "", runMode: "",
isHasLicense: hasLicense(),
result: {}, result: {},
scheduleReceiverOptions: [], scheduleReceiverOptions: [],
operation: true, operation: true,
@ -281,12 +362,14 @@ export default {
testId: String, testId: String,
showCron: false, showCron: false,
form: { form: {
cronValue: "" cronValue: "",
}, },
paramRow: {}, paramRow: {},
activeName: 'first', activeName: "first",
rules: { rules: {
cronValue: [{required: true, validator: validateCron, trigger: 'blur'}], cronValue: [
{ required: true, validator: validateCron, trigger: "blur" },
],
}, },
resourcePools: [], resourcePools: [],
runConfig: { runConfig: {
@ -298,10 +381,10 @@ export default {
retryEnable: false, retryEnable: false,
retryNum: 1, retryNum: 1,
browser: "CHROME", browser: "CHROME",
headlessEnabled: true headlessEnabled: true,
}, },
projectList: [], projectList: [],
testType: 'API', testType: "API",
planId: String, planId: String,
projectIds: new Set(), projectIds: new Set(),
browsers: [ browsers: [
@ -312,28 +395,28 @@ export default {
{ {
label: this.$t("firefox"), label: this.$t("firefox"),
value: "FIREFOX", value: "FIREFOX",
} },
], ],
}; };
}, },
methods: { methods: {
query() { query() {
this.loading = true; this.loading = true;
this.result = getSystemBaseSetting().then(response => { this.result = getSystemBaseSetting().then((response) => {
if (!response.data.runMode) { if (!response.data.runMode) {
response.data.runMode = 'LOCAL' response.data.runMode = "LOCAL";
} }
this.runMode = response.data.runMode; this.runMode = response.data.runMode;
if (this.runMode === 'POOL') { if (this.runMode === "POOL") {
this.runConfig.runWithinResourcePool = true; this.runConfig.runWithinResourcePool = true;
this.getProjectApplication(); this.getProjectApplication();
} else { } else {
this.loading = false; this.loading = false;
} }
}) });
}, },
getProjectApplication() { getProjectApplication() {
getProjectConfig(getCurrentProjectID(), "").then(res => { getProjectConfig(getCurrentProjectID(), "").then((res) => {
if (res.data && res.data.poolEnable && res.data.resourcePoolId) { if (res.data && res.data.poolEnable && res.data.resourcePoolId) {
this.runConfig.resourcePoolId = res.data.resourcePoolId; this.runConfig.resourcePoolId = res.data.resourcePoolId;
} }
@ -350,14 +433,14 @@ export default {
return true; return true;
}, },
updateTask(param) { updateTask(param) {
this.result = updateScheduleEnableByPrimyKey(param).then(response => { this.result = updateScheduleEnableByPrimyKey(param).then((response) => {
this.taskID = this.paramRow.id; this.taskID = this.paramRow.id;
this.findSchedule(this.taskID); this.findSchedule(this.taskID);
this.$emit("refreshTable"); this.$emit("refreshTable");
}); });
}, },
initUserList() { initUserList() {
this.result = getProjectMember().then(response => { this.result = getProjectMember().then((response) => {
this.scheduleReceiverOptions = response.data; this.scheduleReceiverOptions = response.data;
}); });
}, },
@ -379,7 +462,7 @@ export default {
this.dialogVisible = true; this.dialogVisible = true;
this.form.cronValue = this.schedule.value; this.form.cronValue = this.schedule.value;
listenGoBack(this.close); listenGoBack(this.close);
this.activeName = 'first'; this.activeName = "first";
this.getResourcePools(); this.getResourcePools();
this.runConfig.environmentType = ENV_TYPE.JSON; this.runConfig.environmentType = ENV_TYPE.JSON;
this.runConfig.retryEnable = false; this.runConfig.retryEnable = false;
@ -387,22 +470,24 @@ export default {
}, },
findSchedule() { findSchedule() {
let scheduleResourceID = this.testId; let scheduleResourceID = this.testId;
this.result = getPlanSchedule(scheduleResourceID,"TEST_PLAN_TEST").then(response => { this.result = getPlanSchedule(scheduleResourceID, "TEST_PLAN_TEST").then(
if (response.data != null) { (response) => {
this.schedule = response.data; if (response.data != null) {
if (response.data.config) { this.schedule = response.data;
this.runConfig = JSON.parse(response.data.config); if (response.data.config) {
if (this.runConfig.environmentType) { this.runConfig = JSON.parse(response.data.config);
delete this.runConfig.environmentType; if (this.runConfig.environmentType) {
delete this.runConfig.environmentType;
}
} }
} else {
this.schedule = {
value: "",
enable: false,
};
} }
} else {
this.schedule = {
value: '',
enable: false
};
} }
}); );
}, },
crontabFill(value, resultList) { crontabFill(value, resultList) {
// //
@ -412,22 +497,27 @@ export default {
this.schedule.enable = true; this.schedule.enable = true;
} }
this.$refs.crontabResult.resultList = resultList; this.$refs.crontabResult.resultList = resultList;
this.$refs['from'].validate(); this.$refs["from"].validate();
}, },
showCronDialog() { showCronDialog() {
let tmp = this.schedule.value; let tmp = this.schedule.value;
this.schedule.value = ''; this.schedule.value = "";
this.$nextTick(() => { this.$nextTick(() => {
this.schedule.value = tmp; this.schedule.value = tmp;
this.showCron = true; this.showCron = true;
}); });
}, },
saveCron() { saveCron() {
if (this.runConfig.runWithinResourcePool && this.runConfig.resourcePoolId == null) { if (
this.$warning(this.$t('workspace.env_group.please_select_run_within_resource_pool')); this.runConfig.runWithinResourcePool &&
this.runConfig.resourcePoolId == null
) {
this.$warning(
this.$t("workspace.env_group.please_select_run_within_resource_pool")
);
return; return;
} }
this.$refs['from'].validate((valid) => { this.$refs["from"].validate((valid) => {
if (valid) { if (valid) {
this.intervalShortValidate(); this.intervalShortValidate();
let formCronValue = this.form.cronValue; let formCronValue = this.form.cronValue;
@ -452,8 +542,13 @@ export default {
if (!param.workspaceId) { if (!param.workspaceId) {
param.workspaceId = getCurrentWorkspaceId(); param.workspaceId = getCurrentWorkspaceId();
} }
if (this.runConfig.runWithinResourcePool && this.runConfig.resourcePoolId == null) { if (
this.$warning(this.$t('workspace.env_group.please_select_run_within_resource_pool')); this.runConfig.runWithinResourcePool &&
this.runConfig.resourcePoolId == null
) {
this.$warning(
this.$t("workspace.env_group.please_select_run_within_resource_pool")
);
return; return;
} }
param.config = JSON.stringify(this.runConfig); param.config = JSON.stringify(this.runConfig);
@ -462,12 +557,12 @@ export default {
// //
if (param.id) { if (param.id) {
updateSchedule(param).then(() => { updateSchedule(param).then(() => {
this.$success(this.$t('commons.save_success')); this.$success(this.$t("commons.save_success"));
this.$emit("refreshTable"); this.$emit("refreshTable");
}); });
} else { } else {
createSchedule(param).then(() => { createSchedule(param).then(() => {
this.$success(this.$t('commons.save_success')); this.$success(this.$t("commons.save_success"));
this.$emit("refreshTable"); this.$emit("refreshTable");
}); });
} }
@ -475,7 +570,7 @@ export default {
}, },
checkScheduleEdit() { checkScheduleEdit() {
if (this.create) { if (this.create) {
this.$message(this.$t('api_test.environment.please_save_test')); this.$message(this.$t("api_test.environment.please_save_test"));
return false; return false;
} }
return true; return true;
@ -483,13 +578,13 @@ export default {
saveNotice() { saveNotice() {
let param = this.buildParam(); let param = this.buildParam();
saveNotice(param).then(() => { saveNotice(param).then(() => {
this.$success(this.$t('commons.save_success')); this.$success(this.$t("commons.save_success"));
}); });
}, },
close() { close() {
this.dialogVisible = false; this.dialogVisible = false;
this.form.cronValue = ''; this.form.cronValue = "";
this.$refs['from'].resetFields(); this.$refs["from"].resetFields();
if (!this.schedule.value) { if (!this.schedule.value) {
this.$refs.crontabResult.resultList = []; this.$refs.crontabResult.resultList = [];
} }
@ -498,12 +593,12 @@ export default {
intervalShortValidate() { intervalShortValidate() {
if (this.schedule.enable && this.getIntervalTime() < 3 * 60 * 1000) { if (this.schedule.enable && this.getIntervalTime() < 3 * 60 * 1000) {
// return false; // return false;
this.$info(this.$t('schedule.cron_expression_interval_short_error')); this.$info(this.$t("schedule.cron_expression_interval_short_error"));
} }
return true; return true;
}, },
resultListChange() { resultListChange() {
this.$refs['from'].validate(); this.$refs["from"].validate();
}, },
getIntervalTime() { getIntervalTime() {
let resultList = this.$refs.crontabResult.resultList; let resultList = this.$refs.crontabResult.resultList;
@ -515,7 +610,7 @@ export default {
alert(executeTileArr); alert(executeTileArr);
}, },
getResourcePools() { getResourcePools() {
this.result = getQuotaValidResourcePools().then(response => { this.result = getQuotaValidResourcePools().then((response) => {
this.resourcePools = response.data; this.resourcePools = response.data;
}); });
}, },
@ -528,13 +623,12 @@ export default {
computed: { computed: {
isTesterPermission() { isTesterPermission() {
return true; return true;
} },
} },
}; };
</script> </script>
<style scoped> <style scoped>
.inp { .inp {
width: 40%; width: 40%;
margin-right: 20px; margin-right: 20px;
@ -560,7 +654,8 @@ export default {
.head { .head {
border-bottom: 1px solid var(--primary_color); border-bottom: 1px solid var(--primary_color);
color: var(--primary_color); color: var(--primary_color);
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", Arial, sans-serif; font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
Arial, sans-serif;
font-size: 13px; font-size: 13px;
cursor: pointer; cursor: pointer;
} }

View File

@ -117,7 +117,7 @@
</div> </div>
<!-- 失败重试 --> <!-- 失败重试 -->
<div class="mode-row" v-if="isHasLicense"> <div class="mode-row">
<el-checkbox <el-checkbox
v-model="runConfig.retryEnable" v-model="runConfig.retryEnable"
class="ms-failure-div-right" class="ms-failure-div-right"
@ -127,7 +127,7 @@
<span v-if="runConfig.retryEnable"> <span v-if="runConfig.retryEnable">
<el-tooltip placement="top" style="margin: 0 4px 0 2px"> <el-tooltip placement="top" style="margin: 0 4px 0 2px">
<div slot="content">{{ $t("run_mode.retry_message") }}</div> <div slot="content">{{ $t("run_mode.retry_message") }}</div>
<i class="el-icon-question" style="cursor: pointer"/> <i class="el-icon-question" style="cursor: pointer" />
</el-tooltip> </el-tooltip>
<span style="margin-left: 10px"> <span style="margin-left: 10px">
{{ $t("run_mode.retry") }} {{ $t("run_mode.retry") }}
@ -146,9 +146,8 @@
</div> </div>
<div class="mode-row" v-if="runConfig.mode === 'serial'"> <div class="mode-row" v-if="runConfig.mode === 'serial'">
<el-checkbox v-model="runConfig.onSampleError">{{ <el-checkbox v-model="runConfig.onSampleError"
$t("api_test.fail_to_stop") >{{ $t("api_test.fail_to_stop") }}
}}
</el-checkbox> </el-checkbox>
</div> </div>
@ -166,38 +165,38 @@
<el-button @click="close">{{ $t("commons.cancel") }}</el-button> <el-button @click="close">{{ $t("commons.cancel") }}</el-button>
<el-dropdown @command="handleCommand" style="margin-left: 5px"> <el-dropdown @command="handleCommand" style="margin-left: 5px">
<el-button type="primary"> <el-button type="primary">
{{ {{ $t("load_test.save_and_run")
$t("load_test.save_and_run")
}}<i class="el-icon-arrow-down el-icon--right"></i> }}<i class="el-icon-arrow-down el-icon--right"></i>
</el-button> </el-button>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
<el-dropdown-item command="run">{{ <el-dropdown-item command="run"
$t("load_test.save_and_run") >{{ $t("load_test.save_and_run") }}
}}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item command="save">{{ <el-dropdown-item command="save"
$t("commons.save") >{{ $t("commons.save") }}
}}
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
</div> </div>
<ms-dialog-footer v-else @cancel="close" @confirm="handleRunBatch"/> <ms-dialog-footer v-else @cancel="close" @confirm="handleRunBatch" />
</template> </template>
</el-dialog> </el-dialog>
</template> </template>
<script> <script>
import {getCurrentProjectID, getOwnerProjects, hasLicense} from "@/business/utils/sdk-utils"; import {
import {BODY_TYPE as ENV_TYPE} from "@/business/plan/env/ApiTestModel"; getCurrentProjectID,
getOwnerProjects,
} from "@/business/utils/sdk-utils";
import { BODY_TYPE as ENV_TYPE } from "@/business/plan/env/ApiTestModel";
import MsDialogFooter from "metersphere-frontend/src/components/MsDialogFooter"; import MsDialogFooter from "metersphere-frontend/src/components/MsDialogFooter";
import EnvPopover from "@/business/plan/env/EnvPopover"; import EnvPopover from "@/business/plan/env/EnvPopover";
import {getProjectConfig} from "@/api/project"; import { getProjectConfig } from "@/api/project";
import {getSystemBaseSetting} from "metersphere-frontend/src/api/system"; import { getSystemBaseSetting } from "metersphere-frontend/src/api/system";
export default { export default {
name: "MsPlanRunModeWithEnv", name: "MsPlanRunModeWithEnv",
components: {EnvPopover, MsDialogFooter}, components: { EnvPopover, MsDialogFooter },
data() { data() {
return { return {
runMode: "", runMode: "",
@ -220,7 +219,6 @@ export default {
retryNum: 1, retryNum: 1,
browser: "CHROME", browser: "CHROME",
}, },
isHasLicense: hasLicense(),
projectEnvListMap: {}, projectEnvListMap: {},
projectList: [], projectList: [],
projectIds: new Set(), projectIds: new Set(),
@ -267,8 +265,12 @@ export default {
open(testType, runModeConfig) { open(testType, runModeConfig) {
if (runModeConfig) { if (runModeConfig) {
this.runConfig = JSON.parse(runModeConfig); this.runConfig = JSON.parse(runModeConfig);
this.runConfig.onSampleError = this.runConfig.onSampleError === 'true' || this.runConfig.onSampleError === true; this.runConfig.onSampleError =
this.runConfig.runWithinResourcePool = this.runConfig.runWithinResourcePool === 'true' || this.runConfig.runWithinResourcePool === true; this.runConfig.onSampleError === "true" ||
this.runConfig.onSampleError === true;
this.runConfig.runWithinResourcePool =
this.runConfig.runWithinResourcePool === "true" ||
this.runConfig.runWithinResourcePool === true;
} }
this.runModeVisible = true; this.runModeVisible = true;
this.testType = testType; this.testType = testType;
@ -278,21 +280,21 @@ export default {
}, },
query() { query() {
this.loading = true; this.loading = true;
this.result = getSystemBaseSetting().then(response => { this.result = getSystemBaseSetting().then((response) => {
if (!response.data.runMode) { if (!response.data.runMode) {
response.data.runMode = 'LOCAL' response.data.runMode = "LOCAL";
} }
this.runMode = response.data.runMode; this.runMode = response.data.runMode;
if (this.runMode === 'POOL') { if (this.runMode === "POOL") {
this.runConfig.runWithinResourcePool = true; this.runConfig.runWithinResourcePool = true;
this.getProjectApplication(); this.getProjectApplication();
} else { } else {
this.loading = false; this.loading = false;
} }
}) });
}, },
getProjectApplication() { getProjectApplication() {
getProjectConfig(getCurrentProjectID(), "").then(res => { getProjectConfig(getCurrentProjectID(), "").then((res) => {
if (res.data && res.data.poolEnable && res.data.resourcePoolId) { if (res.data && res.data.poolEnable && res.data.resourcePoolId) {
this.runConfig.resourcePoolId = res.data.resourcePoolId; this.runConfig.resourcePoolId = res.data.resourcePoolId;
} }
@ -336,10 +338,9 @@ export default {
this.runConfig.environmentGroupId = id; this.runConfig.environmentGroupId = id;
}, },
getWsProjects() { getWsProjects() {
getOwnerProjects() getOwnerProjects().then((res) => {
.then((res) => { this.projectList = res.data;
this.projectList = res.data; });
});
}, },
showPopover() { showPopover() {
this.projectIds.clear(); this.projectIds.clear();
@ -353,7 +354,7 @@ export default {
param = this.planCaseIds; param = this.planCaseIds;
} else if (this.type === "plan") { } else if (this.type === "plan") {
url = "/test/plan/case/env"; url = "/test/plan/case/env";
param = {id: this.planId}; param = { id: this.planId };
} }
this.$post(url, param, (res) => { this.$post(url, param, (res) => {
let data = res.data; let data = res.data;

View File

@ -62,7 +62,6 @@ import ApiResult from "@/business/plan/view/comonents/report/detail/component/Ap
import TestPlanReportContainer from "@/business/plan/view/comonents/report/detail/TestPlanReportContainer"; import TestPlanReportContainer from "@/business/plan/view/comonents/report/detail/TestPlanReportContainer";
import ApiCases from "@/business/plan/view/comonents/report/detail/component/ApiCases"; import ApiCases from "@/business/plan/view/comonents/report/detail/component/ApiCases";
import TabPaneCount from "@/business/plan/view/comonents/report/detail/component/TabPaneCount"; import TabPaneCount from "@/business/plan/view/comonents/report/detail/component/TabPaneCount";
import {hasLicense} from "metersphere-frontend/src/utils/permission";
import {apiTestExecRerun} from "@/api/remote/ui/api-test"; import {apiTestExecRerun} from "@/api/remote/ui/api-test";
export default { export default {
@ -79,7 +78,7 @@ export default {
}; };
}, },
created() { created() {
this.showRerunBtn = !this.isShare && hasLicense(); this.showRerunBtn = !this.isShare;
}, },
props: [ props: [
'report', 'planId', 'isTemplate', 'isShare', 'shareId', 'isDb' 'report', 'planId', 'isTemplate', 'isShare', 'shareId', 'isDb'

View File

@ -215,7 +215,7 @@ export default {
} }
}, },
rerunVerify() { rerunVerify() {
if (hasLicense() && this.fullTreeNodes && this.fullTreeNodes.length > 0 && !this.isShare) { if (this.fullTreeNodes && this.fullTreeNodes.length > 0 && !this.isShare) {
this.fullTreeNodes.forEach(item => { this.fullTreeNodes.forEach(item => {
item.redirect = true; item.redirect = true;
if (item.totalStatus === 'FAIL' || item.totalStatus === 'ERROR' || item.unExecuteTotal > 0 if (item.totalStatus === 'FAIL' || item.totalStatus === 'ERROR' || item.unExecuteTotal > 0