feat(接口自动化 ): 多模式执行功能
This commit is contained in:
parent
f6384b935c
commit
f19f05db17
|
@ -66,6 +66,7 @@ public class ApiAutomationController {
|
|||
public void update(@RequestPart("request") SaveApiScenarioRequest request, @RequestPart(value = "files") List<MultipartFile> bodyFiles) {
|
||||
apiAutomationService.update(request, bodyFiles);
|
||||
}
|
||||
|
||||
@GetMapping("/delete/{id}")
|
||||
public void delete(@PathVariable String id) {
|
||||
apiAutomationService.delete(id);
|
||||
|
@ -139,10 +140,13 @@ public class ApiAutomationController {
|
|||
}
|
||||
|
||||
@PostMapping(value = "/run/batch")
|
||||
public String runBatcah(@RequestBody RunScenarioRequest request) {
|
||||
public String runBatch(@RequestBody RunScenarioRequest request) {
|
||||
request.setExecuteType(ExecuteType.Saved.name());
|
||||
request.setTriggerMode(ApiRunMode.SCENARIO.name());
|
||||
request.setRunMode(ApiRunMode.SCENARIO.name());
|
||||
if (request.getConfig() != null && request.getConfig().getMode().equals("serial")) {
|
||||
return apiAutomationService.runSerial(request);
|
||||
}
|
||||
return apiAutomationService.run(request);
|
||||
}
|
||||
|
||||
|
@ -174,7 +178,7 @@ public class ApiAutomationController {
|
|||
}
|
||||
|
||||
@PostMapping("/relevance/review")
|
||||
public void testCaseReviewRelevance(@RequestBody ApiCaseRelevanceRequest request){
|
||||
public void testCaseReviewRelevance(@RequestBody ApiCaseRelevanceRequest request) {
|
||||
apiAutomationService.relevanceReview(request);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
package io.metersphere.api.dto.automation;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class RunModeConfig {
|
||||
private String mode;
|
||||
private String reportType;
|
||||
private String reportName;
|
||||
private boolean onSampleError;
|
||||
}
|
|
@ -21,7 +21,7 @@ public class RunScenarioRequest extends ApiScenarioWithBLOBs {
|
|||
|
||||
private String runMode;
|
||||
|
||||
//测试情景和测试计划的关联ID
|
||||
/**测试情景和测试计划的关联ID*/
|
||||
private String planScenarioId;
|
||||
|
||||
private List<String> planCaseIds;
|
||||
|
@ -30,8 +30,9 @@ public class RunScenarioRequest extends ApiScenarioWithBLOBs {
|
|||
|
||||
private String reportUserID;
|
||||
|
||||
private Map<String,String> scenarioTestPlanIdMap;
|
||||
private Map<String, String> scenarioTestPlanIdMap;
|
||||
|
||||
private ApiScenarioRequest condition;
|
||||
|
||||
private RunModeConfig config;
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import java.util.List;
|
|||
@JSONType(typeName = "TestPlan")
|
||||
public class MsTestPlan extends MsTestElement {
|
||||
private String type = "TestPlan";
|
||||
private boolean serializeThreadgroups = false;
|
||||
|
||||
@Override
|
||||
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, ParameterConfig config) {
|
||||
|
@ -35,7 +36,7 @@ public class MsTestPlan extends MsTestElement {
|
|||
testPlan.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("TestPlanGui"));
|
||||
testPlan.setEnabled(true);
|
||||
testPlan.setFunctionalMode(false);
|
||||
testPlan.setSerialized(true);
|
||||
testPlan.setSerialized(serializeThreadgroups);
|
||||
testPlan.setTearDownOnShutdown(true);
|
||||
testPlan.setUserDefinedVariables(new Arguments());
|
||||
return testPlan;
|
||||
|
|
|
@ -19,6 +19,7 @@ import java.util.List;
|
|||
public class MsThreadGroup extends MsTestElement {
|
||||
private String type = "ThreadGroup";
|
||||
private boolean enableCookieShare;
|
||||
private boolean onSampleError;
|
||||
|
||||
@Override
|
||||
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, ParameterConfig config) {
|
||||
|
@ -59,6 +60,9 @@ public class MsThreadGroup extends MsTestElement {
|
|||
threadGroup.setDuration(0);
|
||||
threadGroup.setProperty(ThreadGroup.ON_SAMPLE_ERROR, ThreadGroup.ON_SAMPLE_ERROR_CONTINUE);
|
||||
threadGroup.setScheduler(false);
|
||||
if (onSampleError) {
|
||||
threadGroup.setProperty("ThreadGroup.on_sample_error", "stoptest");
|
||||
}
|
||||
threadGroup.setSamplerController(loopController);
|
||||
return threadGroup;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
package io.metersphere.api.dto.definition.request.sampler;
|
||||
|
||||
public class SamplerEnv {
|
||||
}
|
|
@ -10,5 +10,6 @@ public class HttpConfig {
|
|||
private String domain;
|
||||
private String protocol = "https";
|
||||
private int port;
|
||||
private List<HttpConfigCondition> conditions;
|
||||
private List<KeyValue> headers;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
package io.metersphere.api.dto.scenario;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class HttpConfigCondition {
|
||||
private String type;
|
||||
private List<KeyValue> details;
|
||||
private String protocol;
|
||||
private String socket;
|
||||
private String domain;
|
||||
}
|
|
@ -36,6 +36,8 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl
|
|||
|
||||
public final static String TEST_ID = "ms.test.id";
|
||||
|
||||
public final static String TEST_REPORT_NAME = "ms.test.report.name";
|
||||
|
||||
private final static String THREAD_SPLIT = " ";
|
||||
|
||||
private final static String ID_SPLIT = "-";
|
||||
|
@ -62,6 +64,8 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl
|
|||
private String testId;
|
||||
|
||||
private String debugReportId;
|
||||
// 只有合并报告是这个有值
|
||||
private String reportName;
|
||||
|
||||
//获得控制台内容
|
||||
private PrintStream oldPrintStream = System.out;
|
||||
|
@ -125,7 +129,7 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl
|
|||
TestResult testResult = new TestResult();
|
||||
testResult.setTestId(testId);
|
||||
testResult.setTotal(queue.size());
|
||||
|
||||
testResult.setReportName(this.reportName);
|
||||
// 一个脚本里可能包含多个场景(ThreadGroup),所以要区分开,key: 场景Id
|
||||
final Map<String, ScenarioResult> scenarios = new LinkedHashMap<>();
|
||||
queue.forEach(result -> {
|
||||
|
@ -209,7 +213,7 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl
|
|||
} else {
|
||||
apiDefinitionExecResultService.saveApiResult(testResult, ApiRunMode.API_PLAN.name());
|
||||
}
|
||||
} else if (StringUtils.equalsAny(this.runMode, ApiRunMode.SCENARIO.name(), ApiRunMode.SCENARIO_PLAN.name(), ApiRunMode.SCHEDULE_SCENARIO_PLAN.name(),ApiRunMode.SCHEDULE_SCENARIO.name())) {
|
||||
} else if (StringUtils.equalsAny(this.runMode, ApiRunMode.SCENARIO.name(), ApiRunMode.SCENARIO_PLAN.name(), ApiRunMode.SCHEDULE_SCENARIO_PLAN.name(), ApiRunMode.SCHEDULE_SCENARIO.name())) {
|
||||
// 执行报告不需要存储,由用户确认后在存储
|
||||
testResult.setTestId(testId);
|
||||
ApiScenarioReport scenarioReport = apiScenarioReportService.complete(testResult, this.runMode);
|
||||
|
@ -247,7 +251,7 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl
|
|||
|
||||
}
|
||||
}
|
||||
if (report != null && StringUtils.equals(ReportTriggerMode.API.name(), report.getTriggerMode())||StringUtils.equals(ReportTriggerMode.SCHEDULE.name(), report.getTriggerMode())) {
|
||||
if (report != null && StringUtils.equals(ReportTriggerMode.API.name(), report.getTriggerMode()) || StringUtils.equals(ReportTriggerMode.SCHEDULE.name(), report.getTriggerMode())) {
|
||||
sendTask(report, reportUrl, testResult);
|
||||
}
|
||||
|
||||
|
@ -394,6 +398,7 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl
|
|||
|
||||
private void setParam(BackendListenerContext context) {
|
||||
this.testId = context.getParameter(TEST_ID);
|
||||
this.reportName = context.getParameter(TEST_REPORT_NAME);
|
||||
this.runMode = context.getParameter("runMode");
|
||||
this.debugReportId = context.getParameter("debugReportId");
|
||||
if (StringUtils.isBlank(this.runMode)) {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package io.metersphere.api.jmeter;
|
||||
|
||||
import io.fabric8.kubernetes.client.extended.run.RunConfig;
|
||||
import io.metersphere.api.dto.automation.RunModeConfig;
|
||||
import io.metersphere.commons.constants.ApiRunMode;
|
||||
import io.metersphere.commons.exception.MSException;
|
||||
import io.metersphere.commons.utils.LogUtil;
|
||||
|
@ -88,6 +90,25 @@ public class JMeterService {
|
|||
testPlan.add(testPlan.getArray()[0], backendListener);
|
||||
}
|
||||
|
||||
private void addBackendListener(String testId, String debugReportId, String runMode, HashTree testPlan, RunModeConfig config) {
|
||||
BackendListener backendListener = new BackendListener();
|
||||
backendListener.setName(testId);
|
||||
Arguments arguments = new Arguments();
|
||||
if (config != null && config.getMode().equals("serial") && config.getReportType().equals("setReport")) {
|
||||
arguments.addArgument(APIBackendListenerClient.TEST_REPORT_NAME, config.getReportName());
|
||||
}
|
||||
arguments.addArgument(APIBackendListenerClient.TEST_ID, testId);
|
||||
if (StringUtils.isNotBlank(runMode)) {
|
||||
arguments.addArgument("runMode", runMode);
|
||||
}
|
||||
if (StringUtils.isNotBlank(debugReportId)) {
|
||||
arguments.addArgument("debugReportId", debugReportId);
|
||||
}
|
||||
backendListener.setArguments(arguments);
|
||||
backendListener.setClassname(APIBackendListenerClient.class.getCanonicalName());
|
||||
testPlan.add(testPlan.getArray()[0], backendListener);
|
||||
}
|
||||
|
||||
public void runDefinition(String testId, HashTree testPlan, String debugReportId, String runMode) {
|
||||
try {
|
||||
init();
|
||||
|
@ -99,4 +120,16 @@ public class JMeterService {
|
|||
MSException.throwException(Translator.get("api_load_script_error"));
|
||||
}
|
||||
}
|
||||
|
||||
public void runSerial(String testId, HashTree testPlan, String debugReportId, String runMode, RunModeConfig config) {
|
||||
try {
|
||||
init();
|
||||
addBackendListener(testId, debugReportId, runMode, testPlan, config);
|
||||
LocalRunner runner = new LocalRunner(testPlan);
|
||||
runner.run();
|
||||
} catch (Exception e) {
|
||||
LogUtil.error(e.getMessage(), e);
|
||||
MSException.throwException(Translator.get("api_load_script_error"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,8 @@ public class TestResult {
|
|||
|
||||
private String testId;
|
||||
|
||||
private String reportName;
|
||||
|
||||
private int success = 0;
|
||||
|
||||
private int error = 0;
|
||||
|
|
|
@ -448,7 +448,7 @@ public class ApiAutomationService {
|
|||
String referenced = tr.getReferenced();
|
||||
if (StringUtils.equals(MsTestElementConstants.REF.name(), referenced)) {
|
||||
if (StringUtils.equals(tr.getType(), "HTTPSamplerProxy")) {
|
||||
MsHTTPSamplerProxy http = (MsHTTPSamplerProxy)tr;
|
||||
MsHTTPSamplerProxy http = (MsHTTPSamplerProxy) tr;
|
||||
String refType = tr.getRefType();
|
||||
if (StringUtils.equals(refType, "CASE")) {
|
||||
http.setUrl(null);
|
||||
|
@ -668,7 +668,7 @@ public class ApiAutomationService {
|
|||
}
|
||||
|
||||
/**
|
||||
* 场景测试执行
|
||||
* 场景测试并行执行
|
||||
*
|
||||
* @param request
|
||||
* @return
|
||||
|
@ -737,6 +737,123 @@ public class ApiAutomationService {
|
|||
return request.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成串行HashTree
|
||||
*
|
||||
* @param apiScenarios 场景
|
||||
* @param request 请求参数
|
||||
* @param reportIds 报告ID
|
||||
* @return hashTree
|
||||
*/
|
||||
private HashTree generateHashTree(List<ApiScenarioWithBLOBs> apiScenarios, RunScenarioRequest request, List<String> reportIds) {
|
||||
HashTree jmeterHashTree = new ListedHashTree();
|
||||
MsTestPlan testPlan = new MsTestPlan();
|
||||
testPlan.setSerializeThreadgroups(true);
|
||||
testPlan.setHashTree(new LinkedList<>());
|
||||
try {
|
||||
boolean isFirst = true;
|
||||
for (ApiScenarioWithBLOBs item : apiScenarios) {
|
||||
if (item.getStepTotal() == null || item.getStepTotal() == 0) {
|
||||
// 只有一个场景且没有测试步骤,则提示
|
||||
if (apiScenarios.size() == 1) {
|
||||
MSException.throwException((item.getName() + "," + Translator.get("automation_exec_info")));
|
||||
}
|
||||
LogUtil.warn(item.getName() + "," + Translator.get("automation_exec_info"));
|
||||
continue;
|
||||
}
|
||||
MsThreadGroup group = new MsThreadGroup();
|
||||
group.setLabel(item.getName());
|
||||
group.setName(UUID.randomUUID().toString());
|
||||
group.setOnSampleError(request.getConfig().isOnSampleError());
|
||||
// 批量执行的结果直接存储为报告
|
||||
if (isFirst) {
|
||||
group.setName(request.getId());
|
||||
isFirst = false;
|
||||
}
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
JSONObject element = JSON.parseObject(item.getScenarioDefinition());
|
||||
MsScenario scenario = JSONObject.parseObject(item.getScenarioDefinition(), MsScenario.class);
|
||||
|
||||
// 多态JSON普通转换会丢失内容,需要通过 ObjectMapper 获取
|
||||
if (element != null && StringUtils.isNotEmpty(element.getString("hashTree"))) {
|
||||
LinkedList<MsTestElement> elements = mapper.readValue(element.getString("hashTree"),
|
||||
new TypeReference<LinkedList<MsTestElement>>() {
|
||||
});
|
||||
scenario.setHashTree(elements);
|
||||
}
|
||||
if (StringUtils.isNotEmpty(element.getString("variables"))) {
|
||||
LinkedList<ScenarioVariable> variables = mapper.readValue(element.getString("variables"),
|
||||
new TypeReference<LinkedList<ScenarioVariable>>() {
|
||||
});
|
||||
scenario.setVariables(variables);
|
||||
}
|
||||
group.setEnableCookieShare(scenario.isEnableCookieShare());
|
||||
LinkedList<MsTestElement> scenarios = new LinkedList<>();
|
||||
scenarios.add(scenario);
|
||||
// 创建场景报告
|
||||
if (reportIds != null) {
|
||||
//如果是测试计划页面触发的执行方式,生成报告时createScenarioReport第二个参数需要特殊处理
|
||||
if (StringUtils.equals(request.getRunMode(), ApiRunMode.SCENARIO_PLAN.name())) {
|
||||
String testPlanScenarioId = item.getId();
|
||||
if (request.getScenarioTestPlanIdMap() != null && request.getScenarioTestPlanIdMap().containsKey(item.getId())) {
|
||||
testPlanScenarioId = request.getScenarioTestPlanIdMap().get(item.getId());
|
||||
// 获取场景用例单独的执行环境
|
||||
TestPlanApiScenario planApiScenario = testPlanApiScenarioMapper.selectByPrimaryKey(testPlanScenarioId);
|
||||
String environment = planApiScenario.getEnvironment();
|
||||
if (StringUtils.isNotBlank(environment)) {
|
||||
scenario.setEnvironmentMap(JSON.parseObject(environment, Map.class));
|
||||
}
|
||||
}
|
||||
APIScenarioReportResult reportResult = createScenarioReport(group.getName(), testPlanScenarioId, item.getName(), request.getTriggerMode() == null ? ReportTriggerMode.MANUAL.name() : request.getTriggerMode(),
|
||||
request.getExecuteType(), item.getProjectId(), request.getReportUserID());
|
||||
apiScenarioReportMapper.insert(reportResult);
|
||||
} else {
|
||||
APIScenarioReportResult reportResult = createScenarioReport(group.getName(), item.getId(), item.getName(), request.getTriggerMode() == null ? ReportTriggerMode.MANUAL.name() : request.getTriggerMode(),
|
||||
request.getExecuteType(), item.getProjectId(), request.getReportUserID());
|
||||
apiScenarioReportMapper.insert(reportResult);
|
||||
}
|
||||
reportIds.add(group.getName());
|
||||
}
|
||||
group.setHashTree(scenarios);
|
||||
testPlan.getHashTree().add(group);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
MSException.throwException(ex.getMessage());
|
||||
}
|
||||
|
||||
testPlan.toHashTree(jmeterHashTree, testPlan.getHashTree(), new ParameterConfig());
|
||||
return jmeterHashTree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景串行
|
||||
*
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
public String runSerial(RunScenarioRequest request) {
|
||||
ServiceUtils.getSelectAllIds(request, request.getCondition(),
|
||||
(query) -> extApiScenarioMapper.selectIdsByQuery((ApiScenarioRequest) query));
|
||||
List<String> ids = request.getIds();
|
||||
//检查是否有正在执行中的情景
|
||||
this.checkScenarioIsRunning(ids);
|
||||
List<ApiScenarioWithBLOBs> apiScenarios = extApiScenarioMapper.selectIds(ids);
|
||||
|
||||
String runMode = ApiRunMode.SCENARIO.name();
|
||||
if (StringUtils.isNotBlank(request.getRunMode()) && StringUtils.equals(request.getRunMode(), ApiRunMode.SCENARIO_PLAN.name())) {
|
||||
runMode = ApiRunMode.SCENARIO_PLAN.name();
|
||||
}
|
||||
if (StringUtils.isNotBlank(request.getRunMode()) && StringUtils.equals(request.getRunMode(), ApiRunMode.DEFINITION.name())) {
|
||||
runMode = ApiRunMode.DEFINITION.name();
|
||||
}
|
||||
// 调用执行方法
|
||||
List<String> reportIds = new LinkedList<>();
|
||||
HashTree hashTree = generateHashTree(apiScenarios, request, reportIds);
|
||||
jMeterService.runSerial(JSON.toJSONString(reportIds), hashTree, request.getReportId(), runMode, request.getConfig());
|
||||
return request.getId();
|
||||
}
|
||||
|
||||
public void checkScenarioIsRunning(List<String> ids) {
|
||||
List<ApiScenarioReport> lastReportStatusByIds = apiReportService.selectLastReportByIds(ids);
|
||||
for (ApiScenarioReport report : lastReportStatusByIds) {
|
||||
|
|
|
@ -530,7 +530,6 @@ public class ApiDefinitionService {
|
|||
}
|
||||
|
||||
HashTree hashTree = request.getTestElement().generateHashTree(config);
|
||||
|
||||
String runMode = ApiRunMode.DEFINITION.name();
|
||||
if (StringUtils.isNotBlank(request.getType()) && StringUtils.equals(request.getType(), ApiRunMode.API_PLAN.name())) {
|
||||
runMode = ApiRunMode.API_PLAN.name();
|
||||
|
|
|
@ -16,6 +16,7 @@ import io.metersphere.base.mapper.ApiScenarioReportMapper;
|
|||
import io.metersphere.base.mapper.TestPlanApiScenarioMapper;
|
||||
import io.metersphere.base.mapper.ext.ExtApiScenarioReportMapper;
|
||||
import io.metersphere.base.mapper.ext.ExtTestPlanScenarioCaseMapper;
|
||||
import io.metersphere.commons.constants.APITestStatus;
|
||||
import io.metersphere.commons.constants.ApiRunMode;
|
||||
import io.metersphere.commons.constants.ReportTriggerMode;
|
||||
import io.metersphere.commons.exception.MSException;
|
||||
|
@ -33,10 +34,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||
import javax.annotation.Resource;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
|
@ -59,12 +57,12 @@ public class ApiScenarioReportService {
|
|||
// 更新场景
|
||||
if (result != null) {
|
||||
if (StringUtils.equals(runMode, ApiRunMode.SCENARIO_PLAN.name())) {
|
||||
return updatePlanCase(result);
|
||||
return updatePlanCase(result,runMode);
|
||||
} else if (StringUtils.equals(runMode, ApiRunMode.SCHEDULE_SCENARIO_PLAN.name())) {
|
||||
return updateSchedulePlanCase(result);
|
||||
return updateSchedulePlanCase(result,runMode);
|
||||
} else {
|
||||
updateScenarioStatus(result.getTestId());
|
||||
return updateScenario(result);
|
||||
return updateScenario(result, runMode);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
@ -92,6 +90,30 @@ public class ApiScenarioReportService {
|
|||
}
|
||||
}
|
||||
|
||||
public APIScenarioReportResult createScenarioReport(String scenarioIds, String reportName, String status, String scenarioNames, String triggerMode, String projectId, String userID) {
|
||||
APIScenarioReportResult report = new APIScenarioReportResult();
|
||||
if (triggerMode.equals(ApiRunMode.SCENARIO.name()) || triggerMode.equals(ApiRunMode.DEFINITION.name())) {
|
||||
triggerMode = ReportTriggerMode.MANUAL.name();
|
||||
}
|
||||
report.setId(UUID.randomUUID().toString());
|
||||
report.setName(reportName);
|
||||
report.setCreateTime(System.currentTimeMillis());
|
||||
report.setUpdateTime(System.currentTimeMillis());
|
||||
report.setStatus(status);
|
||||
if (StringUtils.isNotEmpty(userID)) {
|
||||
report.setUserId(userID);
|
||||
} else {
|
||||
report.setUserId(SessionUtils.getUserId());
|
||||
}
|
||||
report.setTriggerMode(triggerMode);
|
||||
report.setExecuteType(ExecuteType.Saved.name());
|
||||
report.setProjectId(projectId);
|
||||
report.setScenarioName(scenarioNames);
|
||||
report.setScenarioId(scenarioIds);
|
||||
apiScenarioReportMapper.insert(report);
|
||||
return report;
|
||||
}
|
||||
|
||||
public ApiScenarioReport editReport(ScenarioResult test) {
|
||||
ApiScenarioReport report = apiScenarioReportMapper.selectByPrimaryKey(test.getName());
|
||||
report.setId(report.getId());
|
||||
|
@ -122,6 +144,17 @@ public class ApiScenarioReportService {
|
|||
return report;
|
||||
}
|
||||
|
||||
private TestResult createTestResult(TestResult result) {
|
||||
TestResult testResult = new TestResult();
|
||||
testResult.setTestId(result.getTestId());
|
||||
testResult.setTotal(result.getTotal());
|
||||
testResult.setError(result.getError());
|
||||
testResult.setPassAssertions(result.getPassAssertions());
|
||||
testResult.setSuccess(result.getSuccess());
|
||||
testResult.setTotalAssertions(result.getTotalAssertions());
|
||||
return testResult;
|
||||
}
|
||||
|
||||
private TestResult createTestResult(String testId, ScenarioResult scenarioResult) {
|
||||
TestResult testResult = new TestResult();
|
||||
testResult.setTestId(testId);
|
||||
|
@ -133,10 +166,15 @@ public class ApiScenarioReportService {
|
|||
return testResult;
|
||||
}
|
||||
|
||||
public ApiScenarioReport updatePlanCase(TestResult result) {
|
||||
public ApiScenarioReport updatePlanCase(TestResult result,String runMode) {
|
||||
// TestPlanApiScenario testPlanApiScenario = testPlanApiScenarioMapper.selectByPrimaryKey(result.getTestId());
|
||||
List<ScenarioResult> scenarioResultList = result.getScenarios();
|
||||
ApiScenarioReport returnReport = null;
|
||||
StringBuilder scenarioIds = new StringBuilder();
|
||||
StringBuilder scenarioNames = new StringBuilder();
|
||||
String projectId = null;
|
||||
String userId = null;
|
||||
TestResult fullResult = createTestResult(result);
|
||||
for (ScenarioResult scenarioResult :
|
||||
scenarioResultList) {
|
||||
ApiScenarioReport report = editReport(scenarioResult);
|
||||
|
@ -151,6 +189,12 @@ public class ApiScenarioReportService {
|
|||
detail.setProjectId(report.getProjectId());
|
||||
apiScenarioReportDetailMapper.insert(detail);
|
||||
|
||||
fullResult.addScenario(scenarioResult);
|
||||
projectId = report.getProjectId();
|
||||
userId = report.getUserId();
|
||||
scenarioIds.append(scenarioResult.getName()).append(",");
|
||||
scenarioNames.append(report.getName()).append(",");
|
||||
|
||||
TestPlanApiScenario testPlanApiScenario = testPlanApiScenarioMapper.selectByPrimaryKey(report.getScenarioId());
|
||||
if (testPlanApiScenario != null) {
|
||||
report.setScenarioId(testPlanApiScenario.getApiScenarioId());
|
||||
|
@ -168,15 +212,20 @@ public class ApiScenarioReportService {
|
|||
}
|
||||
returnReport = report;
|
||||
}
|
||||
|
||||
margeReport(result, scenarioIds, scenarioNames, runMode, projectId, userId);
|
||||
return returnReport;
|
||||
}
|
||||
|
||||
public ApiScenarioReport updateSchedulePlanCase(TestResult result) {
|
||||
public ApiScenarioReport updateSchedulePlanCase(TestResult result,String runMode) {
|
||||
ApiScenarioReport lastReport = null;
|
||||
List<ScenarioResult> scenarioResultList = result.getScenarios();
|
||||
|
||||
List<String> testPlanReportIdList = new ArrayList<>();
|
||||
StringBuilder scenarioIds = new StringBuilder();
|
||||
StringBuilder scenarioNames = new StringBuilder();
|
||||
String projectId = null;
|
||||
String userId = null;
|
||||
TestResult fullResult = createTestResult(result);
|
||||
for (ScenarioResult scenarioResult : scenarioResultList) {
|
||||
// 存储场景报告
|
||||
ApiScenarioReport report = editReport(scenarioResult);
|
||||
|
@ -224,8 +273,16 @@ public class ApiScenarioReportService {
|
|||
testPlanApiScenario.setUpdateTime(System.currentTimeMillis());
|
||||
testPlanApiScenarioMapper.updateByPrimaryKeySelective(testPlanApiScenario);
|
||||
|
||||
fullResult.addScenario(scenarioResult);
|
||||
projectId = report.getProjectId();
|
||||
userId = report.getUserId();
|
||||
scenarioIds.append(scenarioResult.getName()).append(",");
|
||||
scenarioNames.append(report.getName()).append(",");
|
||||
|
||||
lastReport = report;
|
||||
}
|
||||
// 合并报告
|
||||
margeReport(result, scenarioIds, scenarioNames, runMode, projectId, userId);
|
||||
|
||||
TestPlanReportService testPlanReportService = CommonBeanFactory.getBean(TestPlanReportService.class);
|
||||
testPlanReportService.updateReport(testPlanReportIdList, ApiRunMode.SCHEDULE_SCENARIO_PLAN.name(), ReportTriggerMode.SCHEDULE.name());
|
||||
|
@ -266,16 +323,38 @@ public class ApiScenarioReportService {
|
|||
}
|
||||
}
|
||||
|
||||
public ApiScenarioReport updateScenario(TestResult result) {
|
||||
private void margeReport(TestResult result, StringBuilder scenarioIds, StringBuilder scenarioNames, String runMode, String projectId, String userId) {
|
||||
// 合并生成一份报告
|
||||
if (StringUtils.isNotEmpty(result.getReportName())) {
|
||||
ApiScenarioReport report = createScenarioReport(scenarioIds.toString(), result.getReportName(), result.getError() > 0 ? "Error" : "Success", scenarioNames.toString().substring(0, scenarioNames.toString().length() - 1), runMode, projectId, userId);
|
||||
ApiScenarioReportDetail detail = new ApiScenarioReportDetail();
|
||||
detail.setContent(JSON.toJSONString(result).getBytes(StandardCharsets.UTF_8));
|
||||
detail.setReportId(report.getId());
|
||||
detail.setProjectId(report.getProjectId());
|
||||
apiScenarioReportDetailMapper.insert(detail);
|
||||
}
|
||||
}
|
||||
|
||||
public ApiScenarioReport updateScenario(TestResult result, String runMode) {
|
||||
ApiScenarioReport lastReport = null;
|
||||
StringBuilder scenarioIds = new StringBuilder();
|
||||
StringBuilder scenarioNames = new StringBuilder();
|
||||
String projectId = null;
|
||||
String userId = null;
|
||||
TestResult fullResult = createTestResult(result);
|
||||
for (ScenarioResult item : result.getScenarios()) {
|
||||
// 更新报告状态
|
||||
ApiScenarioReport report = editReport(item);
|
||||
// 报告详情内容
|
||||
ApiScenarioReportDetail detail = new ApiScenarioReportDetail();
|
||||
TestResult newResult = createTestResult(result.getTestId(), item);
|
||||
item.setName(report.getScenarioName());
|
||||
newResult.addScenario(item);
|
||||
fullResult.addScenario(item);
|
||||
projectId = report.getProjectId();
|
||||
userId = report.getUserId();
|
||||
scenarioIds.append(item.getName()).append(",");
|
||||
scenarioNames.append(report.getName()).append(",");
|
||||
// 报告详情内容
|
||||
ApiScenarioReportDetail detail = new ApiScenarioReportDetail();
|
||||
detail.setContent(JSON.toJSONString(newResult).getBytes(StandardCharsets.UTF_8));
|
||||
detail.setReportId(report.getId());
|
||||
detail.setProjectId(report.getProjectId());
|
||||
|
@ -295,6 +374,8 @@ public class ApiScenarioReportService {
|
|||
}
|
||||
lastReport = report;
|
||||
}
|
||||
// 合并生成一份报告
|
||||
margeReport(result, scenarioIds, scenarioNames, runMode, projectId, userId);
|
||||
return lastReport;
|
||||
}
|
||||
|
||||
|
|
|
@ -100,7 +100,7 @@
|
|||
|
||||
<select id="list" resultMap="BaseResultMap">
|
||||
SELECT r.name AS test_name,
|
||||
r.name, r.description, r.id, r.project_id, r.create_time, r.update_time, r.status, r.trigger_mode,s.name as scenario_name,
|
||||
r.name, r.description, r.id, r.project_id, r.create_time, r.update_time, r.status, r.trigger_mode,IfNULL(s.name,r.scenario_name) as scenario_name,
|
||||
project.name AS project_name, user.name AS user_name
|
||||
FROM api_scenario_report r
|
||||
LEFT JOIN api_scenario s on r.scenario_id = s.id
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
package io.metersphere.commons.constants;
|
||||
|
||||
public enum ConditionType {
|
||||
NO, PATH, MODULE
|
||||
}
|
|
@ -92,6 +92,9 @@ public class TestCaseReviewScenarioCaseService {
|
|||
request.setIds(scenarioIds);
|
||||
request.setScenarioTestPlanIdMap(scenarioIdApiScarionMap);
|
||||
request.setRunMode(ApiRunMode.SCENARIO_PLAN.name());
|
||||
if (request.getConfig() != null && request.getConfig().getMode().equals("serial")) {
|
||||
return apiAutomationService.runSerial(request);
|
||||
}
|
||||
return apiAutomationService.run(request);
|
||||
}
|
||||
|
||||
|
|
|
@ -106,6 +106,9 @@ public class TestPlanScenarioCaseService {
|
|||
request.setIds(scenarioIds);
|
||||
request.setScenarioTestPlanIdMap(scenarioIdApiScarionMap);
|
||||
request.setRunMode(ApiRunMode.SCENARIO_PLAN.name());
|
||||
if (request.getConfig() != null && request.getConfig().getMode().equals("serial")) {
|
||||
return apiAutomationService.runSerial(request);
|
||||
}
|
||||
return apiAutomationService.run(request);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
|
||||
-- api_scenario_report modify column length
|
||||
ALTER TABLE api_scenario_report MODIFY COLUMN name VARCHAR(3000);
|
||||
-- api_scenario_report modify column length
|
||||
ALTER TABLE api_scenario_report MODIFY COLUMN scenario_id VARCHAR(3000);
|
|
@ -100,14 +100,12 @@
|
|||
formatResult(res) {
|
||||
let resMap = new Map;
|
||||
let array = [];
|
||||
let i = 0;
|
||||
if (res && res.scenarios) {
|
||||
res.scenarios.forEach(item => {
|
||||
if (item && item.requestResults) {
|
||||
item.requestResults.forEach(req => {
|
||||
resMap.set(req.id, req);
|
||||
req.index = i;
|
||||
i++;
|
||||
req.name = item.name + "^@~@^" + req.name;
|
||||
array.push(req);
|
||||
})
|
||||
}
|
||||
|
|
|
@ -157,7 +157,7 @@
|
|||
</batch-edit>
|
||||
|
||||
<batch-move @refresh="search" @moveSave="moveSave" ref="testBatchMove"/>
|
||||
|
||||
<ms-run-mode @handleRunBatch="handleRunBatch" ref="runMode"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -180,6 +180,8 @@ import BatchEdit from "../../../track/case/components/BatchEdit";
|
|||
import {API_SCENARIO_LIST, PROJECT_NAME, WORKSPACE_ID} from "../../../../../common/js/constants";
|
||||
import EnvironmentSelect from "../../definition/components/environment/EnvironmentSelect";
|
||||
import BatchMove from "../../../track/case/components/BatchMove";
|
||||
import MsRunMode from "./common/RunMode";
|
||||
|
||||
import {
|
||||
_filter,
|
||||
_handleSelect,
|
||||
|
@ -213,7 +215,8 @@ export default {
|
|||
MsApiReportDetail,
|
||||
MsScenarioExtendButtons,
|
||||
MsTestPlanList,
|
||||
MsTableOperatorButton
|
||||
MsTableOperatorButton,
|
||||
MsRunMode
|
||||
},
|
||||
props: {
|
||||
referenced: {
|
||||
|
@ -606,9 +609,13 @@ export default {
|
|||
param.condition = this.condition;
|
||||
},
|
||||
handleBatchExecute() {
|
||||
this.$refs.runMode.open();
|
||||
|
||||
},
|
||||
handleRunBatch(config){
|
||||
this.infoDb = false;
|
||||
let url = "/api/automation/run/batch";
|
||||
let run = {};
|
||||
let run = {config: config};
|
||||
run.id = getUUID();
|
||||
this.buildBatchParam(run);
|
||||
this.$post(url, run, response => {
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
destroy-on-close
|
||||
:title="$t('load_test.runtime_config')"
|
||||
width="350px"
|
||||
:visible.sync="runModeVisible"
|
||||
>
|
||||
<div>
|
||||
<span class="ms-mode-span">{{ $t("run_mode.title") }}:</span>
|
||||
<el-radio-group v-model="runConfig.mode">
|
||||
<el-radio label="serial">{{ $t("run_mode.serial") }}</el-radio>
|
||||
<el-radio label="parallel">{{ $t("run_mode.parallel") }}</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div class="ms-mode-div" v-if="runConfig.mode === 'serial'">
|
||||
<span class="ms-mode-span">{{ $t("run_mode.other_config") }}:</span>
|
||||
<el-radio-group v-model="runConfig.reportType">
|
||||
<el-radio label="iddReport">{{ $t("run_mode.idd_report") }}</el-radio>
|
||||
<el-radio label="setReport">{{ $t("run_mode.set_report") }}</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div class="ms-mode-div" v-if="runConfig.reportType === 'setReport'">
|
||||
<span class="ms-mode-span">{{ $t("run_mode.report_name") }}:</span>
|
||||
<el-input
|
||||
v-model="runConfig.reportName"
|
||||
:placeholder="$t('commons.input_content')"
|
||||
size="small"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</div>
|
||||
<template v-slot:footer>
|
||||
<ms-dialog-footer @cancel="close" @confirm="handleRunBatch"/>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MsDialogFooter from "@/business/components/common/components/MsDialogFooter";
|
||||
|
||||
export default {
|
||||
name: "RunMode",
|
||||
components: {MsDialogFooter},
|
||||
data() {
|
||||
return {
|
||||
runModeVisible: false,
|
||||
runConfig: {mode: "serial", reportType: "iddReport", reportName: ""},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.runModeVisible = true;
|
||||
},
|
||||
close() {
|
||||
this.runConfig = {mode: "serial", reportType: "iddReport", reportName: ""};
|
||||
this.runModeVisible = false;
|
||||
},
|
||||
handleRunBatch() {
|
||||
if (this.runConfig.mode === 'serial' && this.runConfig.reportType === 'setReport' && this.runConfig.reportName.trim() === "") {
|
||||
this.$warning(this.$t('commons.input_name'));
|
||||
return;
|
||||
}
|
||||
this.$emit("handleRunBatch", this.runConfig);
|
||||
this.close();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ms-mode-span {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.ms-mode-div {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<el-dialog :close-on-click-modal="false" :title="$t('api_test.environment.environment_config')"
|
||||
:visible.sync="visible" class="environment-dialog" width="60%"
|
||||
@close="close" append-to-body ref="environmentConfig">
|
||||
@close="close" append-to-body destroy-on-close ref="environmentConfig">
|
||||
<el-container v-loading="result.loading">
|
||||
<ms-aside-item :enable-aside-hidden="false" :title="$t('api_test.environment.environment_list')"
|
||||
:data="environments" :item-operators="environmentOperators" :add-fuc="addEnvironment"
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
<el-form :model="condition" :rules="rules" ref="httpConfig">
|
||||
<el-form-item prop="socket">
|
||||
<span class="ms-env-span">{{$t('api_test.environment.socket')}}</span>
|
||||
<el-input v-model="httpConfig.socket" style="width: 80%" :placeholder="$t('api_test.request.url_description')" clearable size="small">
|
||||
<el-input v-model="condition.socket" style="width: 80%" :placeholder="$t('api_test.request.url_description')" clearable size="small">
|
||||
<template v-slot:prepend>
|
||||
<el-select v-model="httpConfig.protocol" class="request-protocol-select" size="small">
|
||||
<el-select v-model="condition.protocol" class="request-protocol-select" size="small">
|
||||
<el-option label="http://" value="http"/>
|
||||
<el-option label="https://" value="https"/>
|
||||
</el-select>
|
||||
|
@ -14,15 +14,17 @@
|
|||
<el-form-item prop="enable">
|
||||
<span class="ms-env-span">{{$t('api_test.environment.condition_enable')}}</span>
|
||||
<el-radio-group v-model="condition.type" @change="typeChange">
|
||||
<el-radio label="no">{{ $t('api_test.definition.document.data_set.none') }}</el-radio>
|
||||
<el-radio label="module">{{$t('test_track.module.module')}}</el-radio>
|
||||
<el-radio label="path">{{$t('api_test.definition.api_path')}}</el-radio>
|
||||
<el-radio label="NO">{{ $t('api_test.definition.document.data_set.none') }}</el-radio>
|
||||
<el-radio label="MODULE">{{$t('test_track.module.module')}}</el-radio>
|
||||
<el-radio label="PATH">{{$t('api_test.definition.api_path')}}</el-radio>
|
||||
</el-radio-group>
|
||||
<el-button type="primary" style="float: right" size="mini" @click="add">{{$t('commons.add')}}</el-button>
|
||||
<div v-if="condition.type === 'module'">
|
||||
<ms-select-tree size="small" :data="moduleOptions" :default-key="condition.value" @getValue="setModule" :obj="moduleObj" clearable checkStrictly multiple/>
|
||||
<el-button type="primary" v-if="!condition.id" style="float: right" size="mini" @click="add">{{$t('commons.add')}}</el-button>
|
||||
<el-button type="primary" v-else style="float: right" size="mini" @click="update">{{$t('commons.update')}}</el-button>
|
||||
|
||||
<div v-if="condition.type === 'MODULE'">
|
||||
<ms-select-tree size="small" :data="moduleOptions" :default-key="condition.ids" @getValue="setModule" :obj="moduleObj" clearable checkStrictly multiple/>
|
||||
</div>
|
||||
<div v-if="condition.type === 'path'">
|
||||
<div v-if="condition.type === 'PATH'">
|
||||
<el-input v-model="pathDetails.name" :placeholder="$t('api_test.value')" clearable size="small">
|
||||
<template v-slot:prepend>
|
||||
<el-select v-model="pathDetails.value" class="request-protocol-select" size="small">
|
||||
|
@ -33,38 +35,27 @@
|
|||
</el-input>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<div class="el-form-item">
|
||||
<el-table :data="httpConfig.conditions" style="width: 100%">
|
||||
<el-table-column prop="domain" :label="$t('load_test.domain')" width="180">
|
||||
<div class="ms-border">
|
||||
<el-table :data="httpConfig.conditions" highlight-current-row @current-change="selectRow">
|
||||
<el-table-column prop="socket" :label="$t('load_test.domain')" width="180">
|
||||
<template v-slot:default="{row}">
|
||||
{{getUrl(row)}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="type"
|
||||
:label="$t('api_test.environment.condition_enable')"
|
||||
show-overflow-tooltip
|
||||
min-width="120px">
|
||||
<el-table-column prop="type" :label="$t('api_test.environment.condition_enable')" show-overflow-tooltip min-width="120px">
|
||||
<template v-slot:default="{row}">
|
||||
{{getName(row)}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="details"
|
||||
show-overflow-tooltip
|
||||
min-width="120px"
|
||||
:label="$t('api_test.value')">
|
||||
<el-table-column prop="details" show-overflow-tooltip min-width="120px" :label="$t('api_test.value')">
|
||||
<template v-slot:default="{row}">
|
||||
{{getDetails(row)}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.operating')" width="100px">
|
||||
<template v-slot:default="{row}">
|
||||
<ms-table-operator-button
|
||||
:tip="$t('api_test.automation.copy')"
|
||||
icon="el-icon-document-copy"
|
||||
@exec="copy(row)"/>
|
||||
<ms-table-operator-button
|
||||
:tip="$t('api_test.automation.remove')"
|
||||
icon="el-icon-delete"
|
||||
@exec="remove(row)"
|
||||
type="danger"
|
||||
v-tester/>
|
||||
<ms-table-operator-button :tip="$t('api_test.automation.copy')" icon="el-icon-document-copy" @exec="copy(row)"/>
|
||||
<ms-table-operator-button :tip="$t('api_test.automation.remove')" icon="el-icon-delete" @exec="remove(row)" type="danger" v-tester/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
@ -82,6 +73,7 @@
|
|||
import MsTableOperatorButton from "@/business/components/common/components/MsTableOperatorButton";
|
||||
import {getUUID} from "@/common/js/utils";
|
||||
import {KeyValue} from "../../../definition/model/ApiTestModel";
|
||||
import Vue from "vue";
|
||||
|
||||
export default {
|
||||
name: "MsEnvironmentHttpConfig",
|
||||
|
@ -96,79 +88,100 @@
|
|||
data() {
|
||||
let socketValidator = (rule, value, callback) => {
|
||||
if (!this.validateSocket(value)) {
|
||||
callback(new Error(this.$t('commons.formatErr')));
|
||||
callback(new Error(this.$t("commons.formatErr")));
|
||||
return false;
|
||||
} else {
|
||||
callback();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
return {
|
||||
headerSuggestions: REQUEST_HEADERS,
|
||||
rules: {
|
||||
socket: [{required: false, validator: socketValidator, trigger: 'blur'}],
|
||||
socket: [{required: false, validator: socketValidator, trigger: "blur"}],
|
||||
},
|
||||
moduleOptions: [],
|
||||
moduleObj: {
|
||||
id: 'id',
|
||||
label: 'name',
|
||||
id: "id",
|
||||
label: "name",
|
||||
},
|
||||
pathDetails: new KeyValue({name: "", value: "contains"}),
|
||||
condition: {type: 'no', details: [new KeyValue({name: "", value: "contains"})], domain: ""},
|
||||
}
|
||||
condition: {type: "NO", details: [new KeyValue({name: "", value: "contains"})], protocol: "", socket: "", domain: ""},
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
projectId() {
|
||||
this.list();
|
||||
}
|
||||
},
|
||||
httpConfig: function (o) {
|
||||
this.condition.protocol = this.httpConfig.protocol;
|
||||
this.condition.socket = this.httpConfig.socket;
|
||||
this.condition.domain = this.httpConfig.domain;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getUrl(row) {
|
||||
return row.protocol + "://" + row.socket;
|
||||
},
|
||||
getName(row) {
|
||||
switch (row.type) {
|
||||
case 'no':
|
||||
return this.$t('api_test.definition.document.data_set.none');
|
||||
case 'module':
|
||||
return this.$t('test_track.module.module');
|
||||
case 'path':
|
||||
return this.$t('api_test.definition.api_path');
|
||||
case "NO":
|
||||
return this.$t("api_test.definition.document.data_set.none");
|
||||
case "MODULE":
|
||||
return this.$t("test_track.module.module");
|
||||
case "PATH":
|
||||
return this.$t("api_test.definition.api_path");
|
||||
}
|
||||
},
|
||||
getDetails(row) {
|
||||
if (row && row.type === 'module') {
|
||||
if (row && row.type === "MODULE") {
|
||||
if (row.details && row.details instanceof Array) {
|
||||
let value = "";
|
||||
row.details.forEach(item => {
|
||||
row.details.forEach((item) => {
|
||||
value += item.name + ",";
|
||||
})
|
||||
});
|
||||
if (value.endsWith(",")) {
|
||||
value = value.substr(0, value.length - 1);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
} else if (row && row.type === 'path' && row.details.length > 0 && row.details[0].name) {
|
||||
return row.details[0].value === 'equals' ? this.$t('commons.adv_search.operators.equals')
|
||||
: this.$t('api_test.request.assertions.contains') + "/" + row.details[0].name;
|
||||
}
|
||||
else {
|
||||
} else if (row && row.type === "PATH" && row.details.length > 0 && row.details[0].name) {
|
||||
return row.details[0].value === "equals" ? this.$t("commons.adv_search.operators.equals") : this.$t("api_test.request.assertions.contains") + row.details[0].name;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
selectRow(row) {
|
||||
if (row) {
|
||||
this.httpConfig.socket = row.socket;
|
||||
this.httpConfig.protocol = row.protocol;
|
||||
this.condition = row;
|
||||
if (row.type === "PATH" && row.details.length > 0) {
|
||||
this.pathDetails = row.details[0];
|
||||
} else if (row.type === "MODULE" && row.details.length > 0) {
|
||||
this.condition.ids = [];
|
||||
row.details.forEach((item) => {
|
||||
this.condition.ids.push(item.value);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
typeChange() {
|
||||
switch (this.condition.type) {
|
||||
case 'no':
|
||||
case "NO":
|
||||
this.condition.details = [];
|
||||
break;
|
||||
case 'module':
|
||||
case "MODULE":
|
||||
this.condition.details = [];
|
||||
break;
|
||||
case 'path':
|
||||
case "PATH":
|
||||
this.pathDetails = new KeyValue({name: "", value: "contains"});
|
||||
break;
|
||||
}
|
||||
},
|
||||
list() {
|
||||
let url = "/api/automation/module/list/" + this.projectId;
|
||||
this.result = this.$get(url, response => {
|
||||
this.result = this.$get(url, (response) => {
|
||||
if (response.data !== undefined && response.data !== null) {
|
||||
this.moduleOptions = response.data;
|
||||
}
|
||||
|
@ -177,14 +190,22 @@
|
|||
setModule(id, data) {
|
||||
if (data && data.length > 0) {
|
||||
this.condition.details = [];
|
||||
data.forEach(item => {
|
||||
data.forEach((item) => {
|
||||
this.condition.details.push(new KeyValue({name: item.name, value: item.id}));
|
||||
})
|
||||
});
|
||||
}
|
||||
},
|
||||
update() {
|
||||
const index = this.httpConfig.conditions.findIndex((d) => d.id === this.condition.id);
|
||||
let obj = {id: this.condition.id, type: this.condition.type, domain: this.httpConfig.domain, socket: this.httpConfig.socket, protocol: this.httpConfig.protocol, details: this.condition.details};
|
||||
if (index !== -1) {
|
||||
Vue.set(this.httpConfig.conditions[index], obj, 1);
|
||||
this.condition = {type: "NO", details: [new KeyValue({name: "", value: "contains"})], protocol: "", socket: "", domain: ""};
|
||||
}
|
||||
},
|
||||
add() {
|
||||
let obj = {id: getUUID(), type: this.condition.type, domain: this.httpConfig.socket};
|
||||
if (this.condition.type === 'path') {
|
||||
let obj = {id: getUUID(), type: this.condition.type, socket: this.httpConfig.socket, protocol: this.httpConfig.protocol, domain: this.httpConfig.domain,};
|
||||
if (this.condition.type === "PATH") {
|
||||
obj.details = [JSON.parse(JSON.stringify(this.pathDetails))];
|
||||
} else {
|
||||
obj.details = this.condition.details ? JSON.parse(JSON.stringify(this.condition.details)) : this.condition.details;
|
||||
|
@ -192,12 +213,12 @@
|
|||
this.httpConfig.conditions.push(obj);
|
||||
},
|
||||
remove(row) {
|
||||
const index = this.httpConfig.conditions.findIndex(d => d.id === row.id);
|
||||
const index = this.httpConfig.conditions.findIndex((d) => d.id === row.id);
|
||||
this.httpConfig.conditions.splice(index, 1);
|
||||
},
|
||||
copy(row) {
|
||||
const index = this.httpConfig.conditions.findIndex(d => d.id === row.id);
|
||||
let obj = {id: getUUID(), type: row.type, domain: row.domain, details: row.details};
|
||||
const index = this.httpConfig.conditions.findIndex((d) => d.id === row.id);
|
||||
let obj = {id: getUUID(), type: row.type, socket: row.socket, details: row.details, protocol: row.protocol, domain: row.domain,};
|
||||
if (index != -1) {
|
||||
this.httpConfig.conditions.splice(index + 1, 0, obj);
|
||||
} else {
|
||||
|
@ -206,7 +227,7 @@
|
|||
},
|
||||
validateSocket(socket) {
|
||||
if (!socket) return true;
|
||||
let urlStr = this.httpConfig.protocol + '://' + socket;
|
||||
let urlStr = this.httpConfig.protocol + "://" + socket;
|
||||
let url = {};
|
||||
try {
|
||||
url = new URL(urlStr);
|
||||
|
@ -216,9 +237,9 @@
|
|||
this.httpConfig.domain = decodeURIComponent(url.hostname);
|
||||
|
||||
this.httpConfig.port = url.port;
|
||||
let path = url.pathname === '/' ? '' : url.pathname;
|
||||
let path = url.pathname === "/" ? "" : url.pathname;
|
||||
if (url.port) {
|
||||
this.httpConfig.socket = this.httpConfig.domain + ':' + url.port + path;
|
||||
this.httpConfig.socket = this.httpConfig.domain + ":" + url.port + path;
|
||||
} else {
|
||||
this.httpConfig.socket = this.httpConfig.domain + path;
|
||||
}
|
||||
|
@ -226,17 +247,16 @@
|
|||
},
|
||||
validate() {
|
||||
let isValidate = false;
|
||||
this.$refs['httpConfig'].validate((valid) => {
|
||||
this.$refs["httpConfig"].validate((valid) => {
|
||||
isValidate = valid;
|
||||
});
|
||||
return isValidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.request-protocol-select {
|
||||
width: 90px;
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ export class Environment extends BaseConfig {
|
|||
this.name = undefined;
|
||||
this.id = undefined;
|
||||
this.config = undefined;
|
||||
|
||||
this.set(options);
|
||||
this.sets({}, options);
|
||||
}
|
||||
|
@ -63,7 +62,6 @@ export class CommonConfig extends BaseConfig {
|
|||
export class HttpConfig extends BaseConfig {
|
||||
constructor(options = {}) {
|
||||
super();
|
||||
|
||||
this.socket = undefined;
|
||||
this.domain = undefined;
|
||||
this.headers = [];
|
||||
|
@ -72,6 +70,7 @@ export class HttpConfig extends BaseConfig {
|
|||
this.conditions = [];
|
||||
this.set(options);
|
||||
this.sets({headers: KeyValue}, options);
|
||||
this.sets({conditions: KeyValue}, options);
|
||||
}
|
||||
|
||||
initOptions(options = {}) {
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
destroy-on-close
|
||||
:title="$t('load_test.runtime_config')"
|
||||
width="350px"
|
||||
:visible.sync="runModeVisible"
|
||||
>
|
||||
<div>
|
||||
<span class="ms-mode-span">{{ $t("run_mode.title") }}:</span>
|
||||
<el-radio-group v-model="runConfig.mode">
|
||||
<el-radio label="serial">{{ $t("run_mode.serial") }}</el-radio>
|
||||
<el-radio label="parallel">{{ $t("run_mode.parallel") }}</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div class="ms-mode-div" v-if="runConfig.mode === 'serial'">
|
||||
<span class="ms-mode-span">{{ $t("run_mode.other_config") }}:</span>
|
||||
<el-checkbox v-model="runConfig.onSampleError">失败停止</el-checkbox>
|
||||
</div>
|
||||
<template v-slot:footer>
|
||||
<ms-dialog-footer @cancel="close" @confirm="handleRunBatch"/>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MsDialogFooter from "@/business/components/common/components/MsDialogFooter";
|
||||
|
||||
export default {
|
||||
name: "MsPlanRunMode",
|
||||
components: {MsDialogFooter},
|
||||
data() {
|
||||
return {
|
||||
runModeVisible: false,
|
||||
runConfig: {mode: "serial", reportType: "iddReport", onSampleError: false},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.runModeVisible = true;
|
||||
},
|
||||
close() {
|
||||
this.runConfig = {mode: "serial", reportType: "iddReport", onSampleError: false};
|
||||
this.runModeVisible = false;
|
||||
},
|
||||
handleRunBatch() {
|
||||
this.$emit("handleRunBatch", this.runConfig);
|
||||
this.close();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ms-mode-span {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.ms-mode-div {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
|
@ -142,6 +142,7 @@
|
|||
<batch-edit :dialog-title="$t('test_track.case.batch_edit_case')" :type-arr="typeArr" :value-arr="valueArr"
|
||||
:select-row="selectRows" ref="batchEdit" @batchEdit="batchEdit"/>
|
||||
|
||||
<ms-plan-run-mode @handleRunBatch="handleRunBatch" ref="runMode"/>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
|
@ -187,6 +188,7 @@ import HeaderCustom from "@/business/components/common/head/HeaderCustom";
|
|||
import {Test_Plan_Api_Case} from "@/business/components/common/model/JsonData";
|
||||
import HeaderLabelOperate from "@/business/components/common/head/HeaderLabelOperate";
|
||||
import MsTableHeaderSelectPopover from "@/business/components/common/components/table/MsTableHeaderSelectPopover";
|
||||
import MsPlanRunMode from "../../../common/PlanRunMode";
|
||||
|
||||
|
||||
export default {
|
||||
|
@ -209,7 +211,8 @@ export default {
|
|||
MsContainer,
|
||||
MsBottomContainer,
|
||||
ShowMoreBtn,
|
||||
MsTableHeaderSelectPopover
|
||||
MsTableHeaderSelectPopover,
|
||||
MsPlanRunMode
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -461,22 +464,22 @@ export default {
|
|||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
getResult(data) {
|
||||
if (RESULT_MAP.get(data)) {
|
||||
return RESULT_MAP.get(data);
|
||||
} else {
|
||||
return RESULT_MAP.get("default");
|
||||
}
|
||||
});
|
||||
},
|
||||
getResult(data) {
|
||||
if (RESULT_MAP.get(data)) {
|
||||
return RESULT_MAP.get(data);
|
||||
} else {
|
||||
return RESULT_MAP.get("default");
|
||||
}
|
||||
},
|
||||
runRefresh(data) {
|
||||
this.rowLoading = "";
|
||||
this.$success(this.$t('schedule.event_success'));
|
||||
this.initTable();
|
||||
},
|
||||
},
|
||||
runRefresh(data) {
|
||||
this.rowLoading = "";
|
||||
this.$success(this.$t('schedule.event_success'));
|
||||
this.initTable();
|
||||
},
|
||||
singleRun(row) {
|
||||
this.runData = [];
|
||||
|
||||
|
@ -498,6 +501,26 @@ export default {
|
|||
this.$refs.batchEdit.open(this.selectRows.size);
|
||||
this.$refs.batchEdit.setSelectRows(this.selectRows);
|
||||
},
|
||||
getData() {
|
||||
return new Promise((resolve) => {
|
||||
let index = 1;
|
||||
this.runData = [];
|
||||
this.selectRows.forEach(row => {
|
||||
this.$get('/api/testcase/get/' + row.caseId, (response) => {
|
||||
let apiCase = response.data;
|
||||
let request = JSON.parse(apiCase.request);
|
||||
request.name = row.id;
|
||||
request.id = row.id;
|
||||
request.useEnvironment = row.environmentId;
|
||||
this.runData.push(request);
|
||||
if (this.selectRows.size === index) {
|
||||
resolve();
|
||||
}
|
||||
index++;
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
batchEdit(form) {
|
||||
let param = {};
|
||||
// 批量修改环境
|
||||
|
@ -537,58 +560,44 @@ export default {
|
|||
}
|
||||
},
|
||||
handleBatchExecute() {
|
||||
if(this.condition != null && this.condition.selectAll){
|
||||
this.$alert(this.$t('commons.option_cannot_spread_pages'), '', {
|
||||
confirmButtonText: this.$t('commons.confirm'),
|
||||
callback: (action) => {
|
||||
if (action === 'confirm') {
|
||||
this.selectRows.forEach(row => {
|
||||
this.$get('/api/testcase/get/' + row.caseId, (response) => {
|
||||
let apiCase = response.data;
|
||||
let request = JSON.parse(apiCase.request);
|
||||
request.name = row.id;
|
||||
request.id = row.id;
|
||||
request.useEnvironment = row.environmentId;
|
||||
let runData = [];
|
||||
runData.push(request);
|
||||
this.batchRun(runData, getUUID().substring(0, 8));
|
||||
});
|
||||
});
|
||||
this.$message('任务执行中,请稍后刷新查看结果');
|
||||
this.search();
|
||||
}
|
||||
}
|
||||
});
|
||||
}else {
|
||||
this.selectRows.forEach(row => {
|
||||
this.$get('/api/testcase/get/' + row.caseId, (response) => {
|
||||
let apiCase = response.data;
|
||||
let request = JSON.parse(apiCase.request);
|
||||
request.name = row.id;
|
||||
request.id = row.id;
|
||||
request.useEnvironment = row.environmentId;
|
||||
let runData = [];
|
||||
runData.push(request);
|
||||
this.batchRun(runData, getUUID().substring(0, 8));
|
||||
});
|
||||
});
|
||||
this.$message('任务执行中,请稍后刷新查看结果');
|
||||
this.search();
|
||||
}
|
||||
this.getData().then(() => {
|
||||
if (this.runData && this.runData.length > 0) {
|
||||
this.$refs.runMode.open();
|
||||
}
|
||||
});
|
||||
},
|
||||
batchRun(runData, reportId) {
|
||||
handleRunBatch(config) {
|
||||
let testPlan = new TestPlan();
|
||||
let projectId = this.$store.state.projectId;
|
||||
let threadGroup = new ThreadGroup();
|
||||
threadGroup.hashTree = [];
|
||||
testPlan.hashTree = [threadGroup];
|
||||
runData.forEach(item => {
|
||||
threadGroup.hashTree.push(item);
|
||||
});
|
||||
let reqObj = {id: reportId, testElement: testPlan, type: 'API_PLAN', reportId: "run", projectId: projectId};
|
||||
let bodyFiles = getBodyUploadFiles(reqObj, runData);
|
||||
this.$fileUpload("/api/definition/run", null, bodyFiles, reqObj, response => {
|
||||
});
|
||||
if (config.mode === 'serial') {
|
||||
testPlan.serializeThreadgroups = true;
|
||||
testPlan.hashTree = [];
|
||||
this.runData.forEach(item => {
|
||||
let threadGroup = new ThreadGroup();
|
||||
threadGroup.onSampleError = config.onSampleError;
|
||||
threadGroup.hashTree = [];
|
||||
threadGroup.hashTree.push(item);
|
||||
testPlan.hashTree.push(threadGroup);
|
||||
});
|
||||
let reqObj = {id: getUUID().substring(0, 8), testElement: testPlan, type: 'API_PLAN', reportId: "run", projectId: projectId};
|
||||
let bodyFiles = getBodyUploadFiles(reqObj, this.runData);
|
||||
this.$fileUpload("/api/definition/run", null, bodyFiles, reqObj, response => {
|
||||
});
|
||||
} else {
|
||||
testPlan.serializeThreadgroups = false;
|
||||
let threadGroup = new ThreadGroup();
|
||||
threadGroup.hashTree = [];
|
||||
testPlan.hashTree = [threadGroup];
|
||||
this.runData.forEach(item => {
|
||||
threadGroup.hashTree.push(item);
|
||||
});
|
||||
let reqObj = {id: getUUID().substring(0, 8), testElement: testPlan, type: 'API_PLAN', reportId: "run", projectId: projectId};
|
||||
let bodyFiles = getBodyUploadFiles(reqObj, this.runData);
|
||||
this.$fileUpload("/api/definition/run", null, bodyFiles, reqObj, response => {
|
||||
});
|
||||
}
|
||||
this.search();
|
||||
this.$message('任务执行中,请稍后刷新查看结果');
|
||||
},
|
||||
autoCheckStatus() { // 检查执行结果,自动更新计划状态
|
||||
if (!this.planId) {
|
||||
|
|
|
@ -101,7 +101,7 @@
|
|||
<!-- 批量编辑 -->
|
||||
<batch-edit :dialog-title="$t('test_track.case.batch_edit_case')" :type-arr="typeArr" :value-arr="valueArr"
|
||||
:select-row="selectRows" ref="batchEdit" @batchEdit="batchEdit"/>
|
||||
|
||||
<ms-plan-run-mode @handleRunBatch="handleRunBatch" ref="runMode"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -135,6 +135,7 @@ import {TEST_CASE_LIST, TEST_PLAN_SCENARIO_CASE} from "@/common/js/constants";
|
|||
import {Test_Plan_Scenario_Case, Track_Test_Case} from "@/business/components/common/model/JsonData";
|
||||
import HeaderLabelOperate from "@/business/components/common/head/HeaderLabelOperate";
|
||||
import BatchEdit from "@/business/components/track/case/components/BatchEdit";
|
||||
import MsPlanRunMode from "../../../common/PlanRunMode";
|
||||
import MsTableHeaderSelectPopover from "@/business/components/common/components/table/MsTableHeaderSelectPopover";
|
||||
|
||||
export default {
|
||||
|
@ -153,6 +154,7 @@ export default {
|
|||
MsScenarioExtendButtons,
|
||||
MsTestPlanList,
|
||||
BatchEdit,
|
||||
MsPlanRunMode,
|
||||
MsTableHeaderSelectPopover
|
||||
},
|
||||
props: {
|
||||
|
@ -292,49 +294,32 @@ export default {
|
|||
})
|
||||
},
|
||||
handleBatchExecute() {
|
||||
// 与同事对接此处功能时得知前段入口取消,if (this.reviewId) {} 函数不会执行。 暂时先注释掉。 By.Song Tianyang
|
||||
// if (this.reviewId) {
|
||||
// this.selectRows.forEach(row => {
|
||||
// let param = this.buildExecuteParam(row);
|
||||
// this.$post("/test/case/review/scenario/case/run", param, response => {
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
if(this.condition != null && this.condition.selectAll){
|
||||
this.$alert(this.$t('commons.option_cannot_spread_pages'), '', {
|
||||
confirmButtonText: this.$t('commons.confirm'),
|
||||
callback: (action) => {
|
||||
if (action === 'confirm') {
|
||||
if (this.planId) {
|
||||
this.selectRows.forEach(row => {
|
||||
let param = this.buildExecuteParam(row);
|
||||
this.$post("/test/plan/scenario/case/run", param, response => {
|
||||
});
|
||||
});
|
||||
}
|
||||
this.$message('任务执行中,请稍后刷新查看结果');
|
||||
this.search();
|
||||
}
|
||||
}
|
||||
this.$refs.runMode.open();
|
||||
},
|
||||
handleRunBatch(config){
|
||||
if (this.reviewId) {
|
||||
let param = {config : config,planCaseIds:[]};
|
||||
this.selectRows.forEach(row => {
|
||||
this.buildExecuteParam(param,row);
|
||||
});
|
||||
}else {
|
||||
if (this.planId) {
|
||||
this.selectRows.forEach(row => {
|
||||
let param = this.buildExecuteParam(row);
|
||||
this.$post("/test/plan/scenario/case/run", param, response => {
|
||||
});
|
||||
});
|
||||
}
|
||||
this.$message('任务执行中,请稍后刷新查看结果');
|
||||
this.search();
|
||||
this.$post("/test/case/review/scenario/case/run", param, response => {});
|
||||
}
|
||||
if (this.planId) {
|
||||
let param = {config : config,planCaseIds:[]};
|
||||
this.selectRows.forEach(row => {
|
||||
this.buildExecuteParam(param,row);
|
||||
});
|
||||
console.log(param)
|
||||
|
||||
this.$post("/test/plan/scenario/case/run", param, response => {});
|
||||
}
|
||||
this.$message('任务执行中,请稍后刷新查看结果');
|
||||
this.search();
|
||||
},
|
||||
execute(row) {
|
||||
this.infoDb = false;
|
||||
let param = this.buildExecuteParam(row);
|
||||
console.log(param)
|
||||
let param ={planCaseIds: []};
|
||||
this.buildExecuteParam(param,row);
|
||||
if (this.planId) {
|
||||
this.$post("/test/plan/scenario/case/run", param, response => {
|
||||
this.runVisible = true;
|
||||
|
@ -348,14 +333,11 @@ export default {
|
|||
});
|
||||
}
|
||||
},
|
||||
buildExecuteParam(row) {
|
||||
let param = {};
|
||||
buildExecuteParam(param,row) {
|
||||
// param.id = row.id;
|
||||
param.id = getUUID();
|
||||
param.planScenarioId = row.id;
|
||||
console.log(row.id)
|
||||
param.projectId = row.projectId;
|
||||
param.planCaseIds = [];
|
||||
param.planCaseIds.push(row.id);
|
||||
return param;
|
||||
},
|
||||
|
|
|
@ -1677,5 +1677,14 @@ export default {
|
|||
header_display_field: 'Header display field',
|
||||
fields_to_be_selected: 'Fields to be selected',
|
||||
selected_fields: 'Selected fields'
|
||||
},
|
||||
run_mode: {
|
||||
title: "Mode",
|
||||
serial: "Serial",
|
||||
parallel: "Parallel",
|
||||
other_config: "Other config",
|
||||
idd_report: "Report",
|
||||
set_report: "Set report",
|
||||
report_name: "Report name",
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1680,5 +1680,14 @@ export default {
|
|||
header_display_field: '表头显示字段',
|
||||
fields_to_be_selected: '待选字段',
|
||||
selected_fields: '已选字段'
|
||||
},
|
||||
run_mode: {
|
||||
title: "模式",
|
||||
serial: "串行",
|
||||
parallel: "并行",
|
||||
other_config: "其他配置",
|
||||
idd_report: "独立报告",
|
||||
set_report: "集合报告",
|
||||
report_name: "报告名称",
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1678,6 +1678,14 @@ export default {
|
|||
header_display_field: '表頭顯示欄位',
|
||||
fields_to_be_selected: '待選欄位',
|
||||
selected_fields: '已選欄位'
|
||||
},
|
||||
run_mode: {
|
||||
title: "模式",
|
||||
serial: "串行",
|
||||
parallel: "並行",
|
||||
other_config: "其他配置",
|
||||
idd_report: "獨立報告",
|
||||
set_report: "集合報告",
|
||||
report_name: "報告名稱",
|
||||
}
|
||||
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue