diff --git a/src/backend/ci/core/common/common-pipeline/src/main/kotlin/com/tencent/devops/common/pipeline/EnvReplacementParser.kt b/src/backend/ci/core/common/common-pipeline/src/main/kotlin/com/tencent/devops/common/pipeline/EnvReplacementParser.kt index d606e6606cdb..731df6dbefcf 100644 --- a/src/backend/ci/core/common/common-pipeline/src/main/kotlin/com/tencent/devops/common/pipeline/EnvReplacementParser.kt +++ b/src/backend/ci/core/common/common-pipeline/src/main/kotlin/com/tencent/devops/common/pipeline/EnvReplacementParser.kt @@ -66,12 +66,17 @@ object EnvReplacementParser { /** * 根据环境变量map进行object处理并保持原类型 - * 根据方言的配置判断是否能够使用${}或者变量值是否存在 + * @param value 等待进行环境变量替换的对象,可以是任意类型 + * @param contextMap 环境变量map值 + * @param contextPair 自定义表达式计算上下文(如果指定则不使用表达式替换或默认替换逻辑) + * @param onlyExpression 只进行表达式替换(若指定了自定义替换逻辑此字段无效,为false) + * @param functions 用户自定义的拓展用函数 + * @param output 表达式计算时输出 */ fun parse( value: String?, contextMap: Map, - dialect: IPipelineDialect, + onlyExpression: Boolean? = false, contextPair: Pair>? = null, functions: Iterable? = null, output: ExpressionOutput? = null @@ -79,7 +84,7 @@ object EnvReplacementParser { val context = EnvReplacementContext( value = value, contextMap = contextMap, - dialect = dialect, + useSingleCurlyBraces = !(onlyExpression ?: false), contextPair = contextPair, functions = functions, output = output @@ -89,11 +94,12 @@ object EnvReplacementParser { /** * 根据环境变量map进行object处理并保持原类型 - * 能够使用${}占位符替换,变量值不存在不抛异常 + * 根据方言的配置判断是否能够使用${}或者变量值是否存在 */ fun parse( value: String?, contextMap: Map, + dialect: IPipelineDialect, contextPair: Pair>? = null, functions: Iterable? = null, output: ExpressionOutput? = null @@ -101,6 +107,7 @@ object EnvReplacementParser { val context = EnvReplacementContext( value = value, contextMap = contextMap, + dialect = dialect, contextPair = contextPair, functions = functions, output = output @@ -109,33 +116,14 @@ object EnvReplacementParser { } fun parse(context: EnvReplacementContext): String { - with (context) { + with(context) { if (value.isNullOrBlank()) return "" - var newValue = value - if (useSingleCurlyBraces) { - newValue = ObjectReplaceEnvVarUtil.replaceEnvVar(newValue, contextMap).let { + val newValue = parseExpression() + + return if (useSingleCurlyBraces) { + ObjectReplaceEnvVarUtil.replaceEnvVar(newValue, contextMap).let { JsonUtil.toJson(it, false) } - } - return if (containsExpressions(newValue)) { - try { - val (executeContext, nameValues) = contextPair - ?: getCustomExecutionContextByMap(contextMap) - ?: return value - parseExpression( - value = newValue, - context = executeContext, - nameValues = nameValues, - functions = functions, - output = output, - contextNotNull = contextNotNull - ) - } catch (ex: VariableNotFoundException) { - throw ex - } catch (ignore: Throwable) { - logger.warn("[$value]|EnvReplacementParser expression invalid: ", ignore) - value - } } else { newValue } @@ -159,11 +147,34 @@ object EnvReplacementParser { ExpressionParser.fillContextByMap(variables, context, nameValue) return Pair(context, nameValue) } catch (ignore: Throwable) { + println("EnvReplacementParser context invalid: $variables") logger.warn("EnvReplacementParser context invalid: $variables", ignore) return null } } + private fun EnvReplacementContext.parseExpression(): String { + if (value.isNullOrBlank()) return "" + return try { + val (executeContext, nameValues) = contextPair + ?: getCustomExecutionContextByMap(contextMap) + ?: return value + parseExpression( + value = value, + context = executeContext, + nameValues = nameValues, + functions = functions, + output = output, + contextNotNull = contextNotNull + ) + } catch (ex: VariableNotFoundException) { + throw ex + } catch (ignore: Throwable) { + logger.warn("[$value]|EnvReplacementParser expression invalid: ", ignore) + value + } + } + private fun parseExpression( value: String, nameValues: List, diff --git a/src/backend/ci/core/common/common-pipeline/src/test/kotlin/com/tencent/devops/common/pipeline/EnvReplaceAndExpressionTest.kt b/src/backend/ci/core/common/common-pipeline/src/test/kotlin/com/tencent/devops/common/pipeline/EnvReplaceAndExpressionTest.kt new file mode 100644 index 000000000000..4cc0dd7ec35a --- /dev/null +++ b/src/backend/ci/core/common/common-pipeline/src/test/kotlin/com/tencent/devops/common/pipeline/EnvReplaceAndExpressionTest.kt @@ -0,0 +1,328 @@ +/* + * Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-CI 蓝鲸持续集成平台 is licensed under the MIT license. + * + * A copy of the MIT License is included in this file. + * + * + * Terms of the MIT License: + * --------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.tencent.devops.common.pipeline + +import com.tencent.devops.common.api.util.JsonUtil +import com.tencent.devops.common.api.util.JsonUtil.toJson +import com.tencent.devops.common.api.util.ObjectReplaceEnvVarUtil +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +@Suppress("ALL", "UNCHECKED_CAST") +class EnvReplaceAndExpressionTest { + + private val envMap: MutableMap = HashMap() + + @BeforeEach + fun setup() { + envMap["normalStrEnvVar"] = "123" + envMap["specStrEnvVar"] = "D:\\tmp\\hha" + envMap["jsonStrEnvVar"] = "{\"abc\":\"123\"}" + } + + private val lineSeparator = System.getProperty("line.separator") + private val jsonExcept = "{$lineSeparator" + + " \"abc\" : \"变量替换测试_{\\\"abc\\\":\\\"123\\\"}\"$lineSeparator" + + "}" + + private val arrayJsonExcept = "[ \"变量替换测试_{\\\"abc\\\":\\\"123\\\"}\" ]" + + @Test + fun replaceList() { + val testBean = TestBean( + testBeanKey = "bean变量替换测试_\${specStrEnvVar}", + testBeanValue = "{\"abc\":\"变量替换测试_\${jsonStrEnvVar}\"}" + ) + // 对list对象进行变量替换 + val originDataListObj = ArrayList() + originDataListObj.add("变量替换测试_\${normalStrEnvVar}") + originDataListObj.add("变量替换测试_\${specStrEnvVar}") + originDataListObj.add("变量替换测试_\${jsonStrEnvVar}") + originDataListObj.add("{\"abc\":\"变量替换测试_\${jsonStrEnvVar}\"}") + originDataListObj.add("[\"变量替换测试_\${jsonStrEnvVar}\"]") + originDataListObj.add(testBean) + val dataMapObj: MutableMap = HashMap() + dataMapObj["dataMapKey"] = "变量替换测试_\${specStrEnvVar}" + dataMapObj["testBean"] = testBean + originDataListObj.add(dataMapObj) + val convertDataObj = ObjectReplaceEnvVarUtil.replaceEnvVar(originDataListObj, envMap) as List<*> + val convertDataObj2 = JsonUtil.to( + EnvReplacementParser.parse(value = toJson(originDataListObj), envMap), + List::class.java + ) + + assertEquals(convertDataObj[0], convertDataObj2[0]) + assertEquals(convertDataObj[1], convertDataObj2[1]) + assertEquals(convertDataObj[2], convertDataObj2[2]) + assertEquals(convertDataObj[3], convertDataObj2[3]) + assertEquals(convertDataObj[4], convertDataObj2[4]) + + val convertTestBean = convertDataObj[5] as TestBean + assertEquals("bean变量替换测试_${envMap["specStrEnvVar"]}", convertTestBean.testBeanKey) + assertEquals(jsonExcept, convertTestBean.testBeanValue) + } + + @Test + fun replaceIllegalJson() { + val objectJson = "{\"abc:\"变量替换测试_\${normalStrEnvVar}\"" + val convertDataObj1 = ObjectReplaceEnvVarUtil.replaceEnvVar(objectJson, envMap) + println(convertDataObj1) + assertEquals("{\"abc:\"变量替换测试_${envMap["normalStrEnvVar"]}\"", convertDataObj1) + + val arrayJson = "[1, \"变量替换测试_\${normalStrEnvVar}\"" + val convertDataObj2 = ObjectReplaceEnvVarUtil.replaceEnvVar(arrayJson, envMap) + println(convertDataObj2) + assertEquals("[1, \"变量替换测试_${envMap["normalStrEnvVar"]}\"", convertDataObj2) + } + + @Test + fun replaceSet() { + + val testBean = TestBean( + testBeanKey = "bean变量替换测试_\${specStrEnvVar}", + testBeanValue = "{\"abc\":\"变量替换测试_\${jsonStrEnvVar}\"}" + ) + // 对set对象进行变量替换 + val originDataSetObj = HashSet() + originDataSetObj.add("1变量替换测试_\${normalStrEnvVar}") + originDataSetObj.add("2变量替换测试_\${specStrEnvVar}") + originDataSetObj.add("3变量替换测试_\${jsonStrEnvVar}") + originDataSetObj.add("{\"abc\":\"变量替换测试_\${jsonStrEnvVar}\"}") + originDataSetObj.add("[\"变量替换测试_\${jsonStrEnvVar}\"]") + originDataSetObj.add(testBean) + + val setDataMapObj: MutableMap = HashMap() + setDataMapObj["dataMapKey"] = "变量替换测试_\${specStrEnvVar}" + setDataMapObj["testBean"] = testBean + originDataSetObj.add(setDataMapObj) + val convertDataObj = (ObjectReplaceEnvVarUtil.replaceEnvVar(originDataSetObj, envMap) as Set<*>) + + convertDataObj.forEach { member -> + when { + member is Map<*, *> -> { + member.forEach { sm -> + when { + sm.key.toString() == "testBean" -> { + assertEquals( + "bean变量替换测试_${envMap["specStrEnvVar"]}", + (sm.value as TestBean).testBeanKey + ) + assertEquals(jsonExcept, (sm.value as TestBean).testBeanValue) + } + sm.key.toString() == "dataMapKey" -> { + assertEquals("变量替换测试_${envMap["specStrEnvVar"]}", sm.value) + } + else -> { + assertEquals(member.toString(), "setDataMapObj") + } + } + } + } + member is TestBean -> { + assertEquals("bean变量替换测试_${envMap["specStrEnvVar"]}", member.testBeanKey) + assertEquals(jsonExcept, member.testBeanValue) + } + member.toString().startsWith("1") -> { + assertEquals("1变量替换测试_${envMap["normalStrEnvVar"]}", member) + } + member.toString().startsWith("2") -> { + assertEquals("2变量替换测试_${envMap["specStrEnvVar"]}", member) + } + member.toString().startsWith("3") -> { + assertEquals("3变量替换测试_${envMap["jsonStrEnvVar"]}", member) + } + member.toString().startsWith("{") -> { + assertEquals(jsonExcept, member) + } + member.toString().startsWith("[") -> { + assertEquals(arrayJsonExcept, member) + } + else -> { + assertEquals(member.toString(), "convertDataObj") + } + } + } + } + + @Test + fun replaceMapWithTestBean() { + // 对map对象进行变量替换 + val originDataMapObj: MutableMap = HashMap() + originDataMapObj["normalStrEnvVarKey"] = "变量替换测试_\${normalStrEnvVar}" + originDataMapObj["specStrEnvVarKey"] = "变量替换测试_\${specStrEnvVar}" + originDataMapObj["jsonStrEnvVarKey1"] = "变量替换测试_\${jsonStrEnvVar}" + originDataMapObj["jsonStrEnvVarKey2"] = "{\"abc\":\"变量替换测试_\${jsonStrEnvVar}\"}" + originDataMapObj["jsonStrEnvVarKey3"] = "\${jsonStrEnvVar}" + var originSubDataMapObj: MutableMap? = HashMap() + originSubDataMapObj!!["normalStrEnvVarKey"] = "变量替换测试_\${normalStrEnvVar}" + originSubDataMapObj["specStrEnvVarKey"] = "变量替换测试_\${specStrEnvVar}" + originSubDataMapObj["jsonStrEnvVarKey1"] = "变量替换测试_\${jsonStrEnvVar}" + originSubDataMapObj["jsonStrEnvVarKey2"] = "\${jsonStrEnvVar}" + + val testBean = TestBean( + testBeanKey = "变量替换测试_\${specStrEnvVar}", + testBeanValue = "{\"abc\":\"变量替换测试_\${jsonStrEnvVar}\"}" + ) + originSubDataMapObj["testBean"] = testBean + originDataMapObj["originSubDataMapObj"] = originSubDataMapObj + val cpb = ObjectReplaceEnvVarUtil.replaceEnvVar(originDataMapObj, envMap) + val testBeanMap = ((cpb as Map)["originSubDataMapObj"] as Map)["testBean"] as TestBean + assertEquals("变量替换测试_${envMap["specStrEnvVar"]}", testBeanMap.testBeanKey) + assertEquals(jsonExcept, testBeanMap.testBeanValue) + // 判断map中jsonStrEnvVarKey3对应的值进行变量替换后能否正常转换为json串 + assertEquals(envMap["jsonStrEnvVar"], (cpb as Map)["jsonStrEnvVarKey3"]!!) + originSubDataMapObj = cpb["originSubDataMapObj"] as MutableMap? + // 判断嵌套的map中jsonStrEnvVarKey2对应的值进行变量替换后能否正常转换为json串 + assertEquals(envMap["jsonStrEnvVar"], originSubDataMapObj!!["jsonStrEnvVarKey2"]!!) + + assertEquals( + toJson(ObjectReplaceEnvVarUtil.replaceEnvVar(toJson(originDataMapObj), envMap)), + toJson(ObjectReplaceEnvVarUtil.replaceEnvVar(originDataMapObj, envMap)), + ) + } + + @Test + fun replaceTestComplexBean() { + // 对普通的javaBean对象进行转换 + val testComplexBean = TestComplexBean() + testComplexBean.testBeanKey = "变量替换测试_\${specStrEnvVar}" + testComplexBean.testBeanValue = "[\"变量替换测试_\${jsonStrEnvVar}\"]" + + val dataList = ArrayList() + dataList.add("变量替换测试_\${normalStrEnvVar}") + dataList.add("变量替换测试_\${specStrEnvVar}") + dataList.add("变量替换测试_\${jsonStrEnvVar}") + dataList.add("{\"abc\":\"变量替换测试_\${jsonStrEnvVar}\"}") + dataList.add("[\"变量替换测试_\${jsonStrEnvVar}\"]") + testComplexBean.dataList = dataList + + var dataMap: MutableMap = HashMap() + dataMap["normalStrEnvVarKey"] = " 变量替换测试_\${normalStrEnvVar} " + dataMap["specStrEnvVarKey"] = "变量替换测试_\${specStrEnvVar}" + dataMap["jsonStrEnvVarKey1"] = "变量替换测试_\${jsonStrEnvVar}" + dataMap["jsonStrEnvVarKey2"] = "{\"abc\":\"变量替换测试_\${jsonStrEnvVar}\"}" + dataMap["jsonStrEnvVarKey3"] = "[\"变量替换测试_\${jsonStrEnvVar}\"]" + val subDataMap: MutableMap = HashMap() + subDataMap["normalStrEnvVarKey"] = "变量替换测试_\${normalStrEnvVar}" + subDataMap["specStrEnvVarKey"] = "变量替换测试_\${specStrEnvVar}" + subDataMap["jsonStrEnvVarKey1"] = "变量替换测试_\${jsonStrEnvVar}" + subDataMap["jsonStrEnvVarKey2"] = "{\"abc\":\"变量替换测试_\${jsonStrEnvVar}\"}" + + val testBean = TestBean( + testBeanKey = "bean变量替换测试_\${specStrEnvVar}", + testBeanValue = "{\"abc\":\"bean变量替换测试_\${jsonStrEnvVar}\"}" + ) + subDataMap["testBean"] = testBean + dataMap["subDataMap"] = subDataMap + testComplexBean.dataMap = dataMap + + val dataSet = HashSet() + dataSet.add("变量替换测试_\${normalStrEnvVar}") + dataSet.add("变量替换测试_\${specStrEnvVar}") + dataSet.add("变量替换测试_\${jsonStrEnvVar}") + dataSet.add("{\"abc\":\"变量替换测试_\${jsonStrEnvVar}\"}") + dataSet.add("[\"变量替换测试_\${jsonStrEnvVar}\"]") + testComplexBean.dataSet = dataSet + + // start to test + var convertDataObj = ObjectReplaceEnvVarUtil.replaceEnvVar(testComplexBean, envMap) + val convertBean = convertDataObj as TestComplexBean + assertEquals("变量替换测试_${envMap["specStrEnvVar"]}", convertBean.testBeanKey) + + assertEquals("变量替换测试_${envMap["normalStrEnvVar"]}", convertBean.dataList!![0]) + assertEquals("变量替换测试_${envMap["specStrEnvVar"]}", convertBean.dataList!![1]) + assertEquals("变量替换测试_${envMap["jsonStrEnvVar"]}", convertBean.dataList!![2]) + assertEquals(jsonExcept, convertBean.dataList!![3]) + assertEquals("[ \"变量替换测试_{\\\"abc\\\":\\\"123\\\"}\" ]", convertBean.dataList!![4]) + + assertEquals(" 变量替换测试_${envMap["normalStrEnvVar"]} ", convertBean.dataMap!!["normalStrEnvVarKey"]) + assertEquals("变量替换测试_${envMap["specStrEnvVar"]}", convertBean.dataMap!!["specStrEnvVarKey"]) + assertEquals("变量替换测试_${envMap["jsonStrEnvVar"]}", convertBean.dataMap!!["jsonStrEnvVarKey1"]) + assertEquals(jsonExcept, convertBean.dataMap!!["jsonStrEnvVarKey2"]) + assertEquals(arrayJsonExcept, convertBean.dataMap!!["jsonStrEnvVarKey3"]) + + // 替换包含null的对象 + dataMap = HashMap() + dataMap["key1"] = "变量" + dataMap["key2"] = arrayOf(null, "哈哈") + + convertDataObj = ObjectReplaceEnvVarUtil.replaceEnvVar(dataMap, envMap) as Map<*, *> + assertEquals(dataMap["key1"], convertDataObj["key1"]) + assertEquals(toJson(dataMap["key2"]!!), convertDataObj["key2"]) + println("convertDataObj=$convertDataObj") + } + + @Test + fun replaceEnvVar() { + + // 对普通字符串进行普通字符串变量替换 + var originDataObj: Any = "变量替换测试_\${normalStrEnvVar}" + var convertDataObj = ObjectReplaceEnvVarUtil.replaceEnvVar(originDataObj, envMap) + assertEquals("变量替换测试_123", toJson(convertDataObj)) + + // 对普通字符串进行带特殊字符字符串变量替换 + originDataObj = "变量替换测试_\${specStrEnvVar}" + convertDataObj = ObjectReplaceEnvVarUtil.replaceEnvVar(originDataObj, envMap) + assertEquals("变量替换测试_D:\\tmp\\hha", toJson(convertDataObj)) + + // 对普通字符串进行json字符串变量替换 + originDataObj = "变量替换测试_\${jsonStrEnvVar}" + convertDataObj = ObjectReplaceEnvVarUtil.replaceEnvVar(originDataObj, envMap) + assertEquals("变量替换测试_{\"abc\":\"123\"}", toJson(convertDataObj)) + + // number类型变量替换 + originDataObj = "[1,2,3]" + convertDataObj = ObjectReplaceEnvVarUtil.replaceEnvVar(originDataObj, envMap) + println(toJson(convertDataObj)) + assertEquals(toJson(JsonUtil.to(originDataObj, List::class.java)), toJson(convertDataObj)) + + // 魔法数字符创测试 + convertDataObj = ObjectReplaceEnvVarUtil.replaceEnvVar("12E2", envMap) + assertEquals("12E2", toJson(convertDataObj)) + // 替换”[133]-[sid-${normalStrEnvVar}]-[sid-zhiliang-test1]“带多个[]的字符串 + convertDataObj = ObjectReplaceEnvVarUtil.replaceEnvVar( + "[133]-[sid-\${normalStrEnvVar}]-[sid-zhiliang-test1]", + envMap + ) + assertEquals("[133]-[sid-123]-[sid-zhiliang-test1]", toJson(convertDataObj)) + } + + internal data class TestBean( + var testBeanKey: String? = null, + var testBeanValue: String? = null + ) + + internal data class TestComplexBean( + var testBeanKey: String? = null, + var testBeanValue: String? = null, + var dataList: List<*>? = null, + var dataMap: Map<*, *>? = null, + var dataSet: Set<*>? = null + ) +} diff --git a/src/backend/ci/core/common/common-pipeline/src/test/kotlin/com/tencent/devops/common/pipeline/EnvReplacementParserTest.kt b/src/backend/ci/core/common/common-pipeline/src/test/kotlin/com/tencent/devops/common/pipeline/EnvReplacementParserTest.kt index c739fc4e475f..8590aabd0609 100644 --- a/src/backend/ci/core/common/common-pipeline/src/test/kotlin/com/tencent/devops/common/pipeline/EnvReplacementParserTest.kt +++ b/src/backend/ci/core/common/common-pipeline/src/test/kotlin/com/tencent/devops/common/pipeline/EnvReplacementParserTest.kt @@ -265,7 +265,8 @@ internal class EnvReplacementParserTest { parseAndEquals( data = data, template = "abcd_\${{center中}}_ffs", - expect = "abcd_\${{center中}}_ffs" + expect = "abcd_\${{center中}}_ffs", + onlyExpression = true ) data["blank"] = "" @@ -368,11 +369,11 @@ internal class EnvReplacementParserTest { ) Assertions.assertEquals( command6, - EnvReplacementParser.parse(command6, data) + EnvReplacementParser.parse(command6, data, true) ) Assertions.assertEquals( "hello\$variables.abc}}", - EnvReplacementParser.parse(command7, data) + EnvReplacementParser.parse(command7, data, true), ) Assertions.assertEquals( "echo hahahahaha", @@ -427,7 +428,7 @@ internal class EnvReplacementParserTest { Assertions.assertEquals("hellovariables.value", EnvReplacementParser.parse(command3, data)) Assertions.assertEquals("hello\${{variables.abc", EnvReplacementParser.parse(command4, data)) Assertions.assertEquals("hello\${{variables.abc}", EnvReplacementParser.parse(command5, data)) - Assertions.assertEquals("hello\${variables.abc}}", EnvReplacementParser.parse(command6, data)) + Assertions.assertEquals("hello\${variables.abc}}", EnvReplacementParser.parse(command6, data, true)) Assertions.assertEquals("hello\$variables.abc}}", EnvReplacementParser.parse(command7, data)) Assertions.assertEquals("echo hahahahaha", EnvReplacementParser.parse(command8, data)) Assertions.assertEquals( @@ -551,9 +552,10 @@ echo true""" data: Map, template: String, expect: String, - contextMap: Map = emptyMap() + contextMap: Map = emptyMap(), + onlyExpression: Boolean? = false ) { - val buff = EnvReplacementParser.parse(template, contextMap.plus(data)) + val buff = EnvReplacementParser.parse(template, contextMap.plus(data), onlyExpression) println("template=$template\nreplaced=$buff\n") Assertions.assertEquals(expect, buff) }