diff --git a/CHANGELOG.md b/CHANGELOG.md index f7f7f567..ff1f274f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #822: The CPF resource processor now supports system expressions and macros in CPF merge files - #578 Added functionality to record and display IPM history of install, uninstall, load, and update - #961: Adding creation of a lock file for a module by using the `-create-lockfile` flag on install. +- #1013: Implement recursive placeholder resolution in Default parameters ### Changed - #316: All parameters, except developer mode, included with a `load`, `install` or `update` command will be propagated to dependencies diff --git a/src/cls/IPM/Storage/Module.cls b/src/cls/IPM/Storage/Module.cls index ce8ba82c..6215a3a7 100644 --- a/src/cls/IPM/Storage/Module.cls +++ b/src/cls/IPM/Storage/Module.cls @@ -1775,6 +1775,7 @@ Method %Evaluate( set customParams("version") = ..VersionString set customParams("verbose") = +$get(pParams("Verbose")) set tAttrValue = ##class(%IPM.Utils.Module).%EvaluateMacro(tAttrValue) + do ##class(%IPM.Storage.ModuleSetting.Default).ResolvePlaceholders(.customParams) set tAttrValue = ##class(%IPM.Storage.ModuleSetting.Default).EvaluateAttribute(tAttrValue,.customParams) set attrValue = ##class(%IPM.Utils.Module).%EvaluateSystemExpression(tAttrValue) diff --git a/src/cls/IPM/Storage/ModuleSetting/Default.cls b/src/cls/IPM/Storage/ModuleSetting/Default.cls index 543500c3..e4544437 100644 --- a/src/cls/IPM/Storage/ModuleSetting/Default.cls +++ b/src/cls/IPM/Storage/ModuleSetting/Default.cls @@ -57,6 +57,40 @@ ClassMethod EvaluateAttribute( return attribute } +ClassMethod ResolvePlaceholders(ByRef customParams) +{ + set found = 1 + set maxLevels = 20 + + while (found && (maxLevels > 0)) { + set found = 0 + set maxLevels = maxLevels - 1 + set param = "" + + for { + set param = $order(customParams(param), 1, data) + quit:param="" + continue:data'["${" + + set varExpr = "${" _ $piece($piece(data, "${", 2), "}") _ "}" + set resolved = ##class(%IPM.Utils.Module).%EvaluateSystemExpression(varExpr) + + if resolved = varExpr { + set internalVar = $piece($piece(data, "${", 2), "}") + set resolved = $get(customParams(internalVar), varExpr) + } + + if resolved '= varExpr { + set prefix = $piece(data, "${", 1) + set suffix = $piece(data, "}", 2, *) + set customParams(param) = prefix _ resolved _ suffix + set found = 1 + } + + } + } +} + Storage Default { diff --git a/tests/integration_tests/Test/PM/Integration/Module.cls b/tests/integration_tests/Test/PM/Integration/Module.cls new file mode 100644 index 00000000..945b9336 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/Module.cls @@ -0,0 +1,88 @@ +Class Test.PM.Integration.Module Extends Test.PM.Integration.Base +{ + +Parameter CommonPathPrefix As STRING = "varresolver"; + +Parameter ModuleName As STRING = "demo-module1"; + +/// This test validates that the IPM engine can handle "chained" ${variables} +/// (placeholders that resolve to other placeholders). +/// 1. Generates a 'module.xml' from XData with deep variable dependencies. +/// 2. Executes IPM Shell 'load' to verify multi-pass expansion. +/// 3. Ensures no unresolved placeholders remain after the load process. +Method TestNestedPlaceholderVar() +{ + do $$$LogMessage(" start Loading the "_..#ModuleName_" module") + set status = ..CreateModuleXml(.moduleDir) + do $$$AssertStatusOK(status,"Created the xml file on "_moduleDir) + + set status = ##class(%IPM.Main).Shell("load "_moduleDir) + do $$$AssertStatusOK(status,"Loaded "_..#CommonPathPrefix_" module successfully from "_moduleDir) + + set module = ##class(%IPM.Storage.Module).NameOpen(..#ModuleName) + do $$$AssertTrue($isobject(module), "Module "_..#ModuleName_" exists in IPM and version is "_ module.Version.ToString()) + + do $$$LogMessage("List all modules") + set status = ##class(%IPM.Main).Shell("list") + + set status = ##class(%IPM.Main).Shell("uninstall "_..#ModuleName) + do $$$AssertStatusOK(status,"uninstalled module "_..#ModuleName_" successfully.") + + set status = ##class(%File).Delete(##class(%File).NormalizeFilename("module.xml",moduleDir)) + do $$$AssertStatusOK(status,"Deleted the module.xml file from "_moduleDir) +} + +Method CreateModuleXml(Output pModuleDir) As %Status +{ + #define NormalizeDirectory(%path) ##class(%File).NormalizeDirectory(%path) + #define UTRoot ^UnitTestRoot + + set sc = 1 + set testRoot = $$$NormalizeDirectory($get($$$UTRoot)) + set pModuleDir = $$$NormalizeDirectory(##class(%File).GetDirectory(testRoot)_"/_data/"_..#CommonPathPrefix_"/") + + if '##class(%File).DirectoryExists(pModuleDir) { + set sc = ##class(%File).CreateDirectoryChain(pModuleDir) + } + do $$$AssertStatusOK(sc,"Directory created "_pModuleDir) + set stream = ##class(%Dictionary.CompiledXData).%OpenId(..%ClassName(1)_"||TestModuleXML").Data + set fileStream = ##class(%Stream.FileBinary).%New() + set fileStream.Filename=##class(%File).NormalizeFilename("module.xml",pModuleDir) + set sc = fileStream.CopyFromAndSave(stream) + do $$$AssertStatusOK(1,"module.xml File created on "_pModuleDir) + + return sc +} + +/// Sample module file +XData TestModuleXML [ MimeType = application/xml ] +{ + + + + + demo-module1 + 1.0.0 + testing the name resolved + module + + + + + + + + + + + + + + src + Module installed successfully! + + + +} + +} diff --git a/tests/unit_tests/Test/PM/Unit/Module.cls b/tests/unit_tests/Test/PM/Unit/Module.cls index bffe7cd1..447f42e1 100644 --- a/tests/unit_tests/Test/PM/Unit/Module.cls +++ b/tests/unit_tests/Test/PM/Unit/Module.cls @@ -60,4 +60,47 @@ Method TestFixUndefinedCLIGenCommand() do $$$AssertStatusOK(sc, "AddWebApps method must now process web app list without error.") } +Method TestResolveAllVariables() +{ + //Setup Test Data + set customParams("count") = 7 + set customParams("version") = "1.0.0" + set customParams("datadefaultcspdir") = "${cspdir}" + set customParams("datadefaultmgrdir") = "${mgrdir}" + set customParams("dataversion") = "${version}" + set customParams("dataDefaultTest2") = "${datadefaultcspdir}xdata" + set customParams("dataDefaultTest3") = "${dataDefaultTest2}xtest" + set customParams("datapath") = "${libdir}data/" + set customParams("ipmdir") = "/usr/irissys/mgr/user/mts" + set customParams("datapath1") = "${ipmdir}data/" + set customParams("mTestVersion") = "${dataDefaultTest2}/xtest/${version}" + set customParams("mTestPlaceHolder") = "${dataDefaultTest2}/xtest/${version}${mgrdir}${mTestVersion}${datapath1}" + + merge customParamsIn = customParams + // output + set customParamsOut("count")=7 + set customParamsOut("dataDefaultTest2")="/usr/irissys/csp/xdata" + set customParamsOut("dataDefaultTest3")="/usr/irissys/csp/xdataxtest" + set customParamsOut("datadefaultcspdir")=##class(%IPM.Utils.Module).%EvaluateSystemExpression("${cspdir}") + set customParamsOut("datadefaultmgrdir")=##class(%IPM.Utils.Module).%EvaluateSystemExpression("${mgrdir}") + set customParamsOut("datapath")=##class(%IPM.Utils.Module).%EvaluateSystemExpression("${libdir}")_"data/" + set customParamsOut("datapath1")="/usr/irissys/mgr/user/mtsdata/" + set customParamsOut("dataversion")="1.0.0" + set customParamsOut("ipmdir")="/usr/irissys/mgr/user/mts" + set customParamsOut("mTestPlaceHolder")="/usr/irissys/csp/xdata/xtest/1.0.0/usr/irissys/mgr//usr/irissys/csp/xdata/xtest/1.0.0/usr/irissys/mgr/user/mtsdata/" + set customParamsOut("mTestVersion")="/usr/irissys/csp/xdata/xtest/1.0.0" + set customParamsOut("version")="1.0.0" + + do ##class(%IPM.Storage.ModuleSetting.Default).ResolvePlaceholders(.customParams) + + do $$$LogMessage("Validate all placholder variables") + + set variables = $listbuild("count","dataDefaultTest2","dataDefaultTest3","datadefaultcspdir","datadefaultmgrdir","datapath","datapath1","dataversion","ipmdir","mTestPlaceHolder","mTestVersion","version") + + set ptr=0 + while $listnext(variables, ptr, variable){ + do $$$AssertEquals(customParams(variable), customParamsOut(variable), variable_" is resolved correctly and the placeholder "_customParamsIn(variable)_" value is "_customParamsOut(variable)) + } +} + }