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.RemakeReportService;
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.jorphan.collections.HashTree;
import org.json.JSONObject;
@ -59,6 +59,8 @@ public class ApiCaseSerialService {
private RedisTemplate<String, Object> redisTemplate;
@Resource
private TestPlanApiCaseMapper testPlanApiCaseMapper;
@Resource
private ApiRetryOnFailureService apiRetryOnFailureService;
public void serial(DBTestQueue executionQueue) {
ApiExecutionQueueDetail queue = executionQueue.getDetail();
@ -154,7 +156,6 @@ public class ApiCaseSerialService {
String data = element.toString();
if (runRequest.isRetryEnable() && runRequest.getRetryNum() > 0) {
// 失败重试
ApiRetryOnFailureService apiRetryOnFailureService = CommonBeanFactory.getBean(ApiRetryOnFailureService.class);
String retryData = apiRetryOnFailureService.retry(data, runRequest.getRetryNum(), true);
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.utils.LoggerUtil;
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.lang3.StringUtils;
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) {
boolean enable = element.has(ENABLE) ? element.optBoolean(ENABLE) : true;
if (!element.has(MIX_ENABLE)) {
element.put(MIX_ENABLE, true);
element.put(MIX_ENABLE, false);
}
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 variableEnable = element.has(VARIABLE_ENABLE) ? element.optBoolean(VARIABLE_ENABLE) : false;
boolean mixEnable = element.has(MIX_ENABLE)
? element.getBoolean(MIX_ENABLE) : true;
? element.getBoolean(MIX_ENABLE) : false;
if (environmentEnable && StringUtils.isNotEmpty(scenarioWithBLOBs.getEnvironmentJson())) {
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 { getUUID } from 'metersphere-frontend/src/utils';
import { getApiScenarioIdByPlanScenarioId } from '@/api/test-plan';
import {getScenarioReport} from '../../../api/scenario-report';
export default {
name: 'MsApiReportViewHeader',

View File

@ -47,7 +47,7 @@
@click.stop
@click="generate"
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') }}
</el-button>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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