生成report

This commit is contained in:
q4speed 2020-05-07 23:59:51 +08:00
parent 94b7ceb1e9
commit 39aed31aa5
26 changed files with 772 additions and 415 deletions

View File

@ -0,0 +1,56 @@
package io.metersphere.api.controller;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import io.metersphere.api.dto.APIReportResult;
import io.metersphere.api.dto.DeleteAPIReportRequest;
import io.metersphere.api.dto.QueryAPIReportRequest;
import io.metersphere.api.service.APIReportService;
import io.metersphere.base.domain.ApiTestReport;
import io.metersphere.commons.constants.RoleConstants;
import io.metersphere.commons.utils.PageUtils;
import io.metersphere.commons.utils.Pager;
import io.metersphere.user.SessionUtils;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import javax.annotation.Resource;
@RestController
@RequestMapping(value = "/api/report")
@RequiresRoles(value = {RoleConstants.TEST_MANAGER, RoleConstants.TEST_USER, RoleConstants.TEST_VIEWER}, logical = Logical.OR)
public class APIReportController {
@Resource
private APIReportService apiReportService;
@GetMapping("recent/{count}")
public List<APIReportResult> recentTest(@PathVariable int count) {
String currentWorkspaceId = SessionUtils.getCurrentWorkspaceId();
QueryAPIReportRequest request = new QueryAPIReportRequest();
request.setWorkspaceId(currentWorkspaceId);
PageHelper.startPage(1, count, true);
return apiReportService.recentTest(request);
}
@PostMapping("/list/{goPage}/{pageSize}")
public Pager<List<APIReportResult>> list(@PathVariable int goPage, @PathVariable int pageSize, @RequestBody QueryAPIReportRequest request) {
Page<Object> page = PageHelper.startPage(goPage, pageSize, true);
request.setWorkspaceId(SessionUtils.getCurrentWorkspaceId());
return PageUtils.setPageInfo(page, apiReportService.list(request));
}
@GetMapping("/get/{testId}")
public ApiTestReport get(@PathVariable String testId) {
return apiReportService.get(testId);
}
@PostMapping("/delete")
public void delete(@RequestBody DeleteAPIReportRequest request) {
apiReportService.delete(request);
}
}

View File

@ -6,13 +6,11 @@ import io.metersphere.api.dto.APITestResult;
import io.metersphere.api.dto.DeleteAPITestRequest;
import io.metersphere.api.dto.QueryAPITestRequest;
import io.metersphere.api.dto.SaveAPITestRequest;
import io.metersphere.api.service.ApiTestService;
import io.metersphere.api.service.APITestService;
import io.metersphere.base.domain.ApiTestWithBLOBs;
import io.metersphere.commons.constants.RoleConstants;
import io.metersphere.commons.utils.PageUtils;
import io.metersphere.commons.utils.Pager;
import io.metersphere.controller.request.testplan.SaveTestPlanRequest;
import io.metersphere.service.FileService;
import io.metersphere.user.SessionUtils;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresRoles;
@ -28,9 +26,7 @@ import javax.annotation.Resource;
@RequiresRoles(value = {RoleConstants.TEST_MANAGER, RoleConstants.TEST_USER, RoleConstants.TEST_VIEWER}, logical = Logical.OR)
public class APITestController {
@Resource
private ApiTestService apiTestService;
@Resource
private FileService fileService;
private APITestService apiTestService;
@GetMapping("recent/{count}")
public List<APITestResult> recentTest(@PathVariable int count) {
@ -48,9 +44,14 @@ public class APITestController {
return PageUtils.setPageInfo(page, apiTestService.list(request));
}
@PostMapping(value = "/save", consumes = {"multipart/form-data"})
public String save(@RequestPart("request") SaveAPITestRequest request, @RequestPart(value = "files") List<MultipartFile> files) {
return apiTestService.save(request, files);
@PostMapping(value = "/create", consumes = {"multipart/form-data"})
public void create(@RequestPart("request") SaveAPITestRequest request, @RequestPart(value = "files") List<MultipartFile> files) {
apiTestService.create(request, files);
}
@PostMapping(value = "/update", consumes = {"multipart/form-data"})
public void update(@RequestPart("request") SaveAPITestRequest request, @RequestPart(value = "files") List<MultipartFile> files) {
apiTestService.update(request, files);
}
@GetMapping("/get/{testId}")
@ -63,8 +64,8 @@ public class APITestController {
apiTestService.delete(request);
}
@PostMapping(value = "/run", consumes = {"multipart/form-data"})
public String run(@RequestPart("request") SaveAPITestRequest request, @RequestPart(value = "files") List<MultipartFile> files) {
return apiTestService.run(request, files);
@PostMapping(value = "/run")
public void run(@RequestBody SaveAPITestRequest request) {
apiTestService.run(request);
}
}

View File

@ -0,0 +1,13 @@
package io.metersphere.api.dto;
import io.metersphere.base.domain.ApiTestReport;
import io.metersphere.base.domain.ApiTestWithBLOBs;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
public class APIReportResult extends ApiTestReport {
private String projectName;
}

View File

@ -0,0 +1,11 @@
package io.metersphere.api.dto;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
public class DeleteAPIReportRequest {
private String id;
}

View File

@ -0,0 +1,16 @@
package io.metersphere.api.dto;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class QueryAPIReportRequest {
private String id;
private String projectId;
private String name;
private String workspaceId;
private boolean recent = false;
}

View File

@ -1,45 +1,149 @@
package io.metersphere.api.jmeter;
import io.metersphere.api.service.APIReportService;
import io.metersphere.api.service.APITestService;
import io.metersphere.commons.constants.APITestStatus;
import io.metersphere.commons.utils.CommonBeanFactory;
import io.metersphere.commons.utils.LogUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.jmeter.assertions.AssertionResult;
import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.visualizers.backend.AbstractBackendListenerClient;
import org.apache.jmeter.visualizers.backend.BackendListenerContext;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* JMeter BackendListener扩展, jmx脚本中使用
*/
public class APIBackendListenerClient extends AbstractBackendListenerClient implements Serializable {
private final AtomicInteger count = new AtomicInteger();
// 与前端JMXGenerator的SPLIT对应用于获取 测试名称 测试ID
private final static String SPLIT = "@@:";
// 测试ID作为key
private final Map<String, List<SampleResult>> queue = new ConcurrentHashMap<>();
@Override
public void handleSampleResults(List<SampleResult> sampleResults, BackendListenerContext context) {
System.out.println(context.getParameter("id"));
sampleResults.forEach(result -> {
for (AssertionResult assertionResult : result.getAssertionResults()) {
System.out.println(assertionResult.getName() + ": " + assertionResult.isError());
System.out.println(assertionResult.getName() + ": " + assertionResult.isFailure());
System.out.println(assertionResult.getName() + ": " + assertionResult.getFailureMessage());
// 将不同的测试脚本按测试ID分开
String label = result.getSampleLabel();
if (!label.contains(SPLIT)) {
LogUtil.error("request name format is invalid, name: " + label);
return;
}
println("getSampleLabel", result.getSampleLabel());
println("getErrorCount", result.getErrorCount());
println("getRequestHeaders", result.getRequestHeaders());
println("getResponseHeaders", result.getResponseHeaders());
println("getSampleLabel", result.getSampleLabel());
println("getSampleLabel", result.getSampleLabel());
println("getResponseCode", result.getResponseCode());
println("getResponseCode size", result.getResponseData().length);
println("getLatency", result.getLatency());
println("end - start", result.getEndTime() - result.getStartTime());
println("getTimeStamp", result.getTimeStamp());
println("getTime", result.getTime());
String name = label.split(SPLIT)[0];
String testId = label.split(SPLIT)[1];
if (!queue.containsKey(testId)) {
List<SampleResult> testResults = new ArrayList<>();
queue.put(testId, testResults);
}
result.setSampleLabel(name);
queue.get(testId).add(result);
});
System.err.println(count.addAndGet(sampleResults.size()));
}
private void println(String name, Object value) {
System.out.println(name + ": " + value);
@Override
public void teardownTest(BackendListenerContext context) throws Exception {
APITestService apiTestService = CommonBeanFactory.getBean(APITestService.class);
if (apiTestService == null) {
LogUtil.error("apiTestService is required");
return;
}
APIReportService apiReportService = CommonBeanFactory.getBean(APIReportService.class);
if (apiReportService == null) {
LogUtil.error("apiReportService is required");
return;
}
queue.forEach((id, sampleResults) -> {
TestResult testResult = new TestResult();
testResult.setId(id);
testResult.setTotal(sampleResults.size());
// key: 场景Id
final Map<String, ScenarioResult> scenarios = new LinkedHashMap<>();
sampleResults.forEach(result -> {
String thread = StringUtils.substringBeforeLast(result.getThreadName(), " ");
String scenarioName = StringUtils.substringBefore(thread, SPLIT);
String scenarioId = StringUtils.substringAfter(thread, SPLIT);
ScenarioResult scenarioResult;
if (!scenarios.containsKey(scenarioId)) {
scenarioResult = new ScenarioResult();
scenarioResult.setId(scenarioId);
scenarioResult.setName(scenarioName);
scenarios.put(scenarioId, scenarioResult);
} else {
scenarioResult = scenarios.get(scenarioId);
}
if (result.isSuccessful()) {
scenarioResult.addSuccess();
testResult.addSuccess();
} else {
scenarioResult.addError();
testResult.addError();
}
RequestResult requestResult = getRequestResult(result);
scenarioResult.getRequestResult().add(requestResult);
testResult.addPassAssertions(requestResult.getPassAssertions());
testResult.addTotalAssertions(requestResult.getTotalAssertions());
scenarioResult.addPassAssertions(requestResult.getPassAssertions());
scenarioResult.addTotalAssertions(requestResult.getTotalAssertions());
});
testResult.getScenarios().addAll(scenarios.values());
apiTestService.changeStatus(id, APITestStatus.Completed);
apiReportService.save(testResult);
});
queue.clear();
super.teardownTest(context);
}
private RequestResult getRequestResult(SampleResult result) {
RequestResult requestResult = new RequestResult();
requestResult.setName(result.getSampleLabel());
requestResult.setUrl(result.getUrlAsString());
requestResult.setSuccess(result.isSuccessful());
requestResult.setBody(result.getSamplerData());
requestResult.setHeaders(result.getRequestHeaders());
requestResult.setRequestSize(result.getSentBytes());
requestResult.setTotalAssertions(result.getAssertionResults().length);
ResponseResult responseResult = requestResult.getResponseResult();
responseResult.setBody(result.getResponseDataAsString());
responseResult.setHeaders(result.getResponseHeaders());
responseResult.setLatency(result.getLatency());
responseResult.setResponseCode(result.getResponseCode());
responseResult.setResponseSize(result.getResponseData().length);
responseResult.setResponseTime(result.getTime());
responseResult.setResponseMessage(result.getResponseMessage());
for (AssertionResult assertionResult : result.getAssertionResults()) {
ResponseAssertionResult responseAssertionResult = getResponseAssertionResult(assertionResult);
if (responseAssertionResult.isPass()) {
requestResult.addPassAssertions();
}
responseResult.getAssertions().add(responseAssertionResult);
}
return requestResult;
}
private ResponseAssertionResult getResponseAssertionResult(AssertionResult assertionResult) {
ResponseAssertionResult responseAssertionResult = new ResponseAssertionResult();
responseAssertionResult.setMessage(assertionResult.getFailureMessage());
responseAssertionResult.setName(assertionResult.getName());
responseAssertionResult.setPass(!assertionResult.isFailure());
return responseAssertionResult;
}
}

View File

@ -0,0 +1,32 @@
package io.metersphere.api.jmeter;
import lombok.Data;
@Data
public class RequestResult {
private String name;
private String url;
private long requestSize;
private boolean success;
private String headers;
private String cookies;
private String body;
private int totalAssertions = 0;
private int passAssertions = 0;
private final ResponseResult responseResult = new ResponseResult();
public void addPassAssertions() {
this.passAssertions++;
}
}

View File

@ -0,0 +1,13 @@
package io.metersphere.api.jmeter;
import lombok.Data;
@Data
public class ResponseAssertionResult {
private String name;
private String message;
private boolean pass;
}

View File

@ -0,0 +1,28 @@
package io.metersphere.api.jmeter;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class ResponseResult {
private String responseCode;
private String responseMessage;
private long responseTime;
private long latency;
private long responseSize;
private String headers;
private String body;
private final List<ResponseAssertionResult> assertions = new ArrayList<>();
}

View File

@ -0,0 +1,42 @@
package io.metersphere.api.jmeter;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class ScenarioResult {
private String id;
private String name;
private long responseTime;
private int error = 0;
private int success = 0;
private int totalAssertions = 0;
private int passAssertions = 0;
private final List<RequestResult> requestResult = new ArrayList<>();
public void addError() {
this.error++;
}
public void addSuccess() {
this.success++;
}
public void addTotalAssertions(int count) {
this.totalAssertions += count;
}
public void addPassAssertions(int count) {
this.passAssertions += count;
}
}

View File

@ -0,0 +1,41 @@
package io.metersphere.api.jmeter;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class TestResult {
private String id;
private int success = 0;
private int error = 0;
private int total = 0;
private int totalAssertions = 0;
private int passAssertions = 0;
private final List<ScenarioResult> scenarios = new ArrayList<>();
public void addError() {
this.error++;
}
public void addSuccess() {
this.success++;
}
public void addTotalAssertions(int count) {
this.totalAssertions += count;
}
public void addPassAssertions(int count) {
this.passAssertions += count;
}
}

View File

@ -0,0 +1,66 @@
package io.metersphere.api.service;
import com.alibaba.fastjson.JSONObject;
import io.metersphere.api.dto.APIReportResult;
import io.metersphere.api.dto.DeleteAPIReportRequest;
import io.metersphere.api.dto.QueryAPIReportRequest;
import io.metersphere.api.jmeter.TestResult;
import io.metersphere.base.domain.ApiTestReport;
import io.metersphere.base.domain.ApiTestWithBLOBs;
import io.metersphere.base.mapper.ApiTestReportMapper;
import io.metersphere.base.mapper.ext.ExtApiTestReportMapper;
import io.metersphere.commons.constants.APITestStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.UUID;
import javax.annotation.Resource;
@Service
@Transactional(rollbackFor = Exception.class)
public class APIReportService {
@Resource
private APITestService apiTestService;
@Resource
private ApiTestReportMapper apiTestReportMapper;
@Resource
private ExtApiTestReportMapper extApiTestReportMapper;
public List<APIReportResult> list(QueryAPIReportRequest request) {
return extApiTestReportMapper.list(request);
}
public List<APIReportResult> recentTest(QueryAPIReportRequest request) {
request.setRecent(true);
return extApiTestReportMapper.list(request);
}
public ApiTestReport get(String id) {
return apiTestReportMapper.selectByPrimaryKey(id);
}
public List<APIReportResult> listByTestId(String testId) {
return extApiTestReportMapper.listByTestId(testId);
}
public void delete(DeleteAPIReportRequest request) {
apiTestReportMapper.deleteByPrimaryKey(request.getId());
}
public void save(TestResult result) {
ApiTestWithBLOBs test = apiTestService.get(result.getId());
ApiTestReport report = new ApiTestReport();
report.setId(UUID.randomUUID().toString());
report.setTestId(result.getId());
report.setName(test.getName());
report.setDescription(test.getDescription());
report.setContent(JSONObject.toJSONString(result));
report.setCreateTime(System.currentTimeMillis());
report.setUpdateTime(System.currentTimeMillis());
report.setStatus(APITestStatus.Completed.name());
apiTestReportMapper.insert(report);
}
}

View File

@ -13,22 +13,21 @@ import io.metersphere.commons.constants.APITestStatus;
import io.metersphere.commons.exception.MSException;
import io.metersphere.i18n.Translator;
import io.metersphere.service.FileService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.annotation.Resource;
@Service
@Transactional(rollbackFor = Exception.class)
public class ApiTestService {
public class APITestService {
@Resource
private ApiTestMapper apiTestMapper;
@ -50,28 +49,21 @@ public class ApiTestService {
return extApiTestMapper.list(request);
}
public String save(SaveAPITestRequest request, List<MultipartFile> files) {
public void create(SaveAPITestRequest request, List<MultipartFile> files) {
if (files == null || files.isEmpty()) {
throw new IllegalArgumentException(Translator.get("file_cannot_be_null"));
}
ApiTestWithBLOBs test = createTest(request);
saveFile(test.getId(), files);
}
final ApiTestWithBLOBs test;
if (StringUtils.isNotBlank(request.getId())) {
// 删除原来的文件
deleteFileByTestId(request.getId());
test = updateTest(request);
} else {
test = createTest(request);
public void update(SaveAPITestRequest request, List<MultipartFile> files) {
if (files == null || files.isEmpty()) {
throw new IllegalArgumentException(Translator.get("file_cannot_be_null"));
}
// 保存新文件
files.forEach(file -> {
final FileMetadata fileMetadata = fileService.saveFile(file);
ApiTestFile apiTestFile = new ApiTestFile();
apiTestFile.setTestId(test.getId());
apiTestFile.setFileId(fileMetadata.getId());
apiTestFileMapper.insert(apiTestFile);
});
return test.getId();
deleteFileByTestId(request.getId());
ApiTestWithBLOBs test = updateTest(request);
saveFile(test.getId(), files);
}
public ApiTestWithBLOBs get(String id) {
@ -83,15 +75,15 @@ public class ApiTestService {
apiTestMapper.deleteByPrimaryKey(request.getId());
}
public String run(SaveAPITestRequest request, List<MultipartFile> files) {
String id = save(request, files);
try {
changeStatus(request.getId(), APITestStatus.Running);
jMeterService.run(files.get(0).getInputStream());
} catch (IOException e) {
MSException.throwException(Translator.get("api_load_script_error"));
public void run(SaveAPITestRequest request) {
ApiTestFile file = getFileByTestId(request.getId());
if (file == null) {
MSException.throwException(Translator.get("file_cannot_be_null"));
}
return id;
byte[] bytes = fileService.loadFileAsBytes(file.getFileId());
InputStream is = new ByteArrayInputStream(bytes);
changeStatus(request.getId(), APITestStatus.Running);
jMeterService.run(is);
}
public void changeStatus(String id, APITestStatus status) {
@ -121,7 +113,7 @@ public class ApiTestService {
}
final ApiTestWithBLOBs test = new ApiTestWithBLOBs();
test.setId(UUID.randomUUID().toString());
test.setId(request.getId());
test.setName(request.getName());
test.setProjectId(request.getProjectId());
test.setScenarioDefinition(request.getScenarioDefinition());
@ -132,6 +124,16 @@ public class ApiTestService {
return test;
}
private void saveFile(String testId, List<MultipartFile> files) {
files.forEach(file -> {
final FileMetadata fileMetadata = fileService.saveFile(file);
ApiTestFile apiTestFile = new ApiTestFile();
apiTestFile.setTestId(testId);
apiTestFile.setFileId(fileMetadata.getId());
apiTestFileMapper.insert(apiTestFile);
});
}
private void deleteFileByTestId(String testId) {
ApiTestFileExample ApiTestFileExample = new ApiTestFileExample();
ApiTestFileExample.createCriteria().andTestIdEqualTo(testId);

View File

@ -1,6 +1,7 @@
package io.metersphere.base.mapper.ext;
import io.metersphere.controller.request.ReportRequest;
import io.metersphere.api.dto.APIReportResult;
import io.metersphere.api.dto.QueryAPIReportRequest;
import io.metersphere.dto.ApiReportDTO;
import org.apache.ibatis.annotations.Param;
@ -8,7 +9,8 @@ import java.util.List;
public interface ExtApiTestReportMapper {
List<ApiReportDTO> getReportList(@Param("reportRequest") ReportRequest request);
List<APIReportResult> list(@Param("request") QueryAPIReportRequest request);
List<APIReportResult> listByTestId(@Param("testId") String testId);
ApiReportDTO getReportTestAndProInfo(@Param("id") String id);
}

View File

@ -2,24 +2,43 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="io.metersphere.base.mapper.ext.ExtApiTestReportMapper">
<select id="getReportList" resultType="io.metersphere.dto.ApiReportDTO">
select ltr.id, ltr.name, ltr.test_id as testId, ltr.description,
ltr.create_time as createTime, ltr.update_time as updateTime, ltr.status as status, lt.name as testName
from api_test_report ltr left join api_test lt on ltr.test_id = lt.id
<resultMap id="BaseResultMap" type="io.metersphere.api.dto.APIReportResult"
extends="io.metersphere.base.mapper.ApiTestReportMapper.BaseResultMap">
<result column="project_name" property="projectName"/>
</resultMap>
<select id="list" resultMap="BaseResultMap">
SELECT t.name, t.description,
r.id, r.test_id, r.create_time, r.update_time, r.status,
project.name AS project_name
FROM api_test_report r JOIN api_test t ON r.test_id = t.id
JOIN project ON project.id = t.project_id
<where>
<if test="reportRequest.name != null">
AND ltr.name like CONCAT('%', #{reportRequest.name},'%')
<if test="request.name != null">
AND r.name like CONCAT('%', #{request.name},'%')
</if>
<if test="request.projectId != null">
AND project.id = #{request.projectId}
</if>
<if test="request.workspaceId != null">
AND project.workspace_id = #{request.workspaceId,jdbcType=VARCHAR}
</if>
</where>
<if test="request.recent">
ORDER BY r.update_time DESC
</if>
</select>
<select id="getReportTestAndProInfo" resultType="io.metersphere.dto.ApiReportDTO">
select ltr.id, ltr.name, ltr.test_id as testId, ltr.description,
ltr.create_time as createTime, ltr.update_time as updateTime, ltr.status as status, ltr.content as content,
lt.name as testName,
p.id as projectId, p.name as projectName
from api_test_report ltr left join api_test lt on ltr.test_id = lt.id left join project p on lt.project_id = p.id
where ltr.id = #{id}
<select id="listByTestId" resultMap="BaseResultMap">
SELECT t.name, t.description,
r.id, r.test_id, r.create_time, r.update_time, r.status,
project.name AS project_name
FROM api_test_report r JOIN api_test t ON r.test_id = t.id
JOIN project ON project.id = t.project_id
<where>
r.test_id = #{testId}
</where>
ORDER BY r.update_time DESC
</select>
</mapper>

View File

@ -1,53 +0,0 @@
package io.metersphere.controller;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import io.metersphere.base.domain.ApiTestReport;
import io.metersphere.commons.constants.RoleConstants;
import io.metersphere.commons.utils.PageUtils;
import io.metersphere.commons.utils.Pager;
import io.metersphere.controller.request.ReportRequest;
import io.metersphere.dto.ApiReportDTO;
import io.metersphere.service.ApiReportService;
import io.metersphere.user.SessionUtils;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping(value = "/api/report")
public class ApiReportController {
@Resource
private ApiReportService apiReportService;
@GetMapping("/recent/{count}")
@RequiresRoles(value = {RoleConstants.TEST_MANAGER, RoleConstants.TEST_USER, RoleConstants.TEST_VIEWER}, logical = Logical.OR)
public List<ApiTestReport> recentProjects(@PathVariable int count) {
String currentWorkspaceId = SessionUtils.getCurrentWorkspaceId();
ReportRequest request = new ReportRequest();
request.setWorkspaceId(currentWorkspaceId);
PageHelper.startPage(1, count);
return apiReportService.getRecentReportList(request);
}
@PostMapping("/list/all/{goPage}/{pageSize}")
public Pager<List<ApiReportDTO>> getReportList(@PathVariable int goPage, @PathVariable int pageSize, @RequestBody ReportRequest request) {
Page<Object> page = PageHelper.startPage(goPage, pageSize, true);
return PageUtils.setPageInfo(page, apiReportService.getReportList(request));
}
@PostMapping("/delete/{reportId}")
public void deleteReport(@PathVariable String reportId) {
apiReportService.deleteReport(reportId);
}
@GetMapping("/test/pro/info/{reportId}")
public ApiReportDTO getReportTestAndProInfo(@PathVariable String reportId) {
return apiReportService.getReportTestAndProInfo(reportId);
}
}

View File

@ -1,42 +0,0 @@
package io.metersphere.service;
import io.metersphere.base.domain.ApiTestReport;
import io.metersphere.base.domain.ApiTestReportExample;
import io.metersphere.base.mapper.ApiTestReportMapper;
import io.metersphere.base.mapper.ext.ExtApiTestReportMapper;
import io.metersphere.controller.request.ReportRequest;
import io.metersphere.dto.ApiReportDTO;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;
@Service
@Transactional(rollbackFor = Exception.class)
public class ApiReportService {
@Resource
private ApiTestReportMapper ApiTestReportMapper;
@Resource
private ExtApiTestReportMapper extApiTestReportMapper;
public List<ApiTestReport> getRecentReportList(ReportRequest request) {
ApiTestReportExample example = new ApiTestReportExample();
example.setOrderByClause("update_time desc");
return ApiTestReportMapper.selectByExample(example);
}
public List<ApiReportDTO> getReportList(ReportRequest request) {
return extApiTestReportMapper.getReportList(request);
}
public void deleteReport(String reportId) {
ApiTestReportMapper.deleteByPrimaryKey(reportId);
}
public ApiReportDTO getReportTestAndProInfo(String reportId) {
return extApiTestReportMapper.getReportTestAndProInfo(reportId);
}
}

View File

@ -32,7 +32,7 @@
<template v-slot:title>{{$t('commons.report')}}</template>
<ms-recent-list :options="reportRecent"/>
<el-divider/>
<ms-show-all :index="'/api/report/all'"/>
<ms-show-all :index="'/api/report/list/all'"/>
<!-- <el-menu-item :index="reportViewPath" class="blank_item"></el-menu-item>-->
</el-submenu>
</el-menu>
@ -90,7 +90,7 @@
title: this.$t('report.recent'),
url: "/api/report/recent/5",
index: function (item) {
return '/api/report/view/' + item.id;
return '/api/report/view?id=' + item.id;
}
}
}

View File

@ -0,0 +1,124 @@
<template>
<div class="container" v-loading="result.loading">
<div class="main-content">
<el-card>
<template v-slot:header>
<ms-table-header :condition.sync="condition" @search="search" :title="$t('commons.test')"
:show-create="false"/>
</template>
<el-table :data="tableData" class="table-content">
<el-table-column
prop="name"
:label="$t('commons.name')"
width="150"
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="description"
:label="$t('commons.description')"
show-overflow-tooltip>
</el-table-column>
<el-table-column
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
width="250"
:label="$t('commons.update_time')">
<template v-slot:default="scope">
<span>{{ scope.row.updateTime | timestampFormatDate }}</span>
</template>
</el-table-column>
<el-table-column
width="150"
:label="$t('commons.operating')">
<template v-slot:default="scope">
<el-button @click="handleView(scope.row)" type="primary" icon="el-icon-s-data" size="mini" circle/>
<el-button @click="handleDelete(scope.row)" type="danger" icon="el-icon-delete" size="mini" circle/>
</template>
</el-table-column>
</el-table>
<ms-table-pagination :change="search" :current-page.sync="currentPage" :page-size.sync="pageSize"
:total="total"/>
</el-card>
</div>
</div>
</template>
<script>
import MsTablePagination from "../../common/pagination/TablePagination";
import MsTableHeader from "../../common/components/MsTableHeader";
export default {
components: {MsTableHeader, MsTablePagination},
data() {
return {
result: {},
condition: {name: ""},
projectId: null,
tableData: [],
multipleSelection: [],
currentPage: 1,
pageSize: 5,
total: 0,
loading: false
}
},
beforeRouteEnter(to, from, next) {
next(self => {
self.testId = to.params.testId;
self.search();
});
},
methods: {
search() {
let param = {
name: this.condition.name,
};
if (this.testId !== 'all') {
param.testId = this.testId;
}
let url = "/api/report/list/" + this.currentPage + "/" + this.pageSize
this.result = this.$post(url, param, response => {
let data = response.data;
this.total = data.itemCount;
this.tableData = data.listObject;
});
},
handleSelectionChange(val) {
this.multipleSelection = val;
},
handleView(report) {
this.$router.push({
path: '/api/report/view/' + report.id,
})
},
handleDelete(report) {
this.$alert(this.$t('load_test.delete_confirm') + report.name + "", '', {
confirmButtonText: this.$t('commons.confirm'),
callback: (action) => {
if (action === 'confirm') {
this.result = this.$post("/api/report/delete", {id: report.id}, () => {
this.$success(this.$t('commons.delete_success'));
this.search();
});
}
}
});
}
}
}
</script>
<style scoped>
.table-content {
width: 100%;
}
</style>

View File

@ -1,169 +0,0 @@
<template>
<div class="container" v-loading="result.loading">
<div class="main-content">
<el-card>
<template v-slot:header>
<el-row type="flex" justify="space-between" align="middle">
<span class="title">{{$t('commons.report')}}</span>
<span class="search">
<el-input type="text" size="small" :placeholder="$t('report.search_by_name')"
prefix-icon="el-icon-search"
maxlength="60"
v-model="condition" @change="search" clearable/>
</span>
</el-row>
</template>
<el-table :data="tableData" class="test-content">
<el-table-column
prop="name"
:label="$t('commons.name')"
width="150"
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="description"
:label="$t('commons.description')"
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
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
width="250"
:label="$t('commons.update_time')">
<template v-slot:default="scope">
<span>{{ scope.row.updateTime | timestampFormatDate }}</span>
</template>
</el-table-column>
<el-table-column
width="150"
:label="$t('commons.operating')">
<template v-slot:default="scope">
<el-button @click="handleEdit(scope.row)" type="primary" icon="el-icon-edit" size="mini" circle/>
<el-button @click="handleDelete(scope.row)" type="danger" icon="el-icon-delete" size="mini" circle/>
</template>
</el-table-column>
</el-table>
<div>
<el-row>
<el-col :span="22" :offset="1">
<div class="table-page">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page.sync="currentPage"
:page-sizes="[5, 10, 20, 50, 100]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
</el-col>
</el-row>
</div>
</el-card>
</div>
</div>
</template>
<script>
export default {
name: "ApiTestReport",
created: function () {
this.initTableData();
},
data() {
return {
result: {},
queryPath: "/api/report/list/all",
deletePath: "/api/report/delete/",
condition: "",
projectId: null,
tableData: [],
multipleSelection: [],
currentPage: 1,
pageSize: 5,
total: 0,
loading: false,
testId: null,
}
},
methods: {
initTableData() {
let param = {
name: this.condition,
};
this.result = this.$post(this.buildPagePath(this.queryPath), param, response => {
let data = response.data;
this.total = data.itemCount;
this.tableData = data.listObject;
});
},
search() {
this.initTableData();
},
buildPagePath(path) {
return path + "/" + this.currentPage + "/" + this.pageSize;
},
handleSizeChange(size) {
this.pageSize = size;
this.initTableData();
},
handleCurrentChange(current) {
this.currentPage = current;
this.initTableData();
},
handleSelectionChange(val) {
this.multipleSelection = val;
},
handleEdit(report) {
this.$router.push({
path: '/api/report/view/' + report.id
})
},
handleDelete(report) {
this.$alert(this.$t('load_test.delete_confirm') + report.name + "", '', {
confirmButtonText: this.$t('commons.confirm'),
callback: (action) => {
if (action === 'confirm') {
this._handleDelete(report);
}
}
});
},
_handleDelete(report) {
this.result = this.$post(this.deletePath + report.id, {}, () => {
this.$message({
message: this.$t('commons.delete_success'),
type: 'success'
});
this.initTableData();
});
},
}
}
</script>
<style scoped>
.test-content {
width: 100%;
}
.table-page {
padding-top: 20px;
margin-right: -9px;
float: right;
}
</style>

View File

@ -16,7 +16,7 @@
{{$t('commons.save')}}
</el-button>
<el-button type="primary" plain :disabled="isDisabled" @click="runTest">
<el-button type="primary" plain @click="runTest">
{{$t('load_test.save_and_run')}}
</el-button>
<el-button type="warning" plain @click="cancel">{{$t('commons.cancel')}}</el-button>
@ -42,6 +42,7 @@
data() {
return {
create: false,
result: {},
projects: [],
change: false,
@ -65,8 +66,10 @@
this.projects = response.data;
})
if (this.id) {
this.create = false;
this.getTest(this.id);
} else {
this.create = true;
this.test = new Test();
if (this.$refs.config) {
this.$refs.config.reset();
@ -89,20 +92,28 @@
});
},
saveTest: function () {
this.change = false;
this.result = this.$request(this.getOptions("/api/save"), response => {
this.test.id = response.data;
this.save(() => {
this.$success(this.$t('commons.save_success'));
})
},
save: function (callback) {
this.change = false;
let url = this.create ? "/api/create" : "/api/update";
this.result = this.$request(this.getOptions(url), response => {
this.create = false;
if (callback) callback();
});
},
runTest: function () {
this.change = false;
this.result = this.$request(this.getOptions("/api/run"), response => {
this.test.id = response.data;
this.save(() => {
this.$success(this.$t('commons.save_success'));
});
this.result = this.$post("/api/run", {id: this.test.id}, response => {
this.$success(this.$t('api_test.running'));
});
})
},
cancel: function () {
this.$router.push('/api/test/list/all');
@ -123,7 +134,6 @@
let jmx = this.test.toJMX();
let blob = new Blob([jmx.xml], {type: "application/octet-stream"});
formData.append("files", new File([blob], jmx.name));
console.log(jmx.xml)
return {
method: 'POST',

View File

@ -6,7 +6,7 @@
<ms-table-header :condition.sync="condition" @search="search" :title="$t('commons.test')"
@create="create" :createTip="$t('load_test.create')"/>
</template>
<el-table :data="tableData" class="test-content">
<el-table :data="tableData" class="table-content">
<el-table-column
prop="name"
:label="$t('commons.name')"
@ -70,8 +70,7 @@
currentPage: 1,
pageSize: 5,
total: 0,
loading: false,
testId: null,
loading: false
}
},
@ -128,7 +127,7 @@
</script>
<style scoped>
.test-content {
.table-content {
width: 100%;
}
</style>

View File

@ -204,6 +204,39 @@ export class ThreadGroup extends DefaultTestElement {
}
}
export class PostThreadGroup extends DefaultTestElement {
constructor(testName) {
super('PostThreadGroup', 'PostThreadGroupGui', 'PostThreadGroup', testName || 'tearDown Thread Group');
this.intProp("ThreadGroup.num_threads", 1);
this.intProp("ThreadGroup.ramp_time", 1);
this.boolProp("ThreadGroup.scheduler", false);
this.stringProp("ThreadGroup.on_sample_error", "continue");
let loopAttrs = {
name: "ThreadGroup.main_controller",
elementType: "LoopController",
guiclass: "LoopControlPanel",
testclass: "LoopController",
testname: "Loop Controller",
enabled: "true"
};
let loopController = this.add(new Element('elementProp', loopAttrs));
loopController.boolProp('LoopController.continue_forever', false);
loopController.stringProp('LoopController.loops', 1);
}
}
export class DebugSampler extends DefaultTestElement {
constructor(testName) {
super('DebugSampler', 'TestBeanGUI', 'DebugSampler', testName || 'Debug Sampler');
this.boolProp("displayJMeterProperties", false);
this.boolProp("displayJMeterVariables", true);
this.boolProp("displaySystemProperties", false);
}
}
export class HTTPSamplerProxy extends DefaultTestElement {
constructor(testName, request) {
super('HTTPSamplerProxy', 'HttpTestSampleGui', 'HTTPSamplerProxy', testName || 'HTTP Request');
@ -219,37 +252,6 @@ export class HTTPSamplerProxy extends DefaultTestElement {
this.stringProp("HTTPSampler.port", this.request.port);
}
}
addRequestArguments(arg) {
if (arg instanceof HTTPSamplerArguments) {
this.add(arg);
}
}
addRequestBody(body) {
if (body instanceof HTTPSamplerArguments) {
this.boolProp('HTTPSampler.postBodyRaw', true);
this.add(body);
}
}
putRequestHeader(header) {
if (header instanceof HeaderManager) {
this.put(header);
}
}
putResponseAssertion(assertion) {
if (assertion instanceof ResponseAssertion) {
this.put(assertion);
}
}
putDurationAssertion(assertion) {
if (assertion instanceof DurationAssertion) {
this.put(assertion);
}
}
}
// 这是一个Element
@ -373,7 +375,9 @@ export class BackendListener extends DefaultTestElement {
constructor(testName, className, args) {
super('BackendListener', 'BackendListenerGui', 'BackendListener', testName || 'Backend Listener');
this.stringProp('classname', className);
this.add(new ElementArguments(args));
if (args && args.length > 0) {
this.add(new ElementArguments(args));
}
}
}
@ -397,3 +401,6 @@ export class ElementArguments extends Element {
}
}
export class Class {
}

View File

@ -5,6 +5,8 @@ import {
TestElement,
TestPlan,
ThreadGroup,
PostThreadGroup,
DebugSampler,
HeaderManager,
HTTPSamplerArguments,
ResponseCodeAssertion,
@ -13,9 +15,21 @@ import {
BackendListener
} from "./JMX";
export const generateId = function () {
return Math.floor(Math.random() * 10000);
};
export const uuid = function () {
let d = new Date().getTime()
let d2 = (performance && performance.now && (performance.now() * 1000)) || 0;
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
let r = Math.random() * 16;
if (d > 0) {
r = (d + r) % 16 | 0;
d = Math.floor(d / 16);
} else {
r = (d2 + r) % 16 | 0;
d2 = Math.floor(d2 / 16);
}
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
}
export const BODY_TYPE = {
KV: "KeyValue",
@ -75,7 +89,7 @@ export class Test extends BaseConfig {
constructor(options) {
super();
this.version = '1.0.0';
this.id = null;
this.id = uuid();
this.name = null;
this.projectId = null;
this.scenarioDefinition = [];
@ -101,7 +115,7 @@ export class Test extends BaseConfig {
export class Scenario extends BaseConfig {
constructor(options) {
super();
this.id = generateId();
this.id = uuid();
this.name = null;
this.url = null;
this.parameters = [];
@ -122,7 +136,7 @@ export class Scenario extends BaseConfig {
export class Request extends BaseConfig {
constructor(options) {
super();
this.id = generateId();
this.id = uuid();
this.name = null;
this.url = null;
this.method = null;
@ -144,6 +158,10 @@ export class Request extends BaseConfig {
options.assertions = new Assertions(options.assertions);
return options;
}
isValid() {
return !!this.url && !!this.method
}
}
export class Body extends BaseConfig {
@ -304,14 +322,23 @@ class JMeterTestPlan extends Element {
class JMXGenerator {
constructor(test) {
if (!test || !test.id || !(test instanceof Test)) return;
if (!test || !(test instanceof Test)) return null;
if (!test.id) {
test.id = "#NULL_TEST_ID#";
}
const SPLIT = "@@:";
let testPlan = new TestPlan(test.name);
test.scenarioDefinition.forEach(scenario => {
let threadGroup = new ThreadGroup(scenario.name);
let threadGroup = new ThreadGroup(scenario.name + SPLIT + scenario.id);
scenario.requests.forEach(request => {
let httpSamplerProxy = new HTTPSamplerProxy(request.name, new JMXRequest(request));
if (!request.isValid()) return;
// test.id用于处理结果时区分属于哪个测试
let name = request.name + SPLIT + test.id;
let httpSamplerProxy = new HTTPSamplerProxy(name, new JMXRequest(request));
this.addRequestHeader(httpSamplerProxy, request);
@ -326,8 +353,15 @@ class JMXGenerator {
threadGroup.put(httpSamplerProxy);
})
this.addBackendListener(threadGroup, test.id);
this.addBackendListener(threadGroup);
testPlan.put(threadGroup);
// 暂时不加
// let tearDownThreadGroup = new PostThreadGroup();
// tearDownThreadGroup.put(new DebugSampler(test.id));
// this.addBackendListener(tearDownThreadGroup);
//
// testPlan.put(tearDownThreadGroup);
})
this.jmeterTestPlan = new JMeterTestPlan();
@ -338,14 +372,14 @@ class JMXGenerator {
let name = request.name + " Headers";
let headers = request.headers.filter(this.filter);
if (headers.length > 0) {
httpSamplerProxy.putRequestHeader(new HeaderManager(name, headers));
httpSamplerProxy.put(new HeaderManager(name, headers));
}
}
addRequestArguments(httpSamplerProxy, request) {
let args = request.parameters.filter(this.filter)
if (args.length > 0) {
httpSamplerProxy.addRequestArguments(new HTTPSamplerArguments(args));
httpSamplerProxy.add(new HTTPSamplerArguments(args));
}
}
@ -357,19 +391,20 @@ class JMXGenerator {
body.push({name: '', value: request.body.raw});
}
httpSamplerProxy.addRequestBody(new HTTPSamplerArguments(body));
httpSamplerProxy.boolProp('HTTPSampler.postBodyRaw', true);
httpSamplerProxy.add(new HTTPSamplerArguments(body));
}
addRequestAssertion(httpSamplerProxy, request) {
let assertions = request.assertions;
if (assertions.regex.length > 0) {
assertions.regex.filter(this.filter).forEach(regex => {
httpSamplerProxy.putResponseAssertion(this.getAssertion(regex));
httpSamplerProxy.put(this.getAssertion(regex));
})
}
if (assertions.duration.isValid()) {
httpSamplerProxy.putDurationAssertion(assertions.duration.type, assertions.duration.value);
httpSamplerProxy.put(assertions.duration.type, assertions.duration.value);
}
}
@ -387,11 +422,10 @@ class JMXGenerator {
}
}
addBackendListener(threadGroup, testId) {
addBackendListener(threadGroup) {
let testName = 'API Backend Listener';
let className = 'io.metersphere.api.jmeter.APIBackendListenerClient';
let args = [{name: 'id', value: testId}];
threadGroup.put(new BackendListener(testName, className, args));
threadGroup.put(new BackendListener(testName, className));
}
filter(config) {

View File

@ -16,7 +16,6 @@ import PersonSetting from "../../settings/personal/PersonSetting";
import SystemWorkspace from "../../settings/system/SystemWorkspace";
import PerformanceChart from "../../performance/report/components/PerformanceChart";
import PerformanceTestReport from "../../performance/report/PerformanceTestReport";
import ApiTestReport from "../../api/report/ApiTestReport";
import ApiTest from "../../api/ApiTest";
import PerformanceTest from "../../performance/PerformanceTest";
import ApiTestConfig from "../../api/test/ApiTestConfig";
@ -30,6 +29,7 @@ import TestPlan from "../../track/plan/TestPlan";
import TestPlanView from "../../track/plan/view/TestPlanView";
import TestCase from "../../track/case/TestCase";
import TestTrack from "../../track/TestTrack";
import ApiReportList from "../../api/report/ApiReportList";
Vue.use(VueRouter);
@ -117,13 +117,13 @@ const router = new VueRouter({
component: MsProject
},
{
path: "report/:type",
name: "fucReport",
component: ApiTestReport
path: "report/list/:testId",
name: "ApiReportList",
component: ApiReportList
},
{
path: "report/view/:reportId",
name: "fucReportView",
name: "ApiReportView",
component: ApiReportView
}
]

View File

@ -176,6 +176,7 @@ export default {
},
api_test: {
save_and_run: "保存并执行",
running: "正在执行",
reset: "重置",
input_name: "请输入测试名称",
select_project: "请选择项目",