diff --git a/backend/pom.xml b/backend/pom.xml index ab143b2730..4b4e6db8c9 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -36,10 +36,6 @@ spring-boot-starter-tomcat org.springframework.boot - - hibernate-validator - org.hibernate.validator - @@ -133,6 +129,13 @@ 5.1 + + + com.alibaba + easyexcel + 2.1.7 + + diff --git a/backend/src/main/java/io/metersphere/base/domain/LoadTestReportDetail.java b/backend/src/main/java/io/metersphere/base/domain/LoadTestReportDetail.java new file mode 100644 index 0000000000..80425ac525 --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/domain/LoadTestReportDetail.java @@ -0,0 +1,27 @@ +package io.metersphere.base.domain; + +import java.io.Serializable; + +public class LoadTestReportDetail implements Serializable { + private String reportId; + + private String content; + + private static final long serialVersionUID = 1L; + + public String getReportId() { + return reportId; + } + + public void setReportId(String reportId) { + this.reportId = reportId == null ? null : reportId.trim(); + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content == null ? null : content.trim(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/base/domain/LoadTestReportDetailExample.java b/backend/src/main/java/io/metersphere/base/domain/LoadTestReportDetailExample.java new file mode 100644 index 0000000000..af84db6d79 --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/domain/LoadTestReportDetailExample.java @@ -0,0 +1,270 @@ +package io.metersphere.base.domain; + +import java.util.ArrayList; +import java.util.List; + +public class LoadTestReportDetailExample { + protected String orderByClause; + + protected boolean distinct; + + protected List oredCriteria; + + public LoadTestReportDetailExample() { + oredCriteria = new ArrayList(); + } + + public void setOrderByClause(String orderByClause) { + this.orderByClause = orderByClause; + } + + public String getOrderByClause() { + return orderByClause; + } + + public void setDistinct(boolean distinct) { + this.distinct = distinct; + } + + public boolean isDistinct() { + return distinct; + } + + public List getOredCriteria() { + return oredCriteria; + } + + public void or(Criteria criteria) { + oredCriteria.add(criteria); + } + + public Criteria or() { + Criteria criteria = createCriteriaInternal(); + oredCriteria.add(criteria); + return criteria; + } + + public Criteria createCriteria() { + Criteria criteria = createCriteriaInternal(); + if (oredCriteria.size() == 0) { + oredCriteria.add(criteria); + } + return criteria; + } + + protected Criteria createCriteriaInternal() { + Criteria criteria = new Criteria(); + return criteria; + } + + public void clear() { + oredCriteria.clear(); + orderByClause = null; + distinct = false; + } + + protected abstract static class GeneratedCriteria { + protected List criteria; + + protected GeneratedCriteria() { + super(); + criteria = new ArrayList(); + } + + public boolean isValid() { + return criteria.size() > 0; + } + + public List getAllCriteria() { + return criteria; + } + + public List getCriteria() { + return criteria; + } + + protected void addCriterion(String condition) { + if (condition == null) { + throw new RuntimeException("Value for condition cannot be null"); + } + criteria.add(new Criterion(condition)); + } + + protected void addCriterion(String condition, Object value, String property) { + if (value == null) { + throw new RuntimeException("Value for " + property + " cannot be null"); + } + criteria.add(new Criterion(condition, value)); + } + + protected void addCriterion(String condition, Object value1, Object value2, String property) { + if (value1 == null || value2 == null) { + throw new RuntimeException("Between values for " + property + " cannot be null"); + } + criteria.add(new Criterion(condition, value1, value2)); + } + + public Criteria andReportIdIsNull() { + addCriterion("report_id is null"); + return (Criteria) this; + } + + public Criteria andReportIdIsNotNull() { + addCriterion("report_id is not null"); + return (Criteria) this; + } + + public Criteria andReportIdEqualTo(String value) { + addCriterion("report_id =", value, "reportId"); + return (Criteria) this; + } + + public Criteria andReportIdNotEqualTo(String value) { + addCriterion("report_id <>", value, "reportId"); + return (Criteria) this; + } + + public Criteria andReportIdGreaterThan(String value) { + addCriterion("report_id >", value, "reportId"); + return (Criteria) this; + } + + public Criteria andReportIdGreaterThanOrEqualTo(String value) { + addCriterion("report_id >=", value, "reportId"); + return (Criteria) this; + } + + public Criteria andReportIdLessThan(String value) { + addCriterion("report_id <", value, "reportId"); + return (Criteria) this; + } + + public Criteria andReportIdLessThanOrEqualTo(String value) { + addCriterion("report_id <=", value, "reportId"); + return (Criteria) this; + } + + public Criteria andReportIdLike(String value) { + addCriterion("report_id like", value, "reportId"); + return (Criteria) this; + } + + public Criteria andReportIdNotLike(String value) { + addCriterion("report_id not like", value, "reportId"); + return (Criteria) this; + } + + public Criteria andReportIdIn(List values) { + addCriterion("report_id in", values, "reportId"); + return (Criteria) this; + } + + public Criteria andReportIdNotIn(List values) { + addCriterion("report_id not in", values, "reportId"); + return (Criteria) this; + } + + public Criteria andReportIdBetween(String value1, String value2) { + addCriterion("report_id between", value1, value2, "reportId"); + return (Criteria) this; + } + + public Criteria andReportIdNotBetween(String value1, String value2) { + addCriterion("report_id not between", value1, value2, "reportId"); + return (Criteria) this; + } + } + + public static class Criteria extends GeneratedCriteria { + + protected Criteria() { + super(); + } + } + + public static class Criterion { + private String condition; + + private Object value; + + private Object secondValue; + + private boolean noValue; + + private boolean singleValue; + + private boolean betweenValue; + + private boolean listValue; + + private String typeHandler; + + public String getCondition() { + return condition; + } + + public Object getValue() { + return value; + } + + public Object getSecondValue() { + return secondValue; + } + + public boolean isNoValue() { + return noValue; + } + + public boolean isSingleValue() { + return singleValue; + } + + public boolean isBetweenValue() { + return betweenValue; + } + + public boolean isListValue() { + return listValue; + } + + public String getTypeHandler() { + return typeHandler; + } + + protected Criterion(String condition) { + super(); + this.condition = condition; + this.typeHandler = null; + this.noValue = true; + } + + protected Criterion(String condition, Object value, String typeHandler) { + super(); + this.condition = condition; + this.value = value; + this.typeHandler = typeHandler; + if (value instanceof List) { + this.listValue = true; + } else { + this.singleValue = true; + } + } + + protected Criterion(String condition, Object value) { + this(condition, value, null); + } + + protected Criterion(String condition, Object value, Object secondValue, String typeHandler) { + super(); + this.condition = condition; + this.value = value; + this.secondValue = secondValue; + this.typeHandler = typeHandler; + this.betweenValue = true; + } + + protected Criterion(String condition, Object value, Object secondValue) { + this(condition, value, secondValue, null); + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/base/domain/ZaleniumTest.java b/backend/src/main/java/io/metersphere/base/domain/ZaleniumTest.java deleted file mode 100644 index 3631aba0d8..0000000000 --- a/backend/src/main/java/io/metersphere/base/domain/ZaleniumTest.java +++ /dev/null @@ -1,194 +0,0 @@ -package io.metersphere.base.domain; - -public class ZaleniumTest { - - private String seleniumSessionId; - private String testName; - private String timestamp; - private String addedToDashboardTime; - private String browser; - private String browserVersion; - private String proxyName; - private String platform; - private String fileName; - private String fileExtension; - private String videoFolderPath; - private String logsFolderPath; - private String testNameNoExtension; - private String screenDimension; - private String timeZone; - private String build; - private String testFileNameTemplate; - private String browserDriverLogFileName; - private String retentionDate; - private String testStatus; - private boolean videoRecorded; - - public String getSeleniumSessionId() { - return seleniumSessionId; - } - - public void setSeleniumSessionId(String seleniumSessionId) { - this.seleniumSessionId = seleniumSessionId; - } - - public String getTestName() { - return testName; - } - - public void setTestName(String testName) { - this.testName = testName; - } - - public String getTimestamp() { - return timestamp; - } - - public void setTimestamp(String timestamp) { - this.timestamp = timestamp; - } - - public String getAddedToDashboardTime() { - return addedToDashboardTime; - } - - public void setAddedToDashboardTime(String addedToDashboardTime) { - this.addedToDashboardTime = addedToDashboardTime; - } - - public String getBrowser() { - return browser; - } - - public void setBrowser(String browser) { - this.browser = browser; - } - - public String getBrowserVersion() { - return browserVersion; - } - - public void setBrowserVersion(String browserVersion) { - this.browserVersion = browserVersion; - } - - public String getProxyName() { - return proxyName; - } - - public void setProxyName(String proxyName) { - this.proxyName = proxyName; - } - - public String getPlatform() { - return platform; - } - - public void setPlatform(String platform) { - this.platform = platform; - } - - public String getFileName() { - return fileName; - } - - public void setFileName(String fileName) { - this.fileName = fileName; - } - - public String getFileExtension() { - return fileExtension; - } - - public void setFileExtension(String fileExtension) { - this.fileExtension = fileExtension; - } - - public String getVideoFolderPath() { - return videoFolderPath; - } - - public void setVideoFolderPath(String videoFolderPath) { - this.videoFolderPath = videoFolderPath; - } - - public String getLogsFolderPath() { - return logsFolderPath; - } - - public void setLogsFolderPath(String logsFolderPath) { - this.logsFolderPath = logsFolderPath; - } - - public String getTestNameNoExtension() { - return testNameNoExtension; - } - - public void setTestNameNoExtension(String testNameNoExtension) { - this.testNameNoExtension = testNameNoExtension; - } - - public String getScreenDimension() { - return screenDimension; - } - - public void setScreenDimension(String screenDimension) { - this.screenDimension = screenDimension; - } - - public String getTimeZone() { - return timeZone; - } - - public void setTimeZone(String timeZone) { - this.timeZone = timeZone; - } - - public String getBuild() { - return build; - } - - public void setBuild(String build) { - this.build = build; - } - - public String getTestFileNameTemplate() { - return testFileNameTemplate; - } - - public void setTestFileNameTemplate(String testFileNameTemplate) { - this.testFileNameTemplate = testFileNameTemplate; - } - - public String getBrowserDriverLogFileName() { - return browserDriverLogFileName; - } - - public void setBrowserDriverLogFileName(String browserDriverLogFileName) { - this.browserDriverLogFileName = browserDriverLogFileName; - } - - public String getRetentionDate() { - return retentionDate; - } - - public void setRetentionDate(String retentionDate) { - this.retentionDate = retentionDate; - } - - public String getTestStatus() { - return testStatus; - } - - public void setTestStatus(String testStatus) { - this.testStatus = testStatus; - } - - public boolean isVideoRecorded() { - return videoRecorded; - } - - public void setVideoRecorded(boolean videoRecorded) { - this.videoRecorded = videoRecorded; - } -} diff --git a/backend/src/main/java/io/metersphere/base/mapper/LoadTestReportDetailMapper.java b/backend/src/main/java/io/metersphere/base/mapper/LoadTestReportDetailMapper.java new file mode 100644 index 0000000000..d19c9961ed --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/mapper/LoadTestReportDetailMapper.java @@ -0,0 +1,35 @@ +package io.metersphere.base.mapper; + +import io.metersphere.base.domain.LoadTestReportDetail; +import io.metersphere.base.domain.LoadTestReportDetailExample; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface LoadTestReportDetailMapper { + long countByExample(LoadTestReportDetailExample example); + + int deleteByExample(LoadTestReportDetailExample example); + + int deleteByPrimaryKey(String reportId); + + int insert(LoadTestReportDetail record); + + int insertSelective(LoadTestReportDetail record); + + List selectByExampleWithBLOBs(LoadTestReportDetailExample example); + + List selectByExample(LoadTestReportDetailExample example); + + LoadTestReportDetail selectByPrimaryKey(String reportId); + + int updateByExampleSelective(@Param("record") LoadTestReportDetail record, @Param("example") LoadTestReportDetailExample example); + + int updateByExampleWithBLOBs(@Param("record") LoadTestReportDetail record, @Param("example") LoadTestReportDetailExample example); + + int updateByExample(@Param("record") LoadTestReportDetail record, @Param("example") LoadTestReportDetailExample example); + + int updateByPrimaryKeySelective(LoadTestReportDetail record); + + int updateByPrimaryKeyWithBLOBs(LoadTestReportDetail record); +} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/base/mapper/LoadTestReportDetailMapper.xml b/backend/src/main/java/io/metersphere/base/mapper/LoadTestReportDetailMapper.xml new file mode 100644 index 0000000000..8a19ff8977 --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/mapper/LoadTestReportDetailMapper.xml @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + and ${criterion.condition} + + + and ${criterion.condition} #{criterion.value} + + + and ${criterion.condition} #{criterion.value} and #{criterion.secondValue} + + + and ${criterion.condition} + + #{listItem} + + + + + + + + + + + + + + + + + + and ${criterion.condition} + + + and ${criterion.condition} #{criterion.value} + + + and ${criterion.condition} #{criterion.value} and #{criterion.secondValue} + + + and ${criterion.condition} + + #{listItem} + + + + + + + + + + + report_id + + + content + + + + + + delete from load_test_report_detail + where report_id = #{reportId,jdbcType=VARCHAR} + + + delete from load_test_report_detail + + + + + + insert into load_test_report_detail (report_id, content) + values (#{reportId,jdbcType=VARCHAR}, #{content,jdbcType=LONGVARCHAR}) + + + insert into load_test_report_detail + + + report_id, + + + content, + + + + + #{reportId,jdbcType=VARCHAR}, + + + #{content,jdbcType=LONGVARCHAR}, + + + + + + update load_test_report_detail + + + report_id = #{record.reportId,jdbcType=VARCHAR}, + + + content = #{record.content,jdbcType=LONGVARCHAR}, + + + + + + + + update load_test_report_detail + set report_id = #{record.reportId,jdbcType=VARCHAR}, + content = #{record.content,jdbcType=LONGVARCHAR} + + + + + + update load_test_report_detail + set report_id = #{record.reportId,jdbcType=VARCHAR} + + + + + + update load_test_report_detail + + + content = #{content,jdbcType=LONGVARCHAR}, + + + where report_id = #{reportId,jdbcType=VARCHAR} + + + update load_test_report_detail + set content = #{content,jdbcType=LONGVARCHAR} + where report_id = #{reportId,jdbcType=VARCHAR} + + \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtLoadTestReportDetailMapper.java b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtLoadTestReportDetailMapper.java new file mode 100644 index 0000000000..ceb2c64339 --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtLoadTestReportDetailMapper.java @@ -0,0 +1,7 @@ +package io.metersphere.base.mapper.ext; + +import org.apache.ibatis.annotations.Param; + +public interface ExtLoadTestReportDetailMapper { + int appendLine(@Param("reportId") String id, @Param("line") String line); +} diff --git a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtLoadTestReportDetailMapper.xml b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtLoadTestReportDetailMapper.xml new file mode 100644 index 0000000000..ca33e6e79d --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtLoadTestReportDetailMapper.xml @@ -0,0 +1,9 @@ + + + + + UPDATE load_test_report_detail + SET content = concat(content, #{line}) + WHERE report_id = #{reportId} + + \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/commons/constants/PerformanceTestStatus.java b/backend/src/main/java/io/metersphere/commons/constants/PerformanceTestStatus.java new file mode 100644 index 0000000000..242883eb3f --- /dev/null +++ b/backend/src/main/java/io/metersphere/commons/constants/PerformanceTestStatus.java @@ -0,0 +1,5 @@ +package io.metersphere.commons.constants; + +public enum PerformanceTestStatus { + Saved, Starting, Running, Completed, Error +} diff --git a/backend/src/main/java/io/metersphere/commons/constants/TestCaseConstants.java b/backend/src/main/java/io/metersphere/commons/constants/TestCaseConstants.java new file mode 100644 index 0000000000..06410341dc --- /dev/null +++ b/backend/src/main/java/io/metersphere/commons/constants/TestCaseConstants.java @@ -0,0 +1,5 @@ +package io.metersphere.commons.constants; + +public class TestCaseConstants { + public static final int MAX_NODE_DEPTH = 5; +} diff --git a/backend/src/main/java/io/metersphere/commons/constants/TestStatus.java b/backend/src/main/java/io/metersphere/commons/constants/TestStatus.java deleted file mode 100644 index ccb4f1de29..0000000000 --- a/backend/src/main/java/io/metersphere/commons/constants/TestStatus.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.metersphere.commons.constants; - -public enum TestStatus { - Starting, Running, Completed, Error -} diff --git a/backend/src/main/java/io/metersphere/controller/PerformanceTestController.java b/backend/src/main/java/io/metersphere/controller/PerformanceTestController.java index 0e50956747..a046035ed0 100644 --- a/backend/src/main/java/io/metersphere/controller/PerformanceTestController.java +++ b/backend/src/main/java/io/metersphere/controller/PerformanceTestController.java @@ -4,7 +4,6 @@ import com.github.pagehelper.Page; import com.github.pagehelper.PageHelper; import io.metersphere.base.domain.FileMetadata; import io.metersphere.commons.constants.RoleConstants; -import io.metersphere.commons.exception.MSException; import io.metersphere.commons.utils.PageUtils; import io.metersphere.commons.utils.Pager; import io.metersphere.controller.request.testplan.*; @@ -86,10 +85,7 @@ public class PerformanceTestController { @PostMapping("/run") public void run(@RequestBody RunTestPlanRequest request) { - boolean started = performanceTestService.run(request); - if (!started) { - MSException.throwException("Start engine error, please check log."); - } + performanceTestService.run(request); } @GetMapping("/file/metadata/{testId}") diff --git a/backend/src/main/java/io/metersphere/controller/TestCaseController.java b/backend/src/main/java/io/metersphere/controller/TestCaseController.java index 69be699528..fee1de1d96 100644 --- a/backend/src/main/java/io/metersphere/controller/TestCaseController.java +++ b/backend/src/main/java/io/metersphere/controller/TestCaseController.java @@ -6,14 +6,11 @@ import io.metersphere.base.domain.*; import io.metersphere.commons.utils.PageUtils; import io.metersphere.commons.utils.Pager; import io.metersphere.controller.request.testcase.QueryTestCaseRequest; -import io.metersphere.controller.request.testplan.QueryTestPlanRequest; -import io.metersphere.dto.LoadTestDTO; -import io.metersphere.dto.TestCaseNodeDTO; -import io.metersphere.dto.TestPlanCaseDTO; -import io.metersphere.service.TestCaseNodeService; +import io.metersphere.excel.domain.ExcelResponse; import io.metersphere.service.TestCaseService; import io.metersphere.user.SessionUtils; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import java.util.List; @@ -74,5 +71,10 @@ public class TestCaseController { return testCaseService.deleteTestCase(testCaseId); } + @PostMapping("/import/{projectId}") + public ExcelResponse testCaseImport(MultipartFile file, @PathVariable String projectId){ + return testCaseService.testCaseImport(file, projectId); + } + } diff --git a/backend/src/main/java/io/metersphere/engine/AbstractEngine.java b/backend/src/main/java/io/metersphere/engine/AbstractEngine.java index cb90d08c1e..c7e07b5d90 100644 --- a/backend/src/main/java/io/metersphere/engine/AbstractEngine.java +++ b/backend/src/main/java/io/metersphere/engine/AbstractEngine.java @@ -6,8 +6,8 @@ import com.alibaba.fastjson.JSONObject; import io.metersphere.base.domain.LoadTestWithBLOBs; import io.metersphere.base.domain.TestResource; import io.metersphere.base.domain.TestResourcePool; +import io.metersphere.commons.constants.PerformanceTestStatus; import io.metersphere.commons.constants.ResourcePoolTypeEnum; -import io.metersphere.commons.constants.TestStatus; import io.metersphere.commons.exception.MSException; import io.metersphere.commons.utils.CommonBeanFactory; import io.metersphere.config.JmeterProperties; @@ -70,7 +70,7 @@ public abstract class AbstractEngine implements Engine { List loadTests = performanceTestService.selectByTestResourcePoolId(loadTest.getTestResourcePoolId()); // 使用当前资源池正在运行的测试占用的并发数 return loadTests.stream() - .filter(t -> TestStatus.Running.name().equals(t.getStatus())) + .filter(t -> PerformanceTestStatus.Running.name().equals(t.getStatus())) .map(this::getThreadNum) .reduce(Integer::sum) .orElse(0); diff --git a/backend/src/main/java/io/metersphere/engine/docker/DockerTestEngine.java b/backend/src/main/java/io/metersphere/engine/docker/DockerTestEngine.java index 06c7892170..1c208ffc49 100644 --- a/backend/src/main/java/io/metersphere/engine/docker/DockerTestEngine.java +++ b/backend/src/main/java/io/metersphere/engine/docker/DockerTestEngine.java @@ -12,6 +12,7 @@ import io.metersphere.engine.EngineContext; import io.metersphere.engine.EngineFactory; import io.metersphere.engine.docker.request.BaseRequest; import io.metersphere.engine.docker.request.TestRequest; +import io.metersphere.i18n.Translator; import org.springframework.web.client.RestTemplate; import java.util.List; @@ -41,7 +42,7 @@ public class DockerTestEngine extends AbstractEngine { .reduce(Integer::sum) .orElse(0); if (threadNum > totalThreadNum - runningSumThreadNum) { - MSException.throwException("Insufficient resources"); + MSException.throwException(Translator.get("max_thread_insufficient")); } List resourceRatio = resourceList.stream() .filter(r -> ResourceStatusEnum.VALID.name().equals(r.getStatus())) diff --git a/backend/src/main/java/io/metersphere/engine/kubernetes/KubernetesTestEngine.java b/backend/src/main/java/io/metersphere/engine/kubernetes/KubernetesTestEngine.java index dedb5e7a8a..e1e8a3f68d 100644 --- a/backend/src/main/java/io/metersphere/engine/kubernetes/KubernetesTestEngine.java +++ b/backend/src/main/java/io/metersphere/engine/kubernetes/KubernetesTestEngine.java @@ -15,6 +15,7 @@ import io.metersphere.engine.kubernetes.crds.jmeter.Jmeter; import io.metersphere.engine.kubernetes.crds.jmeter.JmeterSpec; import io.metersphere.engine.kubernetes.provider.ClientCredential; import io.metersphere.engine.kubernetes.provider.KubernetesProvider; +import io.metersphere.i18n.Translator; import org.apache.commons.collections.MapUtils; import java.util.HashMap; @@ -43,7 +44,7 @@ public class KubernetesTestEngine extends AbstractEngine { Integer maxConcurrency = clientCredential.getMaxConcurrency(); // 当前测试需要的并发数大于剩余的并发数报错 if (threadNum > maxConcurrency - sumThreadNum) { - MSException.throwException("Insufficient resources"); + MSException.throwException(Translator.get("max_thread_insufficient")); } try { EngineContext context = EngineFactory.createContext(loadTest, threadNum, this.getStartTime(), this.getReportId()); diff --git a/backend/src/main/java/io/metersphere/excel/domain/ExcelErrData.java b/backend/src/main/java/io/metersphere/excel/domain/ExcelErrData.java new file mode 100644 index 0000000000..66544759bb --- /dev/null +++ b/backend/src/main/java/io/metersphere/excel/domain/ExcelErrData.java @@ -0,0 +1,42 @@ +package io.metersphere.excel.domain; + +public class ExcelErrData { + + private T t; + + private Integer rowNum; + + private String errMsg; + + public ExcelErrData(){} + + public ExcelErrData(T t, Integer rowNum,String errMsg){ + this.t = t; + this.rowNum = rowNum; + this.errMsg = errMsg; + } + + public T getT() { + return t; + } + + public void setT(T t) { + this.t = t; + } + + public String getErrMsg() { + return errMsg; + } + + public void setErrMsg(String errMsg) { + this.errMsg = errMsg; + } + + public Integer getRowNum() { + return rowNum; + } + + public void setRowNum(Integer rowNum) { + this.rowNum = rowNum; + } +} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/excel/domain/ExcelResponse.java b/backend/src/main/java/io/metersphere/excel/domain/ExcelResponse.java new file mode 100644 index 0000000000..aac623316f --- /dev/null +++ b/backend/src/main/java/io/metersphere/excel/domain/ExcelResponse.java @@ -0,0 +1,25 @@ +package io.metersphere.excel.domain; + +import java.util.List; + +public class ExcelResponse { + + private Boolean success; + private List> errList; + + public Boolean getSuccess() { + return success; + } + + public void setSuccess(Boolean success) { + this.success = success; + } + + public List> getErrList() { + return errList; + } + + public void setErrList(List> errList) { + this.errList = errList; + } +} diff --git a/backend/src/main/java/io/metersphere/excel/domain/TestCaseExcelData.java b/backend/src/main/java/io/metersphere/excel/domain/TestCaseExcelData.java new file mode 100644 index 0000000000..d7a311ebf6 --- /dev/null +++ b/backend/src/main/java/io/metersphere/excel/domain/TestCaseExcelData.java @@ -0,0 +1,137 @@ +package io.metersphere.excel.domain; + +import com.alibaba.excel.annotation.ExcelProperty; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; + +public class TestCaseExcelData { + + @NotBlank + @Length(max=1000) + @ExcelProperty("所属模块") + @Pattern(regexp = "^(?!.*//).*$", message = "格式不正确") + private String nodePath; + + @NotBlank + @Length(max=50) + @ExcelProperty("用例名称") + private String name; + + @NotBlank + @ExcelProperty("用例类型") + @Pattern(regexp = "(^functional$)|(^performance$)|(^api$)", message = "必须为functional、performance、api") + private String type; + + @NotBlank + @ExcelProperty("维护人") + private String maintainer; + + @NotBlank + @ExcelProperty("优先级") + @Pattern(regexp = "(^P0$)|(^P1$)|(^P2$)|(^P3$)", message = "必须为P0、P1、P2、P3") + private String priority; + + @NotBlank + @ExcelProperty("测试方式") + @Pattern(regexp = "(^manual$)|(^auto$)", message = "必须为manual、auto") + private String method; + + @ExcelProperty("前置条件") + @Length(min=0, max=1000) + private String prerequisite; + + @ExcelProperty("备注") + @Length(max=1000) + private String remark; + + @ExcelProperty("步骤描述") + @Length(max=1000) + private String stepDesc; + + @ExcelProperty("预期结果") + @Length(max=1000) + private String stepResult; + + public String getNodePath() { + return nodePath; + } + + public void setNodePath(String nodePath) { + this.nodePath = nodePath; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getMaintainer() { + return maintainer; + } + + public void setMaintainer(String maintainer) { + this.maintainer = maintainer; + } + + public String getPriority() { + return priority; + } + + public void setPriority(String priority) { + this.priority = priority; + } + + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method; + } + + public String getPrerequisite() { + return prerequisite; + } + + public void setPrerequisite(String prerequisite) { + this.prerequisite = prerequisite; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + public String getStepDesc() { + return stepDesc; + } + + public void setStepDesc(String stepDesc) { + this.stepDesc = stepDesc; + } + + public String getStepResult() { + return stepResult; + } + + public void setStepResult(String stepResult) { + this.stepResult = stepResult; + } + +} diff --git a/backend/src/main/java/io/metersphere/excel/listener/EasyExcelListener.java b/backend/src/main/java/io/metersphere/excel/listener/EasyExcelListener.java new file mode 100644 index 0000000000..bbb45f7d8c --- /dev/null +++ b/backend/src/main/java/io/metersphere/excel/listener/EasyExcelListener.java @@ -0,0 +1,140 @@ +package io.metersphere.excel.listener; + +import com.alibaba.excel.annotation.ExcelProperty; +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; +import com.alibaba.excel.exception.ExcelAnalysisException; +import com.alibaba.excel.util.StringUtils; +import io.metersphere.commons.utils.LogUtil; +import io.metersphere.excel.util.ExcelValidateHelper; +import io.metersphere.excel.domain.ExcelErrData; + +import java.lang.reflect.Field; +import java.util.*; + + +public abstract class EasyExcelListener extends AnalysisEventListener { + + protected List> errList = new ArrayList<>(); + + protected List list = new ArrayList<>(); + + /** + * 每隔2000条存储数据库,然后清理list ,方便内存回收 + */ + protected static final int BATCH_COUNT = 2000; + + protected Class clazz; + + + public EasyExcelListener(Class clazz){ + this.clazz = clazz; + } + + /** + * 这个每一条数据解析都会来调用 + * + * @param t + * @param analysisContext + */ + @Override + public void invoke(T t, AnalysisContext analysisContext) { + String errMsg; + Integer rowIndex = analysisContext.readRowHolder().getRowIndex(); + try { + //根据excel数据实体中的javax.validation + 正则表达式来校验excel数据 + errMsg = ExcelValidateHelper.validateEntity(t); + //自定义校验规则 + errMsg = validate(t, errMsg); + } catch (NoSuchFieldException e) { + errMsg = "解析数据出错"; + LogUtil.error(e.getMessage(), e); + } + + if (!StringUtils.isEmpty(errMsg)) { + ExcelErrData excelErrData = new ExcelErrData(t, rowIndex, "第" + rowIndex + "行出错:" + errMsg); + errList.add(excelErrData); + } else { + list.add(t); + } + + if (list.size() > BATCH_COUNT) { + saveData(); + list.clear(); + } + } + + /** + * 可重写该方法 + * 自定义校验规则 + * @param data + * @param errMsg + * @return + */ + public String validate(T data, String errMsg) { + return errMsg; + } + + /** + * 自定义数据保存操作 + */ + public abstract void saveData(); + + @Override + public void doAfterAllAnalysed(AnalysisContext analysisContext) { + saveData(); + list.clear(); + } + + + /** + * 校验excel头部 + * @param headMap 传入excel的头部(第一行数据)数据的index,name + * @param context + */ + @Override + public void invokeHeadMap(Map headMap, AnalysisContext context) { + super.invokeHeadMap(headMap, context); + if (clazz != null){ + try { + Set fieldNameSet = getFieldNameSet(clazz); + Collection values = headMap.values(); + for (String key : fieldNameSet) { + if (!values.contains(key)){ + throw new ExcelAnalysisException("缺少头部信息:" + key); + } + } + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } + } + } + + /** + * @description: 获取注解里ExcelProperty的value + */ + public Set getFieldNameSet(Class clazz) throws NoSuchFieldException { + Set result = new HashSet<>(); + Field field; + Field[] fields = clazz.getDeclaredFields(); + for (int i = 0; i < fields.length ; i++) { + field = clazz.getDeclaredField(fields[i].getName()); + field.setAccessible(true); + ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class); + if(excelProperty != null){ + StringBuilder value = new StringBuilder(); + for (String v : excelProperty.value()) { + value.append(v); + } + result.add(value.toString()); + } + } + return result; + } + + + public List> getErrList() { + return errList; + } + +} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/excel/listener/TestCaseDataListener.java b/backend/src/main/java/io/metersphere/excel/listener/TestCaseDataListener.java new file mode 100644 index 0000000000..268a444328 --- /dev/null +++ b/backend/src/main/java/io/metersphere/excel/listener/TestCaseDataListener.java @@ -0,0 +1,138 @@ +package io.metersphere.excel.listener; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import io.metersphere.excel.domain.TestCaseExcelData; +import io.metersphere.base.domain.TestCaseWithBLOBs; +import io.metersphere.commons.constants.TestCaseConstants; +import io.metersphere.commons.utils.BeanUtils; +import io.metersphere.service.TestCaseService; + +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class TestCaseDataListener extends EasyExcelListener { + + private TestCaseService testCaseService; + + private String projectId; + + Set testCaseNames; + + Set userNames; + + public TestCaseDataListener(TestCaseService testCaseService, String projectId, + Set testCaseNames, Set userNames, Class clazz) { + super(clazz); + this.testCaseService = testCaseService; + this.projectId = projectId; + this.testCaseNames = testCaseNames; + this.userNames = userNames; + } + + @Override + public String validate(TestCaseExcelData data, String errMsg) { + String nodePath = data.getNodePath(); + StringBuilder stringBuilder = new StringBuilder(errMsg); + if ( nodePath.split("/").length > TestCaseConstants.MAX_NODE_DEPTH + 1) { + stringBuilder.append("节点最多为" + TestCaseConstants.MAX_NODE_DEPTH + "层;"); + } + if (!userNames.contains(data.getMaintainer())) { + stringBuilder.append("该工作空间下无该用户:" + data.getMaintainer() + ";"); + } + if (testCaseNames.contains(data.getName())) { + stringBuilder.append("该项目下已存在该测试用例:" + data.getName() + ";"); + } + return stringBuilder.toString(); + } + + @Override + public void saveData() { + + //无错误数据才插入数据 + if (!errList.isEmpty()) { + return; + } + + List result = list.stream() + .map(item -> this.convert2TestCase(item)) + .collect(Collectors.toList()); + + testCaseService.saveImportData(result, projectId); + + } + + + private TestCaseWithBLOBs convert2TestCase(TestCaseExcelData data) { + TestCaseWithBLOBs testCase = new TestCaseWithBLOBs(); + BeanUtils.copyBean(testCase, data); + testCase.setId(UUID.randomUUID().toString()); + testCase.setProjectId(this.projectId); + testCase.setCreateTime(System.currentTimeMillis()); + testCase.setUpdateTime(System.currentTimeMillis()); + String nodePath = data.getNodePath(); + + if (!nodePath.startsWith("/")) { + nodePath = "/" + nodePath; + } + if (nodePath.endsWith("/")) { + nodePath = nodePath.substring(0, nodePath.length() - 1); + } + + testCase.setNodePath(nodePath); + + + JSONArray jsonArray = new JSONArray(); + + String[] stepDesc = new String[0]; + String[] stepRes = new String[0]; + + if (data.getStepDesc() != null) { + stepDesc = data.getStepDesc().split("\n"); + } + if (data.getStepResult() != null) { + stepRes = data.getStepResult().split("\n"); + } + + String pattern = "(^\\d+)(\\.)?"; + int index = stepDesc.length > stepRes.length ? stepDesc.length : stepRes.length; + + for (int i = 0; i < index; i++){ + + JSONObject step = new JSONObject(); + step.put("num", i + 1); + + Pattern descPattern = Pattern.compile(pattern); + Pattern resPattern = Pattern.compile(pattern); + + if (i < stepDesc.length) { + Matcher descMatcher = descPattern.matcher(stepDesc[i]); + if (descMatcher.find()) { + step.put("desc", descMatcher.replaceAll("")); + } else { + step.put("desc", stepDesc[i]); + } + } + + if (i < stepRes.length) { + Matcher resMatcher = resPattern.matcher(stepRes[i]); + if (resMatcher.find()) { + step.put("result", resMatcher.replaceAll("")); + } else { + step.put("result", stepRes[i]); + } + } + + jsonArray.add(step); + } + + testCase.setSteps(jsonArray.toJSONString()); + + return testCase; + } + +} diff --git a/backend/src/main/java/io/metersphere/excel/util/ExcelValidateHelper.java b/backend/src/main/java/io/metersphere/excel/util/ExcelValidateHelper.java new file mode 100644 index 0000000000..8156bbcc6d --- /dev/null +++ b/backend/src/main/java/io/metersphere/excel/util/ExcelValidateHelper.java @@ -0,0 +1,32 @@ +package io.metersphere.excel.util; + +import com.alibaba.excel.annotation.ExcelProperty; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.groups.Default; +import java.lang.reflect.Field; +import java.util.Set; + + +public class ExcelValidateHelper { + + private ExcelValidateHelper(){} + + private static Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + + public static String validateEntity(T obj) throws NoSuchFieldException { + StringBuilder result = new StringBuilder(); + Set> set = validator.validate(obj, Default.class); + if (set != null && !set.isEmpty()) { + for (ConstraintViolation cv : set) { + Field declaredField = obj.getClass().getDeclaredField(cv.getPropertyPath().toString()); + ExcelProperty annotation = declaredField.getAnnotation(ExcelProperty.class); + //拼接错误信息,包含当前出错数据的标题名字+错误信息 + result.append(annotation.value()[0]+cv.getMessage()).append(";"); + } + } + return result.toString(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/exception/ExcelImportException.java b/backend/src/main/java/io/metersphere/exception/ExcelImportException.java new file mode 100644 index 0000000000..8ac826ab74 --- /dev/null +++ b/backend/src/main/java/io/metersphere/exception/ExcelImportException.java @@ -0,0 +1,18 @@ +package io.metersphere.exception; + +/** + * @author jianxing.chen + */ +public class ExcelImportException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public ExcelImportException(String message, Exception e){ + super(message, e); + } + + public ExcelImportException(String message){ + super(message); + } + +} diff --git a/backend/src/main/java/io/metersphere/report/JtlResolver.java b/backend/src/main/java/io/metersphere/report/JtlResolver.java index 3f2490d9e0..f518c37b38 100644 --- a/backend/src/main/java/io/metersphere/report/JtlResolver.java +++ b/backend/src/main/java/io/metersphere/report/JtlResolver.java @@ -104,7 +104,7 @@ public class JtlResolver { String average = decimalFormat.format((float) oneLineElapsedTime / jtlSamplesSize); requestStatistics.setAverage(average); - /** + /* * TP90的计算 * 1,把一段时间内全部的请求的响应时间,从小到大排序,获得序列A * 2,总的请求数量,乘以90%,获得90%对应的请求个数C @@ -123,7 +123,7 @@ public class JtlResolver { requestStatistics.setMax(elapsedList.get(jtlSamplesSize - 1) + ""); requestStatistics.setErrors(decimalFormat.format(failSize * 100.0 / jtlSamplesSize) + "%"); requestStatistics.setKo(failSize); - /** + /* * 所有的相同请求的bytes总和 / 1024 / 请求持续运行的时间=sum(bytes)/1024/total time * total time = 最大时间戳 - 最小时间戳 + 最后请求的响应时间 */ @@ -268,20 +268,34 @@ public class JtlResolver { DecimalFormat decimalFormat = new DecimalFormat("0.00"); List totalLineList = JtlResolver.resolver(jtlString); + // todo + List totalLineList2 = JtlResolver.resolver(jtlString); + // 时间戳转时间 + for (Metric metric : totalLineList2) { + metric.setTimestamp(stampToDate(DATE_TIME_PATTERN, metric.getTimestamp())); + } + + Map> collect2 = Objects.requireNonNull(totalLineList2).stream().collect(Collectors.groupingBy(Metric::getTimestamp)); + List>> entries = new ArrayList<>(collect2.entrySet()); + int maxUsers = 0; + for (Map.Entry> entry : entries) { + List metrics = entry.getValue(); + Map> metricsMap = metrics.stream().collect(Collectors.groupingBy(Metric::getThreadName)); + if (metricsMap.size() > maxUsers) { + maxUsers = metricsMap.size(); + } + } + Map> collect = totalLineList.stream().collect(Collectors.groupingBy(Metric::getTimestamp)); Iterator>> iterator = collect.entrySet().iterator(); - int maxUsers = 0, totalElapsed = 0; + int totalElapsed = 0; float totalBytes = 0f; while (iterator.hasNext()) { Map.Entry> entry = iterator.next(); List metricList = entry.getValue(); - if (metricList.size() > maxUsers) { - maxUsers = metricList.size(); - } - for (Metric metric : metricList) { String elapsed = metric.getElapsed(); totalElapsed += Integer.parseInt(elapsed); @@ -420,22 +434,22 @@ public class JtlResolver { totalLineList.sort(Comparator.comparing(t0 -> Long.valueOf(t0.getTimestamp()))); String startTimeStamp = totalLineList.get(0).getTimestamp(); - String endTimeStamp = totalLineList.get(totalLineList.size() - 1).getTimestamp(); + String endTimeStamp = totalLineList.get(totalLineList.size()-1).getTimestamp(); - DateTimeFormatter dtf = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN); + DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"); String startTime = dtf.format(LocalDateTime.ofInstant(Instant.ofEpochMilli(Long.parseLong(startTimeStamp)), ZoneId.systemDefault())); String endTime = dtf.format(LocalDateTime.ofInstant(Instant.ofEpochMilli(Long.parseLong(endTimeStamp)), ZoneId.systemDefault())); reportTimeInfo.setStartTime(startTime); reportTimeInfo.setEndTime(endTime); + Date startDate = new Date(Long.parseLong(startTimeStamp)); + Date endDate = new Date(Long.parseLong(endTimeStamp)); + long timestamp = endDate.getTime() - startDate.getTime(); + reportTimeInfo.setDuration(String.valueOf(timestamp*1.0 / 1000 / 60)); + + // todo 时间问题 long seconds = Duration.between(Instant.ofEpochMilli(Long.parseLong(startTimeStamp)), Instant.ofEpochMilli(Long.parseLong(endTimeStamp))).getSeconds(); - String duration; - if (seconds / 60 == 0) { - duration = String.valueOf(1); - } else { - duration = String.valueOf(seconds / 60); - } - reportTimeInfo.setDuration(duration); + reportTimeInfo.setDuration(String.valueOf(seconds)); return reportTimeInfo; } diff --git a/backend/src/main/java/io/metersphere/service/PerformanceTestService.java b/backend/src/main/java/io/metersphere/service/PerformanceTestService.java index e72024a14a..37b22f71be 100644 --- a/backend/src/main/java/io/metersphere/service/PerformanceTestService.java +++ b/backend/src/main/java/io/metersphere/service/PerformanceTestService.java @@ -3,9 +3,10 @@ package io.metersphere.service; import io.metersphere.base.domain.*; import io.metersphere.base.mapper.*; import io.metersphere.base.mapper.ext.ExtLoadTestMapper; +import io.metersphere.base.mapper.ext.ExtLoadTestReportDetailMapper; import io.metersphere.base.mapper.ext.ExtLoadTestReportMapper; import io.metersphere.commons.constants.FileType; -import io.metersphere.commons.constants.TestStatus; +import io.metersphere.commons.constants.PerformanceTestStatus; import io.metersphere.commons.exception.MSException; import io.metersphere.commons.utils.LogUtil; import io.metersphere.controller.request.testplan.*; @@ -48,6 +49,10 @@ public class PerformanceTestService { private LoadTestReportMapper loadTestReportMapper; @Resource private ExtLoadTestReportMapper extLoadTestReportMapper; + @Resource + private LoadTestReportDetailMapper loadTestReportDetailMapper; + @Resource + private ExtLoadTestReportDetailMapper extLoadTestReportDetailMapper; public List list(QueryTestPlanRequest request) { return extLoadTestMapper.list(request); @@ -93,6 +98,7 @@ public class PerformanceTestService { loadTest.setTestResourcePoolId(request.getTestResourcePoolId()); loadTest.setLoadConfiguration(request.getLoadConfiguration()); loadTest.setAdvancedConfiguration(request.getAdvancedConfiguration()); + loadTest.setStatus(PerformanceTestStatus.Saved.name()); loadTestMapper.insert(loadTest); return loadTest; } @@ -158,19 +164,22 @@ public class PerformanceTestService { loadTest.setLoadConfiguration(request.getLoadConfiguration()); loadTest.setAdvancedConfiguration(request.getAdvancedConfiguration()); loadTest.setTestResourcePoolId(request.getTestResourcePoolId()); + // todo 修改 load_test 的时候排除状态,这里存在修改了 Running 的测试状态的风险 +// loadTest.setStatus(PerformanceTestStatus.Saved.name()); loadTestMapper.updateByPrimaryKeySelective(loadTest); } return request.getId(); } - public boolean run(RunTestPlanRequest request) { + @Transactional(noRollbackFor = MSException.class)// 保存失败的信息 + public void run(RunTestPlanRequest request) { final LoadTestWithBLOBs loadTest = loadTestMapper.selectByPrimaryKey(request.getId()); if (loadTest == null) { MSException.throwException(Translator.get("run_load_test_not_found") + request.getId()); } - if (StringUtils.equalsAny(loadTest.getStatus(), TestStatus.Running.name(), TestStatus.Starting.name())) { + if (StringUtils.equalsAny(loadTest.getStatus(), PerformanceTestStatus.Running.name(), PerformanceTestStatus.Starting.name())) { MSException.throwException(Translator.get("load_test_is_running")); } @@ -181,12 +190,12 @@ public class PerformanceTestService { MSException.throwException(String.format("Test cannot be run,test ID:%s", request.getId())); } - return startEngine(loadTest, engine); + startEngine(loadTest, engine); // todo:通过调用stop方法能够停止正在运行的engine,但是如果部署了多个backend实例,页面发送的停止请求如何定位到具体的engine } - private boolean startEngine(LoadTestWithBLOBs loadTest, Engine engine) { + private void startEngine(LoadTestWithBLOBs loadTest, Engine engine) { LoadTestReportWithBLOBs testReport = new LoadTestReportWithBLOBs(); testReport.setId(engine.getReportId()); testReport.setCreateTime(engine.getStartTime()); @@ -194,31 +203,32 @@ public class PerformanceTestService { testReport.setTestId(loadTest.getId()); testReport.setName(loadTest.getName()); // 启动测试 - boolean started = true; + try { engine.start(); - // 标记running状态 - loadTest.setStatus(TestStatus.Starting.name()); + // 启动正常修改状态 starting + loadTest.setStatus(PerformanceTestStatus.Starting.name()); loadTestMapper.updateByPrimaryKeySelective(loadTest); - + // 启动正常插入 report testReport.setContent(HEADERS); - testReport.setStatus(TestStatus.Starting.name()); + testReport.setStatus(PerformanceTestStatus.Starting.name()); loadTestReportMapper.insertSelective(testReport); + + LoadTestReportDetail reportDetail = new LoadTestReportDetail(); + reportDetail.setContent(HEADERS); + reportDetail.setReportId(testReport.getId()); + loadTestReportDetailMapper.insertSelective(reportDetail); // append \n extLoadTestReportMapper.appendLine(testReport.getId(), "\n"); - - } catch (Exception e) { + // append \n + extLoadTestReportDetailMapper.appendLine(testReport.getId(), "\n"); + } catch (MSException e) { LogUtil.error(e); - started = false; - - loadTest.setStatus(TestStatus.Error.name()); + loadTest.setStatus(PerformanceTestStatus.Error.name()); + loadTest.setDescription(e.getMessage()); loadTestMapper.updateByPrimaryKeySelective(loadTest); - // - testReport.setStatus(TestStatus.Error.name()); - testReport.setDescription(e.getMessage()); - loadTestReportMapper.insertSelective(testReport); + throw e; } - return started; } public List recentTestPlans(QueryTestPlanRequest request) { diff --git a/backend/src/main/java/io/metersphere/service/ReportService.java b/backend/src/main/java/io/metersphere/service/ReportService.java index 890bfc133c..17b2f047fa 100644 --- a/backend/src/main/java/io/metersphere/service/ReportService.java +++ b/backend/src/main/java/io/metersphere/service/ReportService.java @@ -5,7 +5,7 @@ import io.metersphere.base.domain.LoadTestReportExample; import io.metersphere.base.domain.LoadTestReportWithBLOBs; import io.metersphere.base.mapper.LoadTestReportMapper; import io.metersphere.base.mapper.ext.ExtLoadTestReportMapper; -import io.metersphere.commons.constants.TestStatus; +import io.metersphere.commons.constants.PerformanceTestStatus; import io.metersphere.commons.exception.MSException; import io.metersphere.controller.request.ReportRequest; import io.metersphere.dto.ReportDTO; @@ -112,9 +112,9 @@ public class ReportService { public void checkReportStatus(String reportId) { LoadTestReportWithBLOBs loadTestReport = loadTestReportMapper.selectByPrimaryKey(reportId); String reportStatus = loadTestReport.getStatus(); - if (StringUtils.equals(TestStatus.Running.name(), reportStatus)) { + if (StringUtils.equals(PerformanceTestStatus.Running.name(), reportStatus)) { MSException.throwException("Reporting in progress..."); - } else if (StringUtils.equals(TestStatus.Error.name(), reportStatus)) { + } else if (StringUtils.equals(PerformanceTestStatus.Error.name(), reportStatus)) { MSException.throwException("Report generation error!"); } } diff --git a/backend/src/main/java/io/metersphere/service/TestCaseNodeService.java b/backend/src/main/java/io/metersphere/service/TestCaseNodeService.java index fcf74b3250..63e457f9de 100644 --- a/backend/src/main/java/io/metersphere/service/TestCaseNodeService.java +++ b/backend/src/main/java/io/metersphere/service/TestCaseNodeService.java @@ -6,8 +6,11 @@ import io.metersphere.base.mapper.TestCaseMapper; import io.metersphere.base.mapper.TestCaseNodeMapper; import io.metersphere.base.mapper.TestPlanMapper; import io.metersphere.base.mapper.TestPlanTestCaseMapper; +import io.metersphere.commons.constants.TestCaseConstants; import io.metersphere.commons.utils.BeanUtils; import io.metersphere.dto.TestCaseNodeDTO; +import io.metersphere.exception.ExcelImportException; +import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -30,8 +33,8 @@ public class TestCaseNodeService { public int addNode(TestCaseNode node) { - if(node.getLevel() > 5){ - throw new RuntimeException("模块树最大深度为5层!"); + if(node.getLevel() > TestCaseConstants.MAX_NODE_DEPTH){ + throw new RuntimeException("模块树最大深度为" + TestCaseConstants.MAX_NODE_DEPTH + "层!"); } node.setCreateTime(System.currentTimeMillis()); node.setUpdateTime(System.currentTimeMillis()); @@ -196,4 +199,124 @@ public class TestCaseNodeService { TestPlan testPlan = testPlanMapper.selectByPrimaryKey(planId); return getNodeTreeByProjectId(testPlan.getProjectId()); } + + public Map createNodeByTestCases(List testCases, String projectId) { + + List nodeTrees = getNodeTreeByProjectId(projectId); + + Map pathMap = new HashMap<>(); + + List nodePaths = testCases.stream() + .map(TestCase::getNodePath) + .collect(Collectors.toList()); + + nodePaths.forEach(path -> { + + if (path == null) { + throw new ExcelImportException("所属模块不能为空!"); + } + List nodeNameList = new ArrayList<>(Arrays.asList(path.split("/"))); + Iterator pathIterator = nodeNameList.iterator(); + + Boolean hasNode = false; + String rootNodeName = null; + + if (nodeNameList.size() <= 1) { + throw new ExcelImportException("创建模块失败:" + path); + } else { + pathIterator.next(); + pathIterator.remove(); + + rootNodeName = pathIterator.next().trim(); + for (TestCaseNodeDTO nodeTree : nodeTrees) { + if (StringUtils.equals(rootNodeName, nodeTree.getName())) { + hasNode = true; + createNodeByPathIterator(pathIterator, "/" + rootNodeName, nodeTree, + pathMap, projectId, 2); + }; + } + } + + if (!hasNode) { + createNodeByPath(pathIterator, rootNodeName, null, projectId, 1, "", pathMap); + } + }); + + return pathMap; + + } + + /** + * 根据目标节点路径,创建相关节点 + * @param pathIterator 遍历子路径 + * @param path 当前路径 + * @param treeNode 当前节点 + * @param pathMap 记录节点路径对应的nodeId + */ + private void createNodeByPathIterator(Iterator pathIterator, String path, TestCaseNodeDTO treeNode, + Map pathMap, String projectId, Integer level) { + + List children = treeNode.getChildren(); + + if (children == null || children.isEmpty() || !pathIterator.hasNext()) { + pathMap.put(path , treeNode.getId()); + if (pathIterator.hasNext()) { + createNodeByPath(pathIterator, pathIterator.next().trim(), treeNode, projectId, level, path, pathMap); + } + return; + } + + String nodeName = pathIterator.next().trim(); + + Boolean hasNode = false; + + for (TestCaseNodeDTO child : children) { + if (StringUtils.equals(nodeName, child.getName())) { + hasNode = true; + createNodeByPathIterator(pathIterator, path + "/" + child.getName(), + child, pathMap, projectId, level + 1); + }; + } + + //若子节点中不包含该目标节点,则在该节点下创建 + if (!hasNode) { + createNodeByPath(pathIterator, nodeName, treeNode, projectId, level, path, pathMap); + } + + } + + /** + * + * @param pathIterator 迭代器,遍历子节点 + * @param nodeName 当前节点 + * @param pNode 父节点 + */ + private void createNodeByPath(Iterator pathIterator, String nodeName, + TestCaseNodeDTO pNode, String projectId, Integer level, + String rootPath, Map pathMap) { + + StringBuilder path = new StringBuilder(rootPath); + + Integer pid = insertTestCaseNode(nodeName, pNode == null ? null : pNode.getId(), projectId, level); + path.append("/" + nodeName); + pathMap.put(path.toString(), pid); + while (pathIterator.hasNext()) { + String nextNodeName = pathIterator.next(); + path.append("/" + nextNodeName); + pid = insertTestCaseNode(nextNodeName, pid, projectId, ++level); + pathMap.put(path.toString(), pid); + } + } + + private Integer insertTestCaseNode(String nodName, Integer pId, String projectId, Integer level) { + TestCaseNode testCaseNode = new TestCaseNode(); + testCaseNode.setName(nodName.trim()); + testCaseNode.setpId(pId); + testCaseNode.setProjectId(projectId); + testCaseNode.setCreateTime(System.currentTimeMillis()); + testCaseNode.setUpdateTime(System.currentTimeMillis()); + testCaseNode.setLevel(level); + testCaseNodeMapper.insert(testCaseNode); + return testCaseNode.getId(); + } } diff --git a/backend/src/main/java/io/metersphere/service/TestCaseService.java b/backend/src/main/java/io/metersphere/service/TestCaseService.java index 6dbf5bec4f..371eaf5197 100644 --- a/backend/src/main/java/io/metersphere/service/TestCaseService.java +++ b/backend/src/main/java/io/metersphere/service/TestCaseService.java @@ -1,26 +1,34 @@ package io.metersphere.service; +import com.alibaba.excel.EasyExcelFactory; import com.github.pagehelper.PageHelper; import io.metersphere.base.domain.*; -import io.metersphere.base.mapper.ProjectMapper; -import io.metersphere.base.mapper.TestCaseMapper; -import io.metersphere.base.mapper.TestPlanMapper; -import io.metersphere.base.mapper.TestPlanTestCaseMapper; +import io.metersphere.base.mapper.*; import io.metersphere.base.mapper.ext.ExtTestCaseMapper; +import io.metersphere.commons.utils.LogUtil; import io.metersphere.controller.request.testcase.QueryTestCaseRequest; -import io.metersphere.dto.TestPlanCaseDTO; +import io.metersphere.excel.domain.ExcelErrData; +import io.metersphere.excel.domain.ExcelResponse; +import io.metersphere.excel.domain.TestCaseExcelData; +import io.metersphere.excel.listener.EasyExcelListener; +import io.metersphere.excel.listener.TestCaseDataListener; +import io.metersphere.user.SessionUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.ibatis.session.ExecutorType; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; -import java.util.ArrayList; +import java.io.IOException; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; -import java.util.stream.Stream; @Service @Transactional(rollbackFor = Exception.class) @@ -41,6 +49,15 @@ public class TestCaseService { @Resource ProjectMapper projectMapper; + @Resource + SqlSessionFactory sqlSessionFactory; + + @Resource + TestCaseNodeService testCaseNodeService; + + @Resource + UserMapper userMapper; + public void addTestCase(TestCaseWithBLOBs testCase) { testCase.setId(UUID.randomUUID().toString()); testCase.setCreateTime(System.currentTimeMillis()); @@ -144,4 +161,59 @@ public class TestCaseService { } return projectMapper.selectByPrimaryKey(testCaseWithBLOBs.getProjectId()); } + + public ExcelResponse testCaseImport(MultipartFile file, String projectId) { + + try { + + ExcelResponse excelResponse = new ExcelResponse(); + + String currentWorkspaceId = SessionUtils.getCurrentWorkspaceId(); + QueryTestCaseRequest queryTestCaseRequest = new QueryTestCaseRequest(); + queryTestCaseRequest.setProjectId(projectId); + List testCases = extTestCaseMapper.getTestCaseNames(queryTestCaseRequest); + Set testCaseNames = testCases.stream() + .map(TestCase::getName) + .collect(Collectors.toSet()); + + UserExample userExample = new UserExample(); + userExample.createCriteria().andLastWorkspaceIdEqualTo(currentWorkspaceId); + List users = userMapper.selectByExample(userExample); + Set userNames = users.stream().map(User::getName).collect(Collectors.toSet()); + + EasyExcelListener easyExcelListener = new TestCaseDataListener(this, projectId, + testCaseNames, userNames, TestCaseExcelData.class); + EasyExcelFactory.read(file.getInputStream(), TestCaseExcelData.class, easyExcelListener).sheet().doRead(); + + List> errList = easyExcelListener.getErrList(); + //如果包含错误信息就导出错误信息 + if (!errList.isEmpty()) { + excelResponse.setSuccess(false); + excelResponse.setErrList(errList); + } else { + excelResponse.setSuccess(true); + } + return excelResponse; + + } catch (IOException e) { + LogUtil.error(e.getMessage(), e); + e.printStackTrace(); + } + + return null; + } + + public void saveImportData(List testCases, String projectId) { + + Map nodePathMap = testCaseNodeService.createNodeByTestCases(testCases, projectId); + SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH); + TestCaseMapper mapper = sqlSession.getMapper(TestCaseMapper.class); + if (!testCases.isEmpty()) { + testCases.forEach(testcase -> { + testcase.setNodeId(nodePathMap.get(testcase.getNodePath())); + mapper.insert(testcase); + }); + } + sqlSession.flushStatements(); + } } diff --git a/backend/src/main/resources/db/migration/V2__metersphere_ddl.sql b/backend/src/main/resources/db/migration/V2__metersphere_ddl.sql index 2225e75a3a..74b8ff4364 100644 --- a/backend/src/main/resources/db/migration/V2__metersphere_ddl.sql +++ b/backend/src/main/resources/db/migration/V2__metersphere_ddl.sql @@ -62,6 +62,15 @@ CREATE TABLE IF NOT EXISTS `load_test_report` ( DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_bin; +CREATE TABLE IF NOT EXISTS `load_test_report_detail` ( + `report_id` varchar(50) NOT NULL, + `content` longtext, + PRIMARY KEY (`report_id`) +) + ENGINE=InnoDB + DEFAULT CHARSET=utf8mb4 + COLLATE=utf8mb4_bin; + CREATE TABLE IF NOT EXISTS `organization` ( `id` varchar(50) NOT NULL COMMENT 'Organization ID', `name` varchar(64) NOT NULL COMMENT 'Organization name', diff --git a/backend/src/main/resources/i18n/en-US.json b/backend/src/main/resources/i18n/en-US.json index b88c742725..c5329f867f 100644 --- a/backend/src/main/resources/i18n/en-US.json +++ b/backend/src/main/resources/i18n/en-US.json @@ -18,5 +18,6 @@ "no_nodes_message": "No node message", "duplicate_node_ip": "Duplicate IPs", "only_one_k8s": "Only one K8s can be added", - "organization_id_is_null": "Organization ID cannot be null" + "organization_id_is_null": "Organization ID cannot be null", + "max_thread_insufficient": "The number of concurrent users exceeds" } \ No newline at end of file diff --git a/backend/src/main/resources/i18n/zh-CN.json b/backend/src/main/resources/i18n/zh-CN.json index 56352f221a..0884b07c0f 100644 --- a/backend/src/main/resources/i18n/zh-CN.json +++ b/backend/src/main/resources/i18n/zh-CN.json @@ -18,5 +18,6 @@ "no_nodes_message": "没有节点信息", "duplicate_node_ip": "节点 IP 重复", "only_one_k8s": "只能添加一个 K8s", - "organization_id_is_null": "组织 ID 不能为空" + "organization_id_is_null": "组织 ID 不能为空", + "max_thread_insufficient": "并发用户数超额" } \ No newline at end of file diff --git a/backend/src/test/java/io/metersphere/ReportContentTests.java b/backend/src/test/java/io/metersphere/ReportContentTests.java new file mode 100644 index 0000000000..bac9afcd74 --- /dev/null +++ b/backend/src/test/java/io/metersphere/ReportContentTests.java @@ -0,0 +1,61 @@ +package io.metersphere; + +import com.opencsv.bean.CsvToBean; +import com.opencsv.bean.CsvToBeanBuilder; +import com.opencsv.bean.HeaderColumnNameMappingStrategy; +import io.metersphere.base.domain.LoadTestReportDetail; +import io.metersphere.base.domain.LoadTestReportWithBLOBs; +import io.metersphere.base.mapper.LoadTestReportDetailMapper; +import io.metersphere.base.mapper.LoadTestReportMapper; +import io.metersphere.report.base.Metric; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +import javax.annotation.Resource; +import java.io.Reader; +import java.io.StringReader; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class ReportContentTests { + @Resource + private LoadTestReportDetailMapper loadTestReportDetailMapper; + @Resource + private LoadTestReportMapper loadTestReportMapper; + + @Test + public void test1() { + String reportId = "ba972086-7d74-4f58-99b0-9c014114fd99"; + LoadTestReportDetail loadTestReportDetail = loadTestReportDetailMapper.selectByPrimaryKey(reportId); + LoadTestReportWithBLOBs loadTestReportWithBLOBs = loadTestReportMapper.selectByPrimaryKey(reportId); + + HeaderColumnNameMappingStrategy ms = new HeaderColumnNameMappingStrategy<>(); + ms.setType(Metric.class); + try (Reader reader = new StringReader(loadTestReportDetail.getContent())) { + CsvToBean cb = new CsvToBeanBuilder(reader) + .withType(Metric.class) + .withSkipLines(0) + .withMappingStrategy(ms) + .withIgnoreLeadingWhiteSpace(true) + .build(); + System.out.println(cb.parse().size()); + + } catch (Exception ex) { + ex.printStackTrace(); + } + try (Reader reader = new StringReader(loadTestReportWithBLOBs.getContent())) { + CsvToBean cb = new CsvToBeanBuilder(reader) + .withType(Metric.class) + .withSkipLines(0) + .withMappingStrategy(ms) + .withIgnoreLeadingWhiteSpace(true) + .build(); + System.out.println(cb.parse().size()); + + } catch (Exception ex) { + ex.printStackTrace(); + } + } +} diff --git a/frontend/src/business/components/common/head/HeaderUser.vue b/frontend/src/business/components/common/head/HeaderUser.vue index 6d6ce40435..6b03ed29ac 100644 --- a/frontend/src/business/components/common/head/HeaderUser.vue +++ b/frontend/src/business/components/common/head/HeaderUser.vue @@ -51,7 +51,7 @@ TokenKey, WORKSPACE_ID } from '../../../../common/js/constants'; - import {hasRoles} from "../../../../common/js/utils"; + import {hasRoles, saveLocalStorage} from "../../../../common/js/utils"; export default { name: "MsUser", @@ -136,7 +136,7 @@ changeOrg(data) { let orgId = data.id; this.$post("/user/switch/source/org/" + orgId, {}, response => { - localStorage.setItem(TokenKey, JSON.stringify(response.data)); + saveLocalStorage(response); this.$router.push('/'); window.location.reload(); }) @@ -147,7 +147,7 @@ return false; } this.$post("/user/switch/source/ws/" + workspaceId, {}, response => { - localStorage.setItem(TokenKey, JSON.stringify(response.data)); + saveLocalStorage(response); localStorage.setItem("workspace_id", workspaceId); this.$router.push('/'); window.location.reload(); diff --git a/frontend/src/business/components/performance/report/PerformanceReportView.vue b/frontend/src/business/components/performance/report/PerformanceReportView.vue index 2326cb5493..58a63cc689 100644 --- a/frontend/src/business/components/performance/report/PerformanceReportView.vue +++ b/frontend/src/business/components/performance/report/PerformanceReportView.vue @@ -20,7 +20,7 @@ - 持续时间: {{minutes}} 分钟 + 持续时间: {{minutes}} 分钟 {{seconds}} 秒 开始时间: {{startTime}} @@ -79,6 +79,7 @@ startTime: '0', endTime: '0', minutes: '0', + seconds: '0' } }, methods: { @@ -101,7 +102,9 @@ if(data){ this.startTime = data.startTime; this.endTime = data.endTime; - this.minutes = data.duration; + let duration = data.duration; + this.minutes = Math.floor(duration / 60); + this.seconds = duration % 60; } }) } @@ -144,7 +147,9 @@ if(data){ this.startTime = data.startTime; this.endTime = data.endTime; - this.minutes = data.duration; + let duration = data.duration; + this.minutes = Math.floor(duration / 60); + this.seconds = duration % 60; } }) window.location.reload(); diff --git a/frontend/src/business/components/performance/report/components/TestOverview.vue b/frontend/src/business/components/performance/report/components/TestOverview.vue index 7607967732..b8d3bd73f3 100644 --- a/frontend/src/business/components/performance/report/components/TestOverview.vue +++ b/frontend/src/business/components/performance/report/components/TestOverview.vue @@ -96,6 +96,10 @@ }) this.$get("/performance/report/content/load_chart/" + this.id, res => { let data = res.data; + let userList = data.filter(m => m.groupName === "users").map(m => m.yAxis); + let hitsList = data.filter(m => m.groupName === "hits").map(m => m.yAxis); + let userMax = this._getChartMax(userList); + let hitsMax = this._getChartMax(hitsList); let loadOption = { title: { text: 'Load', @@ -105,30 +109,57 @@ color: '#65A2FF' }, }, + tooltip: { + show: true, + trigger: 'axis' + }, legend: {}, xAxis: {}, yAxis: [{ name: 'User', type: 'value', min: 0, + max: userMax, splitNumber: 5, - // interval: 10 / 5 + interval: userMax / 5 }, { name: 'Hits/s', type: 'value', splitNumber: 5, min: 0, - // max: 5, - // interval: 5 / 5 + max: hitsMax, + interval: hitsMax / 5 } ], series: [] + }; + let setting = { + series: [ + { + name: 'users', + color: '#0CA74A', + }, + { + name: 'hits', + yAxisIndex: '1', + color: '#65A2FF', + }, + { + name: 'errors', + yAxisIndex: '1', + color: '#E6113C', + } + ] } - this.loadOption = this.generateOption(loadOption, data); + this.loadOption = this.generateOption(loadOption, data, setting); }) this.$get("/performance/report/content/res_chart/" + this.id, res => { let data = res.data; + let userList = data.filter(m => m.groupName === "users").map(m => m.yAxis); + let responseTimeList = data.filter(m => m.groupName === "responseTime").map(m => m.yAxis); + let userMax = this._getChartMax(userList); + let resMax = this._getChartMax(responseTimeList); let resOption = { title: { text: 'Response Time', @@ -138,28 +169,55 @@ color: '#99743C' }, }, + tooltip: { + show: true, + trigger: 'axis' + }, legend: {}, xAxis: {}, yAxis: [{ name: 'User', type: 'value', - splitNumber: 5, - min: 0 + min: 0, + max: userMax, + interval: userMax / 5 }, { name: 'Response Time', type: 'value', - splitNumber: 5, - min: 0 + min: 0, + max: resMax, + interval: resMax / 5 } ], series: [] } - this.resOption = this.generateOption(resOption, data); + let setting = { + series: [ + { + name: 'users', + color: '#0CA74A', + }, + { + name: "responseTime", + yAxisIndex: '1', + color: '#99743C', + } + ] + } + this.resOption = this.generateOption(resOption, data, setting); }) }, - generateOption(option, data) { + 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)) { @@ -183,11 +241,24 @@ type: 'line', data: data }; + 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; + } }, watch: { status() { diff --git a/frontend/src/business/components/performance/test/PerformanceTestPlan.vue b/frontend/src/business/components/performance/test/PerformanceTestPlan.vue index effb019c8a..32103259b6 100644 --- a/frontend/src/business/components/performance/test/PerformanceTestPlan.vue +++ b/frontend/src/business/components/performance/test/PerformanceTestPlan.vue @@ -46,7 +46,10 @@ prop="status" :label="$t('commons.status')">