提交 f38e0b2e authored 作者: Matrix's avatar Matrix

feat(行为执行器): 增加了行为执行器的运行与行为变量解析

上级 acd1e56e
......@@ -3,6 +3,7 @@ package org.matrix.actuators.httpclient;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.matrix.actuators.usecase.BaseTestCaseResponseDetail;
......@@ -11,6 +12,7 @@ import org.springframework.http.HttpStatus;
/**
* @author huangxiahao
*/
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
......
package org.matrix.actuators.move;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Lists;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.matrix.actuators.Actuator;
import org.matrix.actuators.httpclient.HttpClientActuator;
import org.matrix.actuators.httpclient.HttpRequestDetail;
import org.matrix.actuators.httpclient.HttpResponseDetail;
import org.matrix.actuators.sql.SqlExpActuator;
import org.matrix.actuators.sql.SqlExpDetail;
import org.matrix.actuators.usecase.CaseActuator;
import org.matrix.actuators.util.CompleteExpressionUtil;
import org.matrix.database.entity.Action;
import org.matrix.database.entity.TestCaseBTO;
import org.matrix.database.service.ITestDataService;
import org.matrix.database.service.impl.ActionServiceImpl;
import org.matrix.enums.ActionType;
import org.matrix.exception.GlobalException;
import org.matrix.util.BeanFlattener;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.matrix.actuators.move.MoveStrategy.*;
import static org.matrix.enums.ActionType.*;
/**
* MoveActuator.
*
* @author Matrix <xhyrzldf@gmail.com>
* @since 2022/2/25 at 3:32 PM
* Suffering is the most powerful teacher of life.
*/
@SuppressWarnings("AlibabaLowerCamelCaseVariableNaming")
@Slf4j
@Component
@AllArgsConstructor
public class MoveActuator implements Actuator {
/**
* 识别行为语法的正则字符串
*/
private static final String MOVE_POOL_REG = "\\$\\{(?<col>[\\w|.]*)}(\\[(?<row>\\d*)])?(?<jp><(\\s)*\\$.*?>)?";
private static final List<String> STRATEGY_LIST = Arrays.asList(PRE_MOVE.name(), MID_MOVE.name(), AFT_MOVE.name());
private final CompleteExpressionUtil resultUtil;
private final ActionServiceImpl actionService;
private final SqlExpActuator sqlExpActuator;
private final HttpClientActuator httpActuator;
private final ITestDataService dataService;
private final CaseActuator caseActuator;
private final CompleteExpressionUtil expressionUtil;
/**
* 动作临时结果保存的的ThreadLocal的结果集保存,key = strategy_actionId (例如PRE_MOVE_1),value 是 数据表,如果存在则add,如果不存在则put
*/
private ThreadLocal<Map<String, List<Map<String, Object>>>> resSet = ThreadLocal.withInitial(HashMap::new);
/**
* 将含有动作结果池语法 ${pre1.name}[0]<$.book> 这样的语法 ${a}[b]<c>
* 其中a是定位列,b是定位行数,c是内容的二次JSON解析
* 可能出现的情况是
* <blockquote>
* <pre>
* a -> 默认第一行,直接返回某个列的数据
* ab -> 第a列第b行,直接返回
* ac -> 第a列第一行,取到的数据用c去解析
* abc -> 第a列第b行,取到的数据用c去解析
* </pre>
* </blockquote>
*
* @param dynamicString 含有动作结果池的字段 ${pre1.name}[0]<$.book> 简写就是 ${a}[b]<c>
* @return 找出动作结果池语法里其中的变量名与对应的下标序列值(默认为0)
*/
public String parseMoveVar(String dynamicString) {
Pattern pattern = Pattern.compile(MOVE_POOL_REG);
Matcher matcher = pattern.matcher(dynamicString);
String col, row, jsonPath;
int rowIndex;
boolean pathResolution;
// 提取、提取、补全 col,row,json_path内容
if (matcher.find()) {
col = matcher.group("col");
if (StringUtils.isEmpty(col)) {
throw new GlobalException("动作语法里必须包含形如${pre1.name}这样的字符串!您提供的是: " + dynamicString);
}
row = matcher.group("row");
if (StringUtils.isEmpty(row)) {
rowIndex = 0;
} else {
rowIndex = Integer.parseInt(row);
}
jsonPath = matcher.group("jp");
if (StringUtils.isEmpty(jsonPath)) {
pathResolution = false;
} else {
pathResolution = true;
}
} else {
throw new GlobalException("提供的语法不正确!正确的语法应当是形如 ${pre1.name}[0]<$.book> 这样的,您提供的是 :" + dynamicString);
}
// 解析,解释 col、row、jp
MoveRegularObject mro = col2RegularObj(col);
List<Map<String, Object>> valueMap = resSet.get().get(mro.getKey());
if (valueMap.size() <= rowIndex) {
throw new GlobalException(String.format("没有找到指定行的数据,您要求的数据行数为%d,实际存在的数据行数为%d", rowIndex, valueMap.size()));
}
String finalValue = null;
if (valueMap.get(rowIndex).get(mro.getColName()) == null) {
log.warn("[行为执行器] {}语法下的 {} 列 数据为 null", dynamicString, mro.getColName());
} else {
finalValue = valueMap.get(rowIndex).get(mro.getColName()).toString();
}
// 是否需要继续解析jsonPath
if (pathResolution) {
finalValue = expressionUtil.completeJsonPathExpression(jsonPath, finalValue);
}
return finalValue;
}
/**
* 中间行为执行器,根据运行时环境决定使用哪一个动作,并根据动作的类型调用不同的动作执行器(该方法用于中间动作与后置动作)
* 其中中间动作与后置动作需要进行结果集数据的替换,然后根据动作类型,策略,ID 来制作主键
* key = actionType_strategy_actionId
* 当 动作类型是 SQL 类型时,value = 数据表
* 当 动作类型是 HTTP/CASE 类型时, value = detail对象的属性(key=属性名,value=属性数值)
*
* @param moveId 行为id
* @param envId 环境id
* @param projectId 项目id
* @param caseResultData 测试用例执行后保留下的结果集,该参数在前置策略中可以为null,在中间/后置策略中必须要提供合理的结果集对象
* @param strategy 动作的策略,即是前置/中间/后置,具体查看{@link MoveStrategy}
*/
public void runMove(Long moveId, Long envId, Long projectId, Object caseResultData, MoveStrategy strategy) {
Action action = actionService.findByMoveAndEnv(moveId, envId)
.orElseThrow(() -> new GlobalException(String.format("没有找到指定的action动作对象,提供查询的moveId = %d,envId = %d", moveId, envId)));
// 如果是 mid 或者 aft 动作,则需要进行结果集的语法替换 与 空值判断
if (strategy != PRE_MOVE) {
if (Objects.isNull(caseResultData)) {
throw new GlobalException(String.format("中间与后置动作必须要提供结果集! moveId = %d,envId = %d", moveId, envId));
}
// 尝试解析并替换动作中要用到用例结果集的情况
String runtimeDetail = resultUtil.completeJsonPathExpression(action.getDetail(), caseResultData);
action.setDetail(runtimeDetail);
}
// 拼接key,准备数据容器
ActionType actionType = action.getType();
String runtimeDetail = action.getDetail();
String key = Lists.newArrayList(strategy, moveId)
.stream().map(Object::toString)
.collect(Collectors.joining("_"));
Map<String, List<Map<String, Object>>> res = resSet.get();
// 分类处理 + 结果集处理(如果不存在则put数据,如果存在则替换数据)
log.info("[动作执行器] 正在执行动作 actionId = {},动作类型 = {},动作策略 = {},动作参数 = {}", action.getId(), action, strategy, runtimeDetail);
if (actionType == SQL_ACTION) {
List<Map<String, Object>> resultMap = sqlActionHandler(envId, projectId, runtimeDetail);
res.put(key, resultMap);
} else if (actionType == HTTP_ACTION) {
res.put(key, Lists.newArrayList(detail2Map(httpActionHandler(envId, projectId, runtimeDetail))));
} else if (actionType == CASE_ACTION) {
res.put(key, Lists.newArrayList(detail2Map(caseActionHandler(envId, projectId, runtimeDetail))));
} else if (actionType == WAIT_ACTION) {
waitActionHandler(runtimeDetail);
} else {
throw new GlobalException("不支持的行为类型: " + actionType);
}
}
/**
* 清理结果集
*/
public void clearMoveRes() {
log.info("[动作执行器] 清理动作结果池缓存");
resSet = ThreadLocal.withInitial(HashMap::new);
}
/**
* 将行为语法里的col部分转化为threadLocal里的key值
*
* @param col 原始col字符串
* @return threadLocal里的key值
*/
private MoveRegularObject col2RegularObj(String col) {
String[] colArray = col.split("\\.");
if (colArray.length != 2) {
throw new GlobalException("行为语法里定位部分的语法不符合规范,您提供的部分是: " + col);
}
String strategy = colArray[0].substring(0, 3).toUpperCase() + "_MOVE";
if (!STRATEGY_LIST.contains(strategy)) {
throw new GlobalException("行为语法里定位部分的语法不符合规范(需要至少包含pre/mid/aft),您提供的部分是:" + col);
}
int actionId;
try {
actionId = Integer.parseInt(colArray[0].substring(3));
} catch (NumberFormatException e) {
throw new GlobalException("行为语法里定位部分的语法没有提供合适的actionId!例如需要提供pre1,pre2,您提供的部分是: " + col);
}
String key = strategy + "_" + actionId;
return new MoveRegularObject(key, colArray[1]);
}
/**
* 线程等待执行器
*
* @param runtimeDetail 要等待的时间,单位毫秒ms
*/
private void waitActionHandler(String runtimeDetail) {
try {
int sleepMills = Integer.parseInt(runtimeDetail);
Thread.sleep(sleepMills);
} catch (InterruptedException e) {
log.error("[线程异常] 线程休眠时发生异常,异常信息 : " + e.getMessage());
} catch (Exception e) {
log.error("[参数/转换异常] 执行行为时参数抓换时发生异常,异常信息 " + e.getMessage());
}
}
/**
* 用例行为解析器
*
* @param envId 环境ID
* @param projectId 项目ID
* @param runtimeDetail 用例行为解析器
*/
private HttpResponseDetail caseActionHandler(Long envId, Long projectId, String runtimeDetail) {
long caseDataId = Long.parseLong(runtimeDetail);
TestCaseBTO caseBTO = dataService.toCaseBTO(caseDataId);
return (HttpResponseDetail) caseActuator.executeTestCase(caseBTO, envId, projectId)
.getBaseTestCaseRequestDetail();
}
/**
* http行为解析器
*
* @param envId 环境ID
* @param projectId 项目ID
* @param runtimeDetail http url 与 参数字符串
*/
private HttpResponseDetail httpActionHandler(Long envId, Long projectId, String runtimeDetail) {
HttpRequestDetail httpRequestDetail = JSON.parseObject(runtimeDetail, HttpRequestDetail.class);
return httpActuator.sendHttpRequest(httpRequestDetail, envId, projectId);
}
/**
* SQL动作解析器
*
* @param envId 环境ID
* @param projectId 项目ID
* @param runtimeDetail SQL字符串
*/
private List<Map<String, Object>> sqlActionHandler(Long envId, Long projectId, String runtimeDetail) {
SqlExpDetail sqlExpDetail = JSON.parseObject(runtimeDetail, SqlExpDetail.class);
return sqlExpActuator.parseSql(sqlExpDetail, envId, projectId);
}
private Map<String, Object> detail2Map(HttpResponseDetail detail) {
return BeanFlattener.deepToMap(detail);
}
}
package org.matrix.actuators.move;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* MoveRegularObject.
*
* @author Matrix <xhyrzldf@gmail.com>
* @since 2022/3/7 at 6:24 PM
* Suffering is the most powerful teacher of life.
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MoveRegularObject {
/**
* 在ThreadLocal里的key值
*/
private String key;
/**
* 要取的字段名
*/
private String colName;
}
package org.matrix.actuators.move;
/**
* MoveStrategy.
*
* @author Matrix <xhyrzldf@gmail.com>
* @since 2022/3/7 at 3:11 PM
* Suffering is the most powerful teacher of life.
*/
public enum MoveStrategy {
/**
* 前置动作
*/
PRE_MOVE,
/**
* 中间动作
*/
MID_MOVE,
/**
* 后置动作
*/
AFT_MOVE;
}
......@@ -45,7 +45,7 @@ import static org.springframework.util.CollectionUtils.isEmpty;
public class SqlExpActuator implements Actuator {
/**
* 用于正则找出形如${id}这样的SQL变量
* 用于正则找出形如${id} 或者 ${id}[1] 这样的SQL变量
*/
public static final String DYNAMIC_VAR_EXP = "\\$\\{(\\w*)}\\[?(\\d*)]?";
......@@ -62,13 +62,51 @@ public class SqlExpActuator implements Actuator {
private final IDynamicVariableService varService;
private final IConnectService connectService;
private final IDataSourceService dataSourceService;
private final ITestCaseService caseService;
private final ITestDataService dataService;
/**
* 解析并运行SQL语句,可以包含别的SQL变量,例如 'select * from user where user.id = ${user_id} '
* @param sqlDetail sql的poolId与sql语句的对象
* @param envId 环境id
* @param projectId 项目id
* @return 运行SQL后取完数值的结果, 是一张数据表, 以LIST<MAP>的形式呈现
*/
public List<Map<String, Object>> parseSql(SqlExpDetail sqlDetail,Long envId,Long projectId){
// 找到第一层的SQL变量,递归替换
String sqlExp = sqlDetail.getSqlExp();
Long connectId = sqlDetail.getPoolId();
List<SqlRegularObject> varList = findDynamicVarList(sqlExp);
for (SqlRegularObject sqlReg : varList) {
sqlExp = sqlExp.replaceAll(sqlReg.getVarName(), parseVarByName(sqlReg.getVarName(), envId, projectId));
}
// 运行SQL
DataSourceDTO dataSourceDTO = Optional.of(connectService.getById(connectId))
.orElseThrow(() -> new GlobalException(String.format("没有找到id = %d 的连接池connect对象", connectId)))
.toDataSourceDTO();
// 替换环境共享变量
sqlExp = envActuator.replaceEnvVar(sqlExp, envId);
// 校验dynamicVar里的detail是否是可以直接执行的SQL
boolean allVarParsed = findDynamicVarList(sqlExp).size() == 0;
if (allVarParsed) {
// 切换数据源,执行SQL,获取数值
Set<String> dataSources = dataSourceService.switchDataSource(dataSourceDTO);
log.info("当前存在的数据源 {}", dataSources);
List<Map<String, Object>> resultMap = jdbcTemplate.queryForList(sqlExp);
dataSourceService.switchMainDataSource();
return resultMap;
} else {
throw new GlobalException(String.format("SQL解析异常,SQL语句为%s,连接池id为%d", sqlDetail.getSqlExp(), sqlDetail.getPoolId()));
}
}
/**
* 解析给定的动态变量ByName
*
* @param varNameString 变量名,形如${id}这样的字符串
* @param varNameString 变量名,形如${id}或者是 ${id}[1]这样的字符串,默认下标为0
* @param projectId 该变量所在的项目ID
* @return 变量递归解析后的值
*/
......@@ -170,7 +208,8 @@ public class SqlExpActuator implements Actuator {
String sqlExp = dynamicVar.getSqlExpDetail().getSqlExp();
sqlExp = envActuator.replaceEnvVar(sqlExp, envId);
List<SqlRegularObject> dynamicVarList = findDynamicVarList(dynamicVar.getDetail());
String detail = dynamicVar.getDetail();
List<SqlRegularObject> dynamicVarList = findDynamicVarList(detail);
// 解析SQL表达式,判断是可以直接执行的SQL还是需要再递归解析动态变量
if (!isEmpty(dynamicVarList)) {
// 如果还存在动态变量,则继续递归解析
......
......@@ -7,6 +7,7 @@ import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.matrix.enums.ActionType;
/**
* <p>
......@@ -37,8 +38,8 @@ public class Action extends BaseEntity {
private String remark;
@ApiModelProperty("类型 1为SQL,2为HTTP,3为CASE,4为WAIT_TIME")
private Integer type;
private ActionType type;
@ApiModelProperty("详细参数")
private String detail;
......
......@@ -3,6 +3,8 @@ package org.matrix.database.service;
import org.matrix.database.entity.Action;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.Optional;
/**
* <p>
* 动作 服务类
......@@ -13,4 +15,6 @@ import com.baomidou.mybatisplus.extension.service.IService;
*/
public interface IActionService extends IService<Action> {
Optional<Action> findByMoveAndEnv(Long moveId, Long envId);
}
package org.matrix.database.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.matrix.database.entity.Action;
import org.matrix.database.mapper.ActionMapper;
import org.matrix.database.service.IActionService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import java.util.Optional;
/**
* <p>
* 动作 服务实现类
......@@ -17,4 +21,18 @@ import org.springframework.stereotype.Service;
@Service
public class ActionServiceImpl extends ServiceImpl<ActionMapper, Action> implements IActionService {
private final ActionMapper actionMapper;
public ActionServiceImpl(ActionMapper mapper) {
this.actionMapper = mapper;
}
@Override
public Optional<Action> findByMoveAndEnv(Long moveId, Long envId) {
LambdaQueryWrapper<Action> actionWrappers = Wrappers.lambdaQuery(Action.class);
Action action = actionMapper.selectOne(actionWrappers
.eq(Action::getMoveId, moveId)
.eq(Action::getEnvId, envId));
return Optional.ofNullable(action);
}
}
......@@ -35,7 +35,7 @@ public class DynamicVariableServiceImpl extends ServiceImpl<DynamicVariableMappe
*/
@Override
public Optional<DynamicVariable> getByName(String name, Long projectId) {
return Optional.ofNullable(mapper.selectOne(Wrappers.lambdaUpdate(DynamicVariable.class)
return Optional.ofNullable(mapper.selectOne(Wrappers.lambdaQuery(DynamicVariable.class)
.eq(DynamicVariable::getName, name)
.eq(DynamicVariable::getProjectId, projectId)));
}
......
package org.matrix.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* ActionType.
* 动作的类型,该变量决定了运行时使用哪一种执行器
*
* @author Matrix <xhyrzldf@gmail.com>
* @since 2022/2/25 at 5:18 PM
* Suffering is the most powerful teacher of life.
*/
@Getter
@AllArgsConstructor
public enum ActionType {
/**
* SQL类型动作,该动作执行SQL语句
*/
SQL_ACTION(1, "SQL类型动作"),
/**
* HTTP类型动作,该动作执行HTTP接口
*/
HTTP_ACTION(2, "HTTP类型动作"),
/**
* 用例类型动作,该动作执行另外的测试用例
*/
CASE_ACTION(3, "用例类型动作"),
/**
* 等待类动作,线程休眠指定的秒数
*/
WAIT_ACTION(4, "等待类动作");
/**
* 数据库里记录的字段使用该字段来记录
*/
@EnumValue
private final int code;
private final String des;
}
package org.matrix.util;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.*;
/**
* BeanFlattener. javaBean 转对象
*
* @author Matrix <xhyrzldf@gmail.com>
* @since 2022/3/7 at 5:15 PM
* Suffering is the most powerful teacher of life.
*/
public final class BeanFlattener {
private BeanFlattener() {}
public static Map<String, Object> deepToMap(Object bean) {
Map<String, Object> map = new LinkedHashMap<>();
try {
putValues(bean, map, null);
} catch (IllegalAccessException x) {
throw new IllegalArgumentException(x);
}
return map;
}
private static void putValues(Object bean,
Map<String, Object> map,
String prefix)
throws IllegalAccessException {
Class<?> cls = bean.getClass();
for (Field field : cls.getDeclaredFields()) {
if (field.isSynthetic() || Modifier.isStatic(field.getModifiers())) {
continue;
}
field.setAccessible(true);
Object value = field.get(bean);
String key;
if (prefix == null) {
key = field.getName();
} else {
key = prefix + "." + field.getName();
}
if (isValue(value)) {
map.put(key, value);
} else {
putValues(value, map, key);
}
}
}
private static final Set<Class<?>> VALUE_CLASSES =
Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
Object.class, String.class, Boolean.class,
Character.class, Byte.class, Short.class,
Integer.class, Long.class, Float.class,
Double.class
// etc.
)));
private static boolean isValue(Object value) {
return value == null
|| value instanceof Enum<?>
|| VALUE_CLASSES.contains(value.getClass());
}
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论