This commit is contained in:
wenyann 2021-03-26 18:13:08 +08:00
commit ca67a3ed56
25 changed files with 826 additions and 76 deletions

View File

@ -7,12 +7,12 @@ import io.metersphere.api.dto.definition.*;
import io.metersphere.api.service.ApiTestCaseService;
import io.metersphere.base.domain.ApiTestCase;
import io.metersphere.base.domain.ApiTestCaseWithBLOBs;
import io.metersphere.commons.constants.ApiRunMode;
import io.metersphere.commons.constants.RoleConstants;
import io.metersphere.commons.utils.PageUtils;
import io.metersphere.commons.utils.Pager;
import io.metersphere.commons.utils.SessionUtils;
import io.metersphere.track.request.testcase.ApiCaseRelevanceRequest;
import io.metersphere.track.service.TestPlanApiCaseService;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.*;
@ -29,7 +29,8 @@ public class ApiTestCaseController {
@Resource
private ApiTestCaseService apiTestCaseService;
@Resource
private TestPlanApiCaseService testPlanApiCaseService;
@PostMapping("/list")
public List<ApiTestCaseResult> list(@RequestBody ApiTestCaseRequest request) {
request.setWorkspaceId(SessionUtils.getCurrentWorkspaceId());
@ -48,6 +49,12 @@ public class ApiTestCaseController {
return null;
}
}
@GetMapping("/getStateByTestPlan/{id}")
public String getStateByTestPlan(@PathVariable String id ) {
String status=testPlanApiCaseService.getState(id);
return status;
}
@PostMapping("/list/{goPage}/{pageSize}")
public Pager<List<ApiTestCaseDTO>> listSimple(@PathVariable int goPage, @PathVariable int pageSize, @RequestBody ApiTestCaseRequest request) {
@ -131,7 +138,8 @@ public class ApiTestCaseController {
return apiTestCaseService.run(request);
}
@GetMapping(value = "/jenkins/exec/result/{id}")
public String getExecResult(@PathVariable String id) {
return apiTestCaseService.getExecResult(id);
public String getExecResult(@PathVariable String id) {
return apiTestCaseService.getExecResult(id);
}
}

View File

@ -75,6 +75,8 @@ import org.apache.jorphan.collections.HashTree;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
public class MsJmeterParser extends ApiImportAbstractParser<ScenarioImport> {
@ -132,12 +134,66 @@ public class MsJmeterParser extends ApiImportAbstractParser<ScenarioImport> {
return (HashTree) field.get(scriptWrapper);
}
public boolean isProtocolDefaultPort(HTTPSamplerProxy source) {
String portAsString = source.getPropertyAsString("HTTPSampler.port");
if (portAsString != null && !portAsString.isEmpty()) {
return false;
} else {
return true;
}
}
public String url(String protocol, String host, String port, String file) {
protocol = protocol.toLowerCase();
if (StringUtils.isNotEmpty(file) && !file.startsWith("/")) {
file += "/";
}
return protocol + "://" + host + ":" + port + file;
}
public String getUrl(HTTPSamplerProxy source) throws MalformedURLException {
String path = source.getPath();
if (!path.startsWith("http://") && !path.startsWith("https://")) {
String domain = source.getDomain();
String protocol = source.getProtocol();
String method = source.getMethod();
StringBuilder pathAndQuery = new StringBuilder(100);
if ("file".equalsIgnoreCase(protocol)) {
domain = null;
} else if (!path.startsWith("/")) {
pathAndQuery.append('/');
}
pathAndQuery.append(path);
if ("GET".equals(method) || "DELETE".equals(method) || "OPTIONS".equals(method)) {
String queryString = source.getQueryString(source.getContentEncoding());
if (queryString.length() > 0) {
if (path.contains("?")) {
pathAndQuery.append("&");
} else {
pathAndQuery.append("?");
}
pathAndQuery.append(queryString);
}
}
String portAsString = source.getPropertyAsString("HTTPSampler.port");
return this.isProtocolDefaultPort(source) ? new URL(protocol, domain, pathAndQuery.toString()).toExternalForm() : this.url(protocol, domain, portAsString, pathAndQuery.toString());
} else {
return new URL(path).toExternalForm();
}
}
private void convertHttpSampler(MsHTTPSamplerProxy samplerProxy, Object key) {
try {
HTTPSamplerProxy source = (HTTPSamplerProxy) key;
BeanUtils.copyBean(samplerProxy, source);
samplerProxy.setRest(new ArrayList<KeyValue>(){{this.add(new KeyValue());}});
samplerProxy.setArguments(new ArrayList<KeyValue>(){{this.add(new KeyValue());}});
samplerProxy.setRest(new ArrayList<KeyValue>() {{
this.add(new KeyValue());
}});
samplerProxy.setArguments(new ArrayList<KeyValue>() {{
this.add(new KeyValue());
}});
if (source != null && source.getHTTPFiles().length > 0) {
samplerProxy.getBody().initBinary();
samplerProxy.getBody().setType(Body.FORM_DATA);
@ -158,7 +214,8 @@ public class MsJmeterParser extends ApiImportAbstractParser<ScenarioImport> {
samplerProxy.getBody().setKvs(keyValues);
}
samplerProxy.setProtocol(RequestType.HTTP);
samplerProxy.setPort(source.getPort() + "");
samplerProxy.setPort(source.getPropertyAsString("HTTPSampler.port"));
samplerProxy.setDomain(source.getDomain());
if (source.getArguments() != null) {
if (source.getPostBodyRaw()) {
samplerProxy.getBody().setType(Body.RAW);
@ -178,10 +235,10 @@ public class MsJmeterParser extends ApiImportAbstractParser<ScenarioImport> {
}
samplerProxy.getBody().initBinary();
}
samplerProxy.setPath("");
// samplerProxy.setPath(source.getPath());
samplerProxy.setMethod(source.getMethod());
if (source.getUrl() != null) {
samplerProxy.setUrl(source.getUrl().toString());
if (this.getUrl(source) != null) {
samplerProxy.setUrl(this.getUrl(source));
}
samplerProxy.setId(UUID.randomUUID().toString());
samplerProxy.setType("HTTPSamplerProxy");

View File

@ -91,6 +91,9 @@ public class MsHTTPSamplerProxy extends MsTestElement {
@JSONField(ordinal = 36)
private MsAuthManager authManager;
@JSONField(ordinal = 37)
private boolean urlOrPath;
@Override
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, ParameterConfig config) {
// 非导出操作且不是启用状态则跳过执行
@ -140,23 +143,26 @@ public class MsHTTPSamplerProxy extends MsTestElement {
url = this.getUrl();
isUrl = true;
}
URL urlObject = new URL(url);
if (isUrl) {
if (StringUtils.isNotEmpty(this.getPort()) && this.getPort().startsWith("${")) {
url.replaceAll(this.getPort(), "10990");
}
URL urlObject = new URL(url);
sampler.setDomain(URLDecoder.decode(urlObject.getHost(), "UTF-8"));
if (urlObject.getPort() > 0) {
if (urlObject.getPort() > 0 && urlObject.getPort() != 10990 && StringUtils.isNotEmpty(this.getPort()) && this.getPort().startsWith("${")) {
sampler.setPort(urlObject.getPort());
} else {
sampler.setProperty("HTTPSampler.port", this.getPort());
}
sampler.setProtocol(urlObject.getProtocol());
sampler.setPath(urlObject.getPath());
} else {
sampler.setDomain(config.getConfig().get(this.getProjectId()).getHttpConfig().getDomain());
sampler.setPort(config.getConfig().get(this.getProjectId()).getHttpConfig().getPort());
sampler.setProtocol(config.getConfig().get(this.getProjectId()).getHttpConfig().getProtocol());
sampler.setPath(this.getPath());
}
String envPath = StringUtils.equals(urlObject.getPath(), "/") ? "" : urlObject.getPath();
if (StringUtils.isNotBlank(this.getPath()) && !isUrl) {
envPath += this.getPath();
sampler.setPath(envPath);
}
String envPath = sampler.getPath();
if (CollectionUtils.isNotEmpty(this.getRest()) && this.isRest()) {
envPath = getRestParameters(URLDecoder.decode(envPath, "UTF-8"));
sampler.setPath(envPath);
@ -177,9 +183,16 @@ public class MsHTTPSamplerProxy extends MsTestElement {
if (!url.startsWith("http://") && !url.startsWith("https://")) {
url = "http://" + url;
}
if (StringUtils.isNotEmpty(this.getPort()) && this.getPort().startsWith("${")) {
url.replaceAll(this.getPort(), "10990");
}
URL urlObject = new URL(url);
sampler.setDomain(URLDecoder.decode(urlObject.getHost(), "UTF-8"));
sampler.setPort(urlObject.getPort());
if (urlObject.getPort() > 0 && urlObject.getPort() != 10990 && StringUtils.isNotEmpty(this.getPort()) && this.getPort().startsWith("${")) {
sampler.setPort(urlObject.getPort());
} else {
sampler.setProperty("HTTPSampler.port", this.getPort());
}
sampler.setProtocol(urlObject.getProtocol());
String envPath = StringUtils.equals(urlObject.getPath(), "/") ? "" : urlObject.getPath();
sampler.setPath(envPath);
@ -327,10 +340,16 @@ public class MsHTTPSamplerProxy extends MsTestElement {
}
public boolean isURL(String str) {
//转换为小写
try {
new URL(str);
return true;
String regex = "^((https|http|ftp|rtsp|mms)?://)"
+ "?(([0-9a-z_!~*'().&=+$%-]+: )?[0-9a-z_!~*'().&=+$%-]+@)?"
+ "(([0-9]{1,3}\\.){3}[0-9]{1,3}" + "|" + "([0-9a-z_!~*'()-]+\\.)*"
+ "([0-9a-z][0-9a-z-]{0,61})?[0-9a-z]\\."
+ "[a-z]{2,6})"
+ "(:[0-9]{1,5})?"
+ "((/?)|"
+ "(/[0-9a-z_!~*'().;?:@&=+$,%#-]+)+/?)$";
return str.matches(regex) || (str.matches("^(http|https|ftp)://.*$") && str.matches(".*://\\$\\{.*$"));
} catch (Exception e) {
return false;
}
@ -339,5 +358,5 @@ public class MsHTTPSamplerProxy extends MsTestElement {
private boolean isRest() {
return this.getRest().stream().filter(KeyValue::isEnable).filter(KeyValue::isValid).toArray().length > 0;
}
}

View File

@ -1,12 +1,10 @@
package io.metersphere.api.jmeter;
import io.metersphere.api.dto.definition.ApiTestCaseInfo;
import io.metersphere.api.dto.scenario.request.RequestType;
import io.metersphere.api.service.*;
import io.metersphere.base.domain.ApiDefinitionExecResult;
import io.metersphere.base.domain.ApiScenarioReport;
import io.metersphere.base.domain.ApiTestReport;
import io.metersphere.base.domain.TestPlanReport;
import io.metersphere.commons.constants.*;
import io.metersphere.commons.utils.CommonBeanFactory;
import io.metersphere.commons.utils.LogUtil;
@ -16,7 +14,6 @@ import io.metersphere.notice.sender.NoticeModel;
import io.metersphere.notice.service.NoticeSendService;
import io.metersphere.service.SystemParameterService;
import io.metersphere.track.service.TestPlanReportService;
import io.metersphere.track.service.TestPlanService;
import io.metersphere.track.service.TestPlanTestCaseService;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
@ -250,7 +247,10 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl
}
}
sendTask(report, reportUrl, testResult);
if (StringUtils.equals(ReportTriggerMode.API.name(), report.getTriggerMode())||StringUtils.equals(ReportTriggerMode.SCHEDULE.name(), report.getTriggerMode())) {
sendTask(report, reportUrl, testResult);
}
}
private static void sendTask(ApiTestReport report, String reportUrl, TestResult testResult) {

View File

@ -19,6 +19,7 @@ import io.metersphere.api.jmeter.JMeterService;
import io.metersphere.base.domain.*;
import io.metersphere.base.mapper.*;
import io.metersphere.base.mapper.ext.*;
import io.metersphere.commons.constants.ApiRunMode;
import io.metersphere.commons.constants.TestPlanStatus;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.*;
@ -551,7 +552,14 @@ public class ApiTestCaseService {
}
public String run(RunCaseRequest request) {
ApiTestCaseWithBLOBs testCaseWithBLOBs = apiTestCaseMapper.selectByPrimaryKey(request.getCaseId());
ApiTestCaseWithBLOBs testCaseWithBLOBs=new ApiTestCaseWithBLOBs();
if(StringUtils.equals(request.getRunMode(), ApiRunMode.JENKINS_API_PLAN.name())){
testCaseWithBLOBs= apiTestCaseMapper.selectByPrimaryKey(request.getReportId());
request.setCaseId(request.getReportId());
}else{
testCaseWithBLOBs= apiTestCaseMapper.selectByPrimaryKey(request.getCaseId());
}
// 多态JSON普通转换会丢失内容需要通过 ObjectMapper 获取
if (testCaseWithBLOBs != null && StringUtils.isNotEmpty(testCaseWithBLOBs.getRequest())) {
try {

View File

@ -91,6 +91,9 @@
<if test="reportRequest.projectId != null">
AND project.id = #{reportRequest.projectId,jdbcType=VARCHAR}
</if>
<if test="reportRequest.testId != null">
AND ltr.test_id = #{reportRequest.testId,jdbcType=VARCHAR}
</if>
<if test="reportRequest.filters != null and reportRequest.filters.size() > 0">
<foreach collection="reportRequest.filters.entrySet()" index="key" item="values">
<if test="values != null and values.size() > 0">

View File

@ -274,7 +274,7 @@
#{value}
</foreach>
</when>
<when test="key=='status'">
<when test="key=='reviewStatus'">
and test_case.review_status in
<foreach collection="values" item="value" separator="," open="(" close=")">
#{value}

View File

@ -12,6 +12,7 @@ import java.util.Map;
public class ReportRequest {
private String name;
private String workspaceId;
private String testId;
private String userId;
private List<OrderRequest> orders;
private Map<String, List<String>> filters;

View File

@ -1,21 +1,13 @@
package io.metersphere.track.service;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import io.metersphere.api.dto.definition.ApiTestCaseDTO;
import io.metersphere.api.dto.definition.ApiTestCaseRequest;
import io.metersphere.api.dto.definition.RunDefinitionRequest;
import io.metersphere.api.dto.definition.TestPlanApiCaseDTO;
import io.metersphere.api.dto.definition.request.MsTestElement;
import io.metersphere.api.dto.definition.request.MsTestPlan;
import io.metersphere.api.dto.definition.request.MsThreadGroup;
import io.metersphere.api.service.ApiDefinitionExecResultService;
import io.metersphere.api.service.ApiDefinitionService;
import io.metersphere.api.service.ApiTestCaseService;
import io.metersphere.base.domain.ApiTestCaseExample;
import io.metersphere.base.domain.ApiTestCaseWithBLOBs;
import io.metersphere.base.domain.TestPlanApiCase;
import io.metersphere.base.domain.TestPlanApiCaseExample;
import io.metersphere.base.mapper.TestPlanApiCaseMapper;
@ -25,15 +17,16 @@ import io.metersphere.commons.utils.Pager;
import io.metersphere.commons.utils.ServiceUtils;
import io.metersphere.commons.utils.SessionUtils;
import io.metersphere.track.request.testcase.TestPlanApiCaseBatchRequest;
import org.apache.jmeter.testelement.TestElement;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Service
@Transactional(rollbackFor = Exception.class)
@ -150,4 +143,11 @@ public class TestPlanApiCaseService {
});
}
}
public String getState(String id) {
TestPlanApiCaseExample example = new TestPlanApiCaseExample();
example.createCriteria().andApiCaseIdEqualTo(id);
return testPlanApiCaseMapper.selectByExample(example).get(0).getStatus();
}
}

View File

@ -926,7 +926,7 @@
return bodyUploadFiles;
},
editScenario() {
return new Promise((resolve, reject) => {
return new Promise((resolve) => {
document.getElementById("inputDelay").focus(); // input
this.$refs['currentScenario'].validate((valid) => {
if (valid) {

View File

@ -0,0 +1,70 @@
<template>
<ms-container>
<ms-main-container>
<el-row :gutter="20">
<el-col :span="24">
<overview-compare-card ref="overviewCard"/>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<load-compare-card ref="loadCard"/>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<response-time-compare-card ref="responseTimeCard"/>
</el-col>
</el-row>
</ms-main-container>
</ms-container>
</template>
<script>
import MsContainer from "@/business/components/common/components/MsContainer";
import MsMainContainer from "@/business/components/common/components/MsMainContainer";
import {checkoutTestManagerOrTestUser} from "@/common/js/utils";
import OverviewCompareCard from "@/business/components/performance/report/components/OverviewCompareCard";
import MsChart from "@/business/components/common/chart/MsChart";
import LoadCompareCard from "@/business/components/performance/report/components/LoadCompareCard";
import ResponseTimeCompareCard from "@/business/components/performance/report/components/ResponseTimeCompareCard";
export default {
name: "PerformanceReportCompare",
components: {ResponseTimeCompareCard, LoadCompareCard, MsChart, OverviewCompareCard, MsMainContainer, MsContainer},
mounted() {
this.init();
},
computed: {
isReadOnly() {
return !checkoutTestManagerOrTestUser();
}
},
data() {
return {}
},
methods: {
init() {
this.$refs.overviewCard.initTable();
this.$refs.loadCard.initCard();
this.$refs.responseTimeCard.initCard();
}
},
watch: {
'$route'(to) {
if (to.name !== "ReportCompare") {
return;
}
this.init();
}
}
}
</script>
<style scoped>
.el-row {
padding-bottom: 20px;
}
</style>

View File

@ -25,13 +25,12 @@
<el-button :disabled="isReadOnly" type="info" plain size="mini" @click="handleExport(reportName)">
{{ $t('test_track.plan_view.export_report') }}
</el-button>
<el-button :disabled="isReadOnly || report.status !== 'Completed'" type="default" plain size="mini" @click="compareReports()">
{{ $t('report.compare') }}
</el-button>
<el-button :disabled="isReadOnly" type="warning" plain size="mini" @click="downloadJtl()">
{{ $t('report.downloadJtl') }}
</el-button>
<!--<el-button :disabled="isReadOnly" type="warning" plain size="mini">-->
<!--{{$t('report.compare')}}-->
<!--</el-button>-->
</el-row>
</el-col>
<el-col :span="8">
@ -83,6 +82,7 @@
</div>
</el-dialog>
</ms-main-container>
<same-test-reports ref="compareReports"/>
</ms-container>
</template>
@ -99,11 +99,13 @@ import {checkoutTestManagerOrTestUser, exportPdf} from "@/common/js/utils";
import html2canvas from 'html2canvas';
import MsPerformanceReportExport from "./PerformanceReportExport";
import {Message} from "element-ui";
import SameTestReports from "@/business/components/performance/report/components/SameTestReports";
export default {
name: "PerformanceReportView",
components: {
SameTestReports,
MsPerformanceReportExport,
MsReportErrorLog,
MsReportLogDetails,
@ -312,6 +314,9 @@ export default {
Message.error({message: JSON.parse(data).message || e.message, showClose: true});
});
});
},
compareReports() {
this.$refs.compareReports.open(this.report);
}
},
created() {
@ -327,6 +332,8 @@ export default {
this.$set(this.report, "id", this.reportId);
this.$set(this.report, "status", data.status);
this.$set(this.report, "testId", data.testId);
this.$set(this.report, "name", data.name);
this.$set(this.report, "createTime", data.createTime);
this.$set(this.report, "loadConfiguration", data.loadConfiguration);
this.checkReportStatus(data.status);
if (this.status === "Completed" || this.status === "Running") {

View File

@ -25,37 +25,32 @@
<el-table-column
prop="name"
:label="$t('commons.name')"
width="150"
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="testName"
:label="$t('report.test_name')"
width="150"
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="projectName"
:label="$t('report.project_name')"
width="150"
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="userName"
:label="$t('report.user_name')"
width="150"
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="createTime"
sortable
width="250"
:label="$t('commons.create_time')">
<template v-slot:default="scope">
<span>{{ scope.row.createTime | timestampFormatDate }}</span>
</template>
</el-table-column>
<el-table-column prop="triggerMode" width="150" :label="'触发方式'" column-key="triggerMode"
<el-table-column prop="triggerMode" width="150" :label="$t('test_track.report.list.trigger_mode')" column-key="triggerMode"
:filters="triggerFilters">
<template v-slot:default="scope">
<report-trigger-mode-item :trigger-mode="scope.row.triggerMode"/>
@ -76,6 +71,8 @@
<template v-slot:default="scope">
<ms-table-operator-button :tip="$t('api_report.detail')" icon="el-icon-s-data"
@exec="handleEdit(scope.row)" type="primary"/>
<ms-table-operator-button :tip="$t('load_test.report.diff')" icon="el-icon-s-operation"
@exec="handleDiff(scope.row)" type="warning"/>
<ms-table-operator-button :is-tester-permission="true" :tip="$t('api_report.delete')"
icon="el-icon-delete" @exec="handleDelete(scope.row)" type="danger"/>
</template>
@ -85,6 +82,7 @@
:total="total"/>
</el-card>
</ms-main-container>
<same-test-reports ref="compareReports"/>
</ms-container>
</template>
@ -101,11 +99,14 @@ import MsTableHeader from "../../common/components/MsTableHeader";
import {LIST_CHANGE, PerformanceEvent} from "@/business/components/common/head/ListEvent";
import ShowMoreBtn from "../../track/case/components/ShowMoreBtn";
import {_filter, _sort} from "@/common/js/tableUtils";
import MsDialogFooter from "@/business/components/common/components/MsDialogFooter";
import SameTestReports from "@/business/components/performance/report/components/SameTestReports";
export default {
name: "PerformanceTestReportList",
components: {
SameTestReports,
MsDialogFooter,
MsTableHeader,
ReportTriggerModeItem,
MsTableOperatorButton,
@ -200,6 +201,9 @@ export default {
}
});
},
handleDiff(report) {
this.$refs.compareReports.open(report);
},
_handleDeleteNoMsg(report) {
this.result = this.$post(this.deletePath + report.id, {}, () => {
this.initTableData();

View File

@ -0,0 +1,182 @@
<template>
<el-card>
<template v-slot:header>
<span class="title">Load</span>
</template>
<div v-for="(option, index) in loadList" :key="index">
<ms-chart ref="chart1" :options="option" :autoresize="true"></ms-chart>
</div>
</el-card>
</template>
<script>
import MsChart from "@/business/components/common/chart/MsChart";
export default {
name: "LoadCompareCard",
components: {MsChart},
data() {
return {
loadList: []
}
},
methods: {
initCard() {
this.loadList = [];
this.reportId = this.$route.path.split('/')[4];
this.compareReports = JSON.parse(localStorage.getItem("compareReports"));
this.compareReports.forEach(report => {
this.initOverview(report);
})
},
initOverview(report) {
this.$get("/performance/report/content/load_chart/" + report.id).then(res => {
let data = res.data.data;
let yAxisList = data.filter(m => m.yAxis2 === -1).map(m => m.yAxis);
let yAxis2List = data.filter(m => m.yAxis === -1).map(m => m.yAxis2);
let yAxisListMax = this._getChartMax(yAxisList);
let yAxis2ListMax = this._getChartMax(yAxis2List);
let yAxisIndex0List = data.filter(m => m.yAxis2 === -1).map(m => m.groupName);
yAxisIndex0List = this._unique(yAxisIndex0List);
let yAxisIndex1List = data.filter(m => m.yAxis === -1).map(m => m.groupName);
yAxisIndex1List = this._unique(yAxisIndex1List);
let loadOption = {
title: {
text: report.name + " " + this.$options.filters['timestampFormatDate'](report.createTime),
left: 'center',
top: 20,
textStyle: {
color: '#65A2FF'
},
},
tooltip: {
show: true,
trigger: 'axis',
// extraCssText: 'z-index: 999;',
confine: true,
},
legend: {},
xAxis: {},
yAxis: [{
name: 'User',
type: 'value',
min: 0,
max: yAxisListMax,
splitNumber: 5,
interval: yAxisListMax / 5
},
{
name: 'Transactions/s',
type: 'value',
splitNumber: 5,
min: 0,
max: yAxis2ListMax,
interval: yAxis2ListMax / 5
}
],
series: []
};
let setting = {
series: [
{
name: 'users',
color: '#0CA74A',
},
{
name: 'hits',
yAxisIndex: '1',
color: '#65A2FF',
},
{
name: 'errors',
yAxisIndex: '1',
color: '#E6113C',
}
]
}
yAxisIndex0List.forEach(item => {
setting["series"].splice(0, 0, {name: item, yAxisIndex: '0'})
})
yAxisIndex1List.forEach(item => {
setting["series"].splice(0, 0, {name: item, yAxisIndex: '1'})
})
this.loadList.push(this.generateOption(loadOption, data, setting));
}).catch(() => {
this.loadList = [];
})
},
generateOption(option, data, setting) {
let chartData = data;
let seriesArray = [];
for (let set in setting) {
if (set === "series") {
seriesArray = setting[set];
continue;
}
this.$set(option, set, setting[set]);
}
let legend = [], series = {}, xAxis = [], seriesData = [];
chartData.forEach(item => {
if (!xAxis.includes(item.xAxis)) {
xAxis.push(item.xAxis);
}
xAxis.sort()
let name = item.groupName
if (!legend.includes(name)) {
legend.push(name)
series[name] = []
}
if (item.yAxis === -1) {
series[name].splice(xAxis.indexOf(item.xAxis), 0, [item.xAxis, item.yAxis2.toFixed(2)]);
} else {
series[name].splice(xAxis.indexOf(item.xAxis), 0, [item.xAxis, item.yAxis.toFixed(2)]);
}
})
this.$set(option.legend, "data", legend);
this.$set(option.legend, "type", "scroll");
this.$set(option.legend, "bottom", "10px");
this.$set(option.xAxis, "data", xAxis);
for (let name in series) {
let d = series[name];
d.sort((a, b) => a[0].localeCompare(b[0]));
let items = {
name: name,
type: 'line',
data: d,
smooth: true
};
let seriesArrayNames = seriesArray.map(m => m.name);
if (seriesArrayNames.includes(name)) {
for (let j = 0; j < seriesArray.length; j++) {
let seriesObj = seriesArray[j];
if (seriesObj['name'] === name) {
Object.assign(items, seriesObj);
}
}
}
seriesData.push(items);
}
this.$set(option, "series", seriesData);
return option;
},
_getChartMax(arr) {
const max = Math.max(...arr);
return Math.ceil(max / 4.5) * 5;
},
_unique(arr) {
return Array.from(new Set(arr));
}
}
}
</script>
<style scoped>
.echarts {
width: 100%;
height: 300px;
}
</style>

View File

@ -0,0 +1,67 @@
<template>
<el-card class="table-card">
<template v-slot:header>
<span class="title">Overview</span>
</template>
<el-table border :data="overviewList" class="adjust-table test-content">
<el-table-column prop="name" :label="$t('commons.name')"/>
<el-table-column prop="createTime" :label="$t('commons.create_time')">
<template v-slot:default="scope">
<span>{{ scope.row.createTime | timestampFormatDate }}</span>
</template>
</el-table-column>
<el-table-column prop="maxUsers" label="Max Users"/>
<el-table-column prop="avgTransactions" label="Avg.Transactions"/>
<el-table-column prop="errors" label="Errors"/>
<el-table-column prop="avgResponseTime" label="Avg.Response Time"/>
<el-table-column prop="responseTime90" label="90% Response Time"/>
<el-table-column prop="avgBandwidth" label="Avg.Bandwidth"/>
</el-table>
</el-card>
</template>
<script>
export default {
name: "OverviewCompareCard",
data() {
return {
reportId: null,
compareReports: [],
overviewList: [],
};
},
methods: {
initTable() {
this.overviewList = [];
this.reportId = this.$route.path.split('/')[4];
this.compareReports = JSON.parse(localStorage.getItem("compareReports"));
this.compareReports.forEach(report => {
this.initOverview(report);
})
},
initOverview(report) {
this.$get("/performance/report/content/testoverview/" + report.id).then(res => {
let data = res.data.data;
this.overviewList.push({
name: report.name,
createTime: report.createTime,
maxUsers: data.maxUsers,
avgThroughput: data.avgThroughput,
avgTransactions: data.avgTransactions,
errors: data.errors,
avgResponseTime: data.avgResponseTime,
responseTime90: data.responseTime90,
avgBandwidth: data.avgBandwidth,
})
}).catch(() => {
})
}
}
}
</script>
<style scoped>
</style>

View File

@ -37,7 +37,7 @@
<el-table-column
prop="ko"
label="KO"
label="FAIL"
align="center"
/>

View File

@ -0,0 +1,188 @@
<template>
<el-card>
<template v-slot:header>
<span class="title">Response Time</span>
</template>
<div v-for="(option, index) in responseTimeList" :key="index">
<ms-chart ref="chart1" :options="option" :autoresize="true"></ms-chart>
</div>
</el-card>
</template>
<script>
import MsChart from "@/business/components/common/chart/MsChart";
export default {
name: "ResponseTimeCompareCard",
components: {MsChart},
data() {
return {
responseTimeList: []
}
},
methods: {
initCard() {
this.responseTimeList = [];
this.reportId = this.$route.path.split('/')[4];
this.compareReports = JSON.parse(localStorage.getItem("compareReports"));
this.compareReports.forEach(report => {
this.initOverview(report);
})
},
initOverview(report) {
this.$get("/performance/report/content/res_chart/" + report.id).then(res => {
let data = res.data.data;
let yAxisList = data.filter(m => m.yAxis2 === -1).map(m => m.yAxis);
let yAxis2List = data.filter(m => m.yAxis === -1).map(m => m.yAxis2);
let yAxisListMax = this._getChartMax(yAxisList);
let yAxis2ListMax = this._getChartMax(yAxis2List);
let yAxisIndex0List = data.filter(m => m.yAxis2 === -1).map(m => m.groupName);
yAxisIndex0List = this._unique(yAxisIndex0List);
let yAxisIndex1List = data.filter(m => m.yAxis === -1).map(m => m.groupName);
yAxisIndex1List = this._unique(yAxisIndex1List);
let resOption = {
title: {
text: report.name + " " + this.$options.filters['timestampFormatDate'](report.createTime),
left: 'center',
top: 20,
textStyle: {
color: '#99743C'
},
},
tooltip: {
show: true,
trigger: 'axis',
// extraCssText: 'z-index: 999;',
confine: true,
formatter: function (params, ticket, callback) {
let result = "";
let name = params[0].name;
result += name + "<br/>";
for (let i = 0; i < params.length; i++) {
let seriesName = params[i].seriesName;
if (seriesName.length > 100) {
seriesName = seriesName.substring(0, 100);
}
let value = params[i].value;
let marker = params[i].marker;
result += marker + seriesName + ": " + value[1] + "<br/>";
}
return result;
}
},
legend: {},
xAxis: {},
yAxis: [{
name: 'User',
type: 'value',
min: 0,
max: yAxisListMax,
interval: yAxisListMax / 5
},
{
name: 'Response Time',
type: 'value',
min: 0,
max: yAxis2ListMax,
interval: yAxis2ListMax / 5
}
],
series: []
}
let setting = {
series: [
{
name: 'users',
color: '#0CA74A',
}
]
}
yAxisIndex0List.forEach(item => {
setting["series"].splice(0, 0, {name: item, yAxisIndex: '0'})
})
yAxisIndex1List.forEach(item => {
setting["series"].splice(0, 0, {name: item, yAxisIndex: '1'})
})
this.responseTimeList.push(this.generateOption(resOption, data, setting));
}).catch(() => {
this.responseTimeList = [];
})
},
generateOption(option, data, setting) {
let chartData = data;
let seriesArray = [];
for (let set in setting) {
if (set === "series") {
seriesArray = setting[set];
continue;
}
this.$set(option, set, setting[set]);
}
let legend = [], series = {}, xAxis = [], seriesData = [];
chartData.forEach(item => {
if (!xAxis.includes(item.xAxis)) {
xAxis.push(item.xAxis);
}
xAxis.sort()
let name = item.groupName
if (!legend.includes(name)) {
legend.push(name)
series[name] = []
}
if (item.yAxis === -1) {
series[name].splice(xAxis.indexOf(item.xAxis), 0, [item.xAxis, item.yAxis2.toFixed(2)]);
} else {
series[name].splice(xAxis.indexOf(item.xAxis), 0, [item.xAxis, item.yAxis.toFixed(2)]);
}
})
this.$set(option.legend, "data", legend);
this.$set(option.legend, "type", "scroll");
this.$set(option.legend, "bottom", "10px");
this.$set(option.xAxis, "data", xAxis);
for (let name in series) {
let d = series[name];
d.sort((a, b) => a[0].localeCompare(b[0]));
let items = {
name: name,
type: 'line',
data: d,
smooth: true
};
let seriesArrayNames = seriesArray.map(m => m.name);
if (seriesArrayNames.includes(name)) {
for (let j = 0; j < seriesArray.length; j++) {
let seriesObj = seriesArray[j];
if (seriesObj['name'] === name) {
Object.assign(items, seriesObj);
}
}
}
seriesData.push(items);
}
this.$set(option, "series", seriesData);
return option;
},
_getChartMax(arr) {
const max = Math.max(...arr);
return Math.ceil(max / 4.5) * 5;
},
_unique(arr) {
return Array.from(new Set(arr));
}
}
}
</script>
<style scoped>
.echarts {
width: 100%;
height: 300px;
}
</style>

View File

@ -0,0 +1,130 @@
<template>
<el-dialog :close-on-click-modal="false"
:destroy-on-close="true"
:title="$t('已完成的测试报告')" width="60%"
:visible.sync="loadReportVisible">
<el-table v-loading="reportLoadingResult.loading"
class="basic-config"
:data="tableData"
@select-all="handleSelectAll"
@select="handleSelectionChange">
<el-table-column type="selection"/>
<el-table-column
prop="name"
:label="$t('commons.name')"
show-overflow-tooltip>
<template v-slot:default="scope">
<i v-if="scope.row.id === report.id" class="el-icon-star-on"></i> {{ scope.row.name }}
</template>
</el-table-column>
<el-table-column
prop="userName"
:label="$t('report.user_name')"
show-overflow-tooltip>
</el-table-column>
<el-table-column prop="triggerMode"
:label="$t('test_track.report.list.trigger_mode')">
<template v-slot:default="scope">
<report-trigger-mode-item :trigger-mode="scope.row.triggerMode"/>
</template>
</el-table-column>
<el-table-column
:label="$t('commons.create_time')">
<template v-slot:default="scope">
<i class="el-icon-time"/>
<span class="last-modified">{{ scope.row.createTime | timestampFormatDate }}</span>
</template>
</el-table-column>
</el-table>
<ms-table-pagination :change="getCompareReports" :current-page.sync="currentPage" :page-size.sync="pageSize"
:total="total"/>
<template v-slot:footer>
<ms-dialog-footer @cancel="close" @confirm="handleCompare"/>
</template>
</el-dialog>
</template>
<script>
import MsTablePagination from "@/business/components/common/pagination/TablePagination";
import MsDialogFooter from "@/business/components/common/components/MsDialogFooter";
import ReportTriggerModeItem from "@/business/components/common/tableItem/ReportTriggerModeItem";
export default {
name: "SameTestReports",
components: {ReportTriggerModeItem, MsDialogFooter, MsTablePagination},
data() {
return {
loadReportVisible: false,
reportLoadingResult: {},
tableData: [],
currentPage: 1,
pageSize: 10,
total: 0,
selectIds: new Set,
report: {},
compareReports: [],
}
},
methods: {
open(report) {
this.report = report;
this.getCompareReports(report);
this.compareReports.push(report);
this.loadReportVisible = true;
},
close() {
this.loadReportVisible = false;
},
getCompareReports(report) {
let condition = {
testId: report.testId,
filters: {status: ["Completed"]}
};
this.reportLoadingResult = this.$post('/performance/report/list/all/' + this.currentPage + "/" + this.pageSize, condition, res => {
let data = res.data;
this.total = data.itemCount;
this.tableData = data.listObject;
})
},
handleCompare() {
let reportIds = [...this.selectIds];
this.tableData
.filter(r => reportIds.indexOf(r.id) > -1 && this.report.id !== r.id)
.forEach(r => this.compareReports.push(r));
localStorage.setItem("compareReports", JSON.stringify(this.compareReports));
this.close();
this.$router.push({path: '/performance/report/compare/' + reportIds[0]});
},
handleSelectAll(selection) {
if (selection.length > 0) {
this.tableData.forEach(item => {
this.selectIds.add(item.id);
});
} else {
this.tableData.forEach(item => {
if (this.selectIds.has(item.id)) {
this.selectIds.delete(item.id);
}
});
}
},
handleSelectionChange(selection, row) {
if (this.selectIds.has(row.id)) {
this.selectIds.delete(row.id);
} else {
this.selectIds.add(row.id);
}
},
}
}
</script>
<style scoped>
</style>

View File

@ -1,5 +1,3 @@
import MsProject from "@/business/components/settings/project/MsProject";
const PerformanceTest = () => import('@/business/components/performance/PerformanceTest')
const PerformanceTestHome = () => import('@/business/components/performance/home/PerformanceTestHome')
const EditPerformanceTest = () => import('@/business/components/performance/test/EditPerformanceTest')
@ -7,6 +5,7 @@ const PerformanceTestList = () => import('@/business/components/performance/test
const PerformanceTestReportList = () => import('@/business/components/performance/report/PerformanceTestReportList')
const PerformanceChart = () => import('@/business/components/performance/report/components/PerformanceChart')
const PerformanceReportView = () => import('@/business/components/performance/report/PerformanceReportView')
const PerformanceReportCompare = () => import('@/business/components/performance/report/PerformanceReportCompare')
export default {
path: "/performance",
@ -62,6 +61,11 @@ export default {
path: "report/view/:reportId",
name: "perReportView",
component: PerformanceReportView
}
},
{
path: "report/compare/:reportId",
name: "ReportCompare",
component: PerformanceReportCompare,
},
]
}

View File

@ -285,6 +285,7 @@ export default {
}
},
nodeChange(node, nodeIds, pNodes) {
this.activeName = "default";
this.selectNodeIds = nodeIds;
this.selectNode = node;
this.selectParentNodes = pNodes;

View File

@ -303,20 +303,12 @@ export default {
},
created: function () {
this.$emit('setCondition', this.condition);
if (this.trashEnable) {
this.condition.filters = {status: ["Trash"]};
} else {
this.condition.filters = {status: ["Prepare", "Pass", "UnPass"]};
}
this.condition.filters = {reviewStatus: ["Prepare", "Pass", "UnPass"]};
this.initTableData();
},
activated() {
if (this.trashEnable) {
this.condition.filters = {status: ["Trash"]};
} else {
this.condition.filters = {status: ["Prepare", "Pass", "UnPass"]};
}
this.condition.filters = {reviewStatus: ["Prepare", "Pass", "UnPass"]};
this.initTableData();
},
watch: {
@ -372,15 +364,15 @@ export default {
case 'coverage':
this.condition.caseCoverage = 'coverage';
break;
/* case 'Prepare':
this.condition.filters.status = [this.selectDataRange];
case 'Prepare':
this.condition.filters.reviewStatus = [this.selectDataRange];
break;
case 'Pass':
this.condition.filters.status = [this.selectDataRange];
this.condition.filters.reviewStatus = [this.selectDataRange];
break;
case 'UnPass':
this.condition.filters.status = [this.selectDataRange];
break;*/
this.condition.filters.reviewStatus = [this.selectDataRange];
break;
}
if (this.projectId) {
this.condition.projectId = this.projectId;

@ -1 +1 @@
Subproject commit 07951ba17aef6f29e50cfd68e40de3266f9a60cd
Subproject commit 2115bd28a90854d2b6276a90878934715498c584

View File

@ -541,7 +541,10 @@ export default {
threadgroup_at_least_one: 'At least one ThreadGroup is enabled',
load_api_automation_jmx: 'Import API automation scenario',
project_file_exist: "The file already exists in the project, please import it directly",
project_file_update_type_error: 'Updated file types must be consistent'
project_file_update_type_error: 'Updated file types must be consistent',
report: {
diff: "Compare"
},
},
api_test: {
creator: "Creator",

View File

@ -438,7 +438,7 @@ export default {
export: '导出',
export_to_ms_format: '导出 MeterSphere 格式',
export_to_swagger3_format: '导出 Swagger3.0 格式',
compare: '',
compare: '报告对比',
generation_error: '报告生成错误, 无法查看, 请检查日志详情!',
being_generated: '报告正在生成中...',
delete_confirm: '确认删除报告: ',
@ -540,7 +540,10 @@ export default {
threadgroup_at_least_one: '至少启用一个线程组',
load_api_automation_jmx: '引用接口自动化场景',
project_file_exist: "项目中已存在该文件,请直接引用",
project_file_update_type_error: '更新的文件类型必须一致'
report: {
diff: "对比"
},
project_file_update_type_error: '更新的文件类型必须一致',
},
api_test: {
creator: "创建人",

View File

@ -438,7 +438,7 @@ export default {
export: '導出',
export_to_ms_format: '導出 MeterSphere 格式',
export_to_swagger3_format: '導出 Swagger3.0 格式',
compare: '比較',
compare: '報告比較',
generation_error: '報告生成錯誤, 無法查看, 請檢查日誌詳情!',
being_generated: '報告正在生成中...',
delete_confirm: '確認刪除報告: ',
@ -540,7 +540,10 @@ export default {
threadgroup_at_least_one: '至少啟用一個線程組',
load_api_automation_jmx: '引用接口自動化場景',
project_file_exist: "項目中已存在該文件,請直接引用",
project_file_update_type_error: '更新的文件類型必須一致'
project_file_update_type_error: '更新的文件類型必須一致',
report: {
diff: "對比"
},
},
api_test: {
creator: "創建人",