From 26c1f4743835d17951524a1b9c40fa356f557ec4 Mon Sep 17 00:00:00 2001 From: James Lechtner Date: Fri, 30 Jan 2026 16:26:14 -0500 Subject: [PATCH 1/8] Adding functionality to install from a lock file --- src/cls/IPM/General/AbstractHistory.cls | 9 +- src/cls/IPM/General/LockFile.cls | 52 +++++- src/cls/IPM/Lifecycle/Base.cls | 1 + src/cls/IPM/Main.cls | 155 ++++++++++++++++- src/cls/IPM/Repo/Definition.cls | 10 ++ src/cls/IPM/Repo/Filesystem/Definition.cls | 12 +- src/cls/IPM/Repo/Oras/Definition.cls | 15 +- src/cls/IPM/Repo/Remote/Definition.cls | 15 +- src/cls/IPM/Utils/Module.cls | 12 +- .../Test/PM/Integration/LockFile.cls | 160 ++++++++++++++---- .../expected-files/lock-mod-oras.json | 12 +- .../lock-mod-g-all-repo-types/module.xml | 4 - .../lock-mod-oras/module-lock.json | 52 ++++++ .../lock-mod-remote/module-lock.json | 33 ++++ .../lock-mod-remote/module.xml | 6 + 15 files changed, 496 insertions(+), 52 deletions(-) create mode 100644 tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-other-repos/lock-mod-oras/module-lock.json create mode 100644 tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-other-repos/lock-mod-remote/module-lock.json diff --git a/src/cls/IPM/General/AbstractHistory.cls b/src/cls/IPM/General/AbstractHistory.cls index 10dd29641..e54a4545b 100644 --- a/src/cls/IPM/General/AbstractHistory.cls +++ b/src/cls/IPM/General/AbstractHistory.cls @@ -8,8 +8,8 @@ Include (%IPM.Common, %IPM.Formatting) Class %IPM.General.AbstractHistory Extends %Persistent [ Abstract, NoExtent ] { -/// Action of this history record. Can be load, install, uninstall or update -Property Action As %String(VALUELIST = ",load,install,uninstall,update") [ Required ]; +/// Action of this history record. Can be load, install, ci, uninstall or update +Property Action As %String(VALUELIST = ",load,install,ci,uninstall,update") [ Required ]; /// Name of the package being logged. This is not necessarily required, e.g. when loading a nonexistent directory. Property Package As %IPM.DataType.ModuleName; @@ -66,6 +66,11 @@ ClassMethod InstallInit(Package As %IPM.DataType.ModuleName) As %IPM.General.Abs quit ..Init("install", Package) } +ClassMethod CleanInstallInit(Package As %IPM.DataType.ModuleName) As %IPM.General.AbstractHistory +{ + quit ..Init("ci", Package) +} + ClassMethod LoadInit(Package As %IPM.DataType.ModuleName = "") As %IPM.General.AbstractHistory { // Package name may not known at this point, so use a placeholder diff --git a/src/cls/IPM/General/LockFile.cls b/src/cls/IPM/General/LockFile.cls index 4d0cac0cb..4d2b3bb13 100644 --- a/src/cls/IPM/General/LockFile.cls +++ b/src/cls/IPM/General/LockFile.cls @@ -69,12 +69,12 @@ ClassMethod CreateLockFileForModule( $$$ThrowOnError(lockFile.Dependencies.SetAt(dependencyVal, mod.Name)) // Add the dependency's repository to the lock file - do AddRepositoryToLockFile(.lockFile, mod.Repository, verbose) + do ..AddRepositoryToLockFile(.lockFile, mod.Repository, verbose) } // Add repository for base module if not already added by a dependency // Skip undefined repositories as that means the module was installed via the zpm "load" command if (module.Repository '= "") { - do AddRepositoryToLockFile(.lockFile, module.Repository, verbose) + do ..AddRepositoryToLockFile(.lockFile, module.Repository, verbose) } $$$ThrowOnError(lockFile.%JSONExportToStream(.lockFileJSON, "LockFileMapping")) @@ -116,4 +116,52 @@ ClassMethod GetRepo(repoName As %String) As %IPM.Repo.Definition [ Internal ] } } +ClassMethod InstallFromLockFile( + directory As %String, + ByRef params) +{ + set verbose = $get(params("Verbose"), 0) + + set lockFilePath = ##class(%File).NormalizeFilename("module-lock.json", directory) + set lockFileJSON = ##class(%DynamicObject).%FromJSONFile(lockFilePath) + + set repositories = lockFileJSON.%Get("repositories", {}) + set dependencies = lockFileJSON.%Get("dependencies", {}) + + // Install repositories (if they don't already exist) + set repoIter = repositories.%GetIterator() + while repoIter.%GetNext(.repoName, .repoVals) { + if ##class(%IPM.Repo.Definition).ServerDefinitionKeyExists(repoName) { + if (verbose) { + write !, "Repo: "_repoName_" already exists, skipping creating new one from lock file" + } + continue + } + elseif (verbose) { + write !, "Creating repo: "_repoName_" from lock file" + } + do ##class(%IPM.Repo.Definition).CollectServerTypes(.types) + set repoClass = types(repoVals.type) + set repoVals.name = repoName + do $classmethod(repoClass, "LockFileValuesToModifiers", repoVals, .modifiers) + $$$ThrowOnError($classmethod(repoClass,"Configure",0,.modifiers,.tData,repoClass)) + } + + // Install modules (even if they already exist) + set depIter = dependencies.%GetIterator() + while depIter.%GetNext(.depName, .depVals) { + //break + set version = depVals.version + set repository = depVals.repository + + // Call CleanInstall() on dependency modules but set flag "LockFileInstallStarted" so we don't try installing from the dependency module's lock file + set commandInfo = "ci" + set commandInfo("data", "Verbose") = verbose + set commandInfo("parameters","module") = repository_"/"_depName + set commandInfo("parameters", "version") = version + set commandInfo("data", "LockFileInstallStarted") = 1 + do ##class(%IPM.Main).CleanInstall(.commandInfo) + } +} + } diff --git a/src/cls/IPM/Lifecycle/Base.cls b/src/cls/IPM/Lifecycle/Base.cls index 673c098cc..c07403409 100644 --- a/src/cls/IPM/Lifecycle/Base.cls +++ b/src/cls/IPM/Lifecycle/Base.cls @@ -1221,6 +1221,7 @@ Method %Export( "changelog.md", "license", "requirements.txt", + "module-lock.json", ) set tRes = ##class(%File).FileSetFunc(..Module.Root) while tRes.%Next() { diff --git a/src/cls/IPM/Main.cls b/src/cls/IPM/Main.cls index 2b62aa8ff..81f464734 100644 --- a/src/cls/IPM/Main.cls +++ b/src/cls/IPM/Main.cls @@ -790,6 +790,25 @@ generate /my/path -export 00000,PacketName2,IgnorePacket2^00000,PacketName3,Igno history details 3 -phases + +Installs a module from a lock file + + Installs a module from its lock file. Will first install all listed repositories followed by dependency modules and then the base module. + + + + + ci mymodule 3.0.0 + + + + + + + + + + } @@ -1030,8 +1049,10 @@ ClassMethod ShellInternal( do ..ModuleVersion(.tCommandInfo) } elseif (tCommandInfo = "information") { do ..Information(.tCommandInfo) - } elseif (tCommandInfo = "history") { - do ..History(.tCommandInfo) + } elseif (tCommandInfo = "history") { + do ..History(.tCommandInfo) + } elseif (tCommandInfo = "ci") { + do ..CleanInstall(.tCommandInfo) } } catch pException { if (pException.Code = $$$ERCTRLC) { @@ -2395,12 +2416,11 @@ ClassMethod Install( $$$ThrowStatus($$$ERROR($$$GeneralError, "Deployed package '" _ tModuleName _ "' " _ tResult.VersionString _ " not supported on this platform " _ platformVersion _ ".")) } } - $$$ThrowOnError(log.SetSource(tResult.ServerName)) - $$$ThrowOnError(log.SetVersion(tResult.Version)) + $$$ThrowOnError(log.SetSource(tResult.ServerName)) + $$$ThrowOnError(log.SetVersion(tResult.Version)) $$$ThrowOnError(##class(%IPM.Utils.Module).LoadQualifiedReference(tResult, .tParams, , log)) } } else { - set tPrefix = "" if (tModuleName '= "") { if (tVersion '= "") { $$$ThrowStatus($$$ERROR($$$GeneralError, tModuleName_" "_tVersion_" not found in any repository.")) @@ -2414,10 +2434,131 @@ ClassMethod Install( } } } catch ex { - $$$ThrowOnError(log.Finalize(ex.AsStatus(), devMode)) + $$$ThrowOnError(log.Finalize(ex.AsStatus(), devMode)) + throw ex + } + $$$ThrowOnError(log.Finalize($$$OK, devMode)) +} + +ClassMethod CleanInstall(ByRef commandInfo) [ Internal ] +{ + set moduleName = $get(commandInfo("parameters","module")) + set version = $get(commandInfo("parameters","version")) + set verbose = $get(commandInfo("data","Verbose")) + set log = ##class(%IPM.General.HistoryTemp).CleanInstallInit(moduleName) + + // TODO: Add "path"? (see Update() for more info of calling install v load) + + if verbose { + write !, "Going to run a clean install on "_moduleName + } + + // Indicating to commandInfo that this is a clean install command, not an install or load command. commandInfo will be passed to either Install() or Load() to continue performing the update. + set commandInfo("data","cmd") = "ci" + set commandInfo("data","CleanInstall") = 1 + set log = ##class(%IPM.General.HistoryTemp).UpdateInit(moduleName) + + // Forward execution to install + do ..Install(.commandInfo, log) + + + + /* + zwrite commandInfo + return + set registry = "" + set moduleName = $get(commandInfo("parameters","module")) + if (moduleName["/") { + set $listbuild(registry, moduleName) = $listfromstring(moduleName, "/") + } + + if pLog = "" { + set log = ##class(%IPM.General.HistoryTemp).CleanInstallInit(moduleName) + } else { + set log = pLog + } + + try { + if (moduleName = "") { + $$$ThrowStatus($$$ERROR($$$GeneralError, "No module name specified.")) + } + + if moduleName = $$$IPMModuleName { + $$$ThrowOnError(..CheckModuleNamespace()) + } + // check if any registries are configured and enabled + new SQLCODE, count + &sql(SELECT COUNT(*) into :count FROM %IPM_Repo.Definition WHERE Enabled = 1) + $$$ThrowSQLIfError(SQLCODE,%message) + if (SQLCODE = 0) && (count = 0) { + $$$ThrowStatus($$$ERROR($$$GeneralError, "No repositories are configured and enabled in this namespace.")) + } + + set version = $get(commandInfo("parameters","version")) + + set searchCriteria = ##class(%IPM.Repo.SearchCriteria).%New() + set searchCriteria.Registry = registry + set searchCriteria.Name = $$$lcase(moduleName) + set searchCriteria.VersionExpression = version + $$$ThrowOnError(##class(%IPM.Repo.Utils).SearchRepositoriesForModule(searchCriteria,.results)) + + if (results.Count() > 0) { + set result = "" + #dim result As %IPM.Storage.QualifiedModuleInfo + // Results are ordered by semantic version, descending. (So the "latest" version will always be first.) + if ('$$$HasModifier(commandInfo,"prompt") || (results.Count() = 1)) { // && (tKeywords = "") { + set result = results.GetAt(1) + } elseif (results.Count() > 0) { + for i=1:1:results.Count() { + set tResultInfo = results.GetAt(i) + set tOptArray(i) = tResultInfo.DisplayName_" "_tResultInfo.VersionString_" @ "_tResultInfo.ServerName + } + + set tValue = "" + set tResponse = ##class(%Library.Prompt).GetMenu("Which version?",.tValue,.tOptArray,,$$$InitialDisplayMask+$$$EnableQuitCharMask) + if (tResponse '= $$$SuccessResponse) { + $$$ThrowStatus($$$ERROR($$$GeneralError,"Operation cancelled.")) + } + if (tValue '= "") { + set result = results.GetAt(tValue) + } + } + + if (result '= "") { + do ##class(%IPM.Lifecycle.Base).GetDefaultParameters(.params) + merge params = commandInfo("data") + set params("cmd") = "ci" + set params("CleanInstall") = 1 + set params("Verbose") = 1 // TODO: Replace this with actual new parameter instead of shoe-horning this in for logging at other parts of the loading + if result.Deployed { + set platformVersion = $system.Version.GetMajor() _ "." _$system.Version.GetMinor() + set result.PlatformVersion = platformVersion + if ('result.PlatformVersions.Find(platformVersion)) { + $$$ThrowStatus($$$ERROR($$$GeneralError, "Deployed package '"_moduleName_"' "_result.VersionString_" not supported on this platform "_platformVersion_".")) + } + } + $$$ThrowOnError(log.SetSource(result.ServerName)) + $$$ThrowOnError(log.SetVersion(result.Version)) + $$$ThrowOnError(##class(%IPM.Utils.Module).LoadQualifiedReference(result, .params, , log)) + } + } else { + set tPrefix = "" + if (moduleName '= "") { + if (version '= "") { + $$$ThrowStatus($$$ERROR($$$GeneralError, moduleName_" "_version_" not found in any repository.")) + } else { + $$$ThrowStatus($$$ERROR($$$GeneralError, "'"_moduleName_"' not found in any repository.")) + } + } else { + write !,"No modules found. Are there any repositories configured in the current namespace?" + } + } + } catch ex { + $$$ThrowOnError(log.Finalize(ex.AsStatus())) throw ex } - $$$ThrowOnError(log.Finalize($$$OK, devMode)) + $$$ThrowOnError(log.Finalize($$$OK)) + */ } ClassMethod Reinstall(ByRef pCommandInfo) [ Internal ] diff --git a/src/cls/IPM/Repo/Definition.cls b/src/cls/IPM/Repo/Definition.cls index b36b3b504..bdc2f9aa9 100644 --- a/src/cls/IPM/Repo/Definition.cls +++ b/src/cls/IPM/Repo/Definition.cls @@ -288,6 +288,16 @@ Method LockFileTypeGet() return ..#MONIKER } +/// We use the "LockFileMapping" XData block to export a repo definition to a lock file. +/// When importing from a lock file, doing an import and save using the same XData block won't work. +/// Instead, we must create a set of modifiers and call %IPM.Repo.Definition::Configure() +/// This method accepts the JSON repository values from a lock file and populates a modifers object to then call Configure() with +ClassMethod LockFileValuesToModifiers( + lockFileValues As %DynamicObject, + Output modifiers) [ Abstract ] +{ +} + Storage Default { diff --git a/src/cls/IPM/Repo/Filesystem/Definition.cls b/src/cls/IPM/Repo/Filesystem/Definition.cls index 582b32405..788b0a9cf 100644 --- a/src/cls/IPM/Repo/Filesystem/Definition.cls +++ b/src/cls/IPM/Repo/Filesystem/Definition.cls @@ -357,11 +357,21 @@ ClassMethod ScanDirectory( quit tSC } +ClassMethod LockFileValuesToModifiers( + lockFileValues As %DynamicObject, + Output modifiers) +{ + set modifiers("filesystem") = "" + set modifiers("name") = lockFileValues.%Get("name") + set modifiers("path") = lockFileValues.%Get("root") + set modifiers("depth") = lockFileValues.%Get("depth") + set modifiers("read-only") = lockFileValues.%Get("readOnly") +} + XData LockFileMapping { - diff --git a/src/cls/IPM/Repo/Oras/Definition.cls b/src/cls/IPM/Repo/Oras/Definition.cls index 629e15831..59798ffe1 100644 --- a/src/cls/IPM/Repo/Oras/Definition.cls +++ b/src/cls/IPM/Repo/Oras/Definition.cls @@ -140,11 +140,24 @@ Method GetPublishingManager(ByRef status) return ##class(%IPM.Repo.Oras.PublishManager).%Get(.status) } +ClassMethod LockFileValuesToModifiers( + lockFileValues As %DynamicObject, + Output modifiers) +{ + set modifiers("oras") = "" + set modifiers("name") = lockFileValues.%Get("name") + set modifiers("read-only") = lockFileValues.%Get("readOnly") + set modifiers("url") = lockFileValues.%Get("url") + // TODO: Implement calling system variables to get username/password/token values + set modifiers("username") = "" + set modifiers("password") = "" + set modifiers("token") = "" +} + XData LockFileMapping { - diff --git a/src/cls/IPM/Repo/Remote/Definition.cls b/src/cls/IPM/Repo/Remote/Definition.cls index d259b39a0..335b5340a 100644 --- a/src/cls/IPM/Repo/Remote/Definition.cls +++ b/src/cls/IPM/Repo/Remote/Definition.cls @@ -132,6 +132,20 @@ Method GetPublishingManager(ByRef status) return ##class(%IPM.Repo.Remote.PublishManager).%Get(.status) } +ClassMethod LockFileValuesToModifiers( + lockFileValues As %DynamicObject, + Output modifiers) +{ + set modifiers("remote") = "" + set modifiers("name") = lockFileValues.%Get("name") + set modifiers("url") = lockFileValues.%Get("url") + set modifiers("read-only") = lockFileValues.%Get("readOnly") + // TODO: Implement calling system variables to get username/password/token values + set modifiers("username") = "" + set modifiers("password") = "" + set modifiers("token") = "" +} + Method LockFileTypeGet() { return ..#MONIKERALIAS @@ -141,7 +155,6 @@ XData LockFileMapping { - diff --git a/src/cls/IPM/Utils/Module.cls b/src/cls/IPM/Utils/Module.cls index d19b92efe..d5e6221c4 100644 --- a/src/cls/IPM/Utils/Module.cls +++ b/src/cls/IPM/Utils/Module.cls @@ -271,6 +271,9 @@ ClassMethod LoadModuleFromDirectory( { set tSC = $$$OK try { + if $get(pParams("CleanInstall"), 0) && '$get(pParams("LockFileInstallStarted"), 0) { + do ##class(%IPM.General.LockFile).InstallFromLockFile(pDirectory, .pParams) + } set tVerbose = $get(pParams("Verbose")) // LoadNewModule goes all the way through Reload->Validate->Compile->Activate, also compiling the new module. write:tVerbose !,"Loading from ",pDirectory,! @@ -1205,7 +1208,12 @@ ClassMethod LoadNewModule( if $get(params("CreateLockFile"), 0) && '$data(params("LockFileModule")){ set params("LockFileModule") = tModule.Name } - do ..LoadDependencies(tModule, .params) + + // If installing from a lock file, don't need to load dependencies since dependencies will be installed in order anyways + if ('$get(params("CleanInstall"), 0)) { + do ..LoadDependencies(tModule, .params) + } + set tSC = $system.OBJ.Load(pDirectory_"module.xml",$select(tVerbose:"d",1:"-d"),,.tLoadedList) $$$ThrowOnError(tSC) @@ -1281,7 +1289,7 @@ ClassMethod LoadDependencies( // Create lock file if specified for this module if $get(pParams("CreateLockFile"), 0) && (pModule.Name = $get(pParams("LockFileModule"))) { try { - do ##class(%IPM.General.LockFile).CreateLockFileForModule(pModule, flatDepList, .tParams) + do ##class(%IPM.General.LockFile).CreateLockFileForModule(pModule, flatDepList, .pParams) } catch (ex) { write !, $$$FormatText("Error creating lock file for %1 - %2", pModule.Name, ex.DisplayString()), ! } diff --git a/tests/integration_tests/Test/PM/Integration/LockFile.cls b/tests/integration_tests/Test/PM/Integration/LockFile.cls index 15d96fa99..8344fd8fe 100644 --- a/tests/integration_tests/Test/PM/Integration/LockFile.cls +++ b/tests/integration_tests/Test/PM/Integration/LockFile.cls @@ -85,10 +85,9 @@ Method OnAfterOneTest() As %Status Method Test01Module0Dependencies() { try { - set moduleName = ..#ModuleA - do ..AssertInstallCreatesLockFileAsExpected(moduleName) + do ..ExecuteGenericLockFileTest(..#ModuleA) } catch e { - do $$$AssertStatusOK(e.AsStatus(),"An exception occurred in Module0Dependencies.") + do $$$AssertStatusOK(e.AsStatus(),"An exception occurred in Test01Module0Dependencies.") } } @@ -97,10 +96,9 @@ Method Test01Module0Dependencies() Method Test02Module2Dependencies0Transient() { try { - set moduleName = ..#ModuleC - do ..AssertInstallCreatesLockFileAsExpected(moduleName) + do ..ExecuteGenericLockFileTest(..#ModuleC) } catch e { - do $$$AssertStatusOK(e.AsStatus(),"An exception occurred in Module2Dependencies0Transient.") + do $$$AssertStatusOK(e.AsStatus(),"An exception occurred in Test02Module2Dependencies0Transient.") } } @@ -109,10 +107,9 @@ Method Test02Module2Dependencies0Transient() Method Test03Module1Dependency2Transient() { try { - set moduleName = ..#ModuleD - do ..AssertInstallCreatesLockFileAsExpected(moduleName) + do ..ExecuteGenericLockFileTest(..#ModuleD) } catch e { - do $$$AssertStatusOK(e.AsStatus(),"An exception occurred in Module1Dependency2Transient.") + do $$$AssertStatusOK(e.AsStatus(),"An exception occurred in Test03Module1Dependency2Transient.") } } @@ -121,16 +118,16 @@ Method Test03Module1Dependency2Transient() Method Test04Module2Dependencies1Transient() { try { - set moduleName = ..#ModuleF - do ..AssertInstallCreatesLockFileAsExpected(moduleName) + do ..ExecuteGenericLockFileTest(..#ModuleF) } catch e { - do $$$AssertStatusOK(e.AsStatus(),"An exception occurred in Module2Dependencies1Transient.") + do $$$AssertStatusOK(e.AsStatus(),"An exception occurred in Test04Module2Dependencies1Transient.") } } /// Edge cases below: /// All repository types /// Uses Module G +/// TODO: Update lock-mod-oras: Created lock file with mods-other-repos filesystem repo instead of oras + remote repo Method Test05ModuleDependenciesAllRepositoryTypes() { set sc = $$$OK @@ -149,7 +146,7 @@ Method Test05ModuleDependenciesAllRepositoryTypes() set sc = ##class(%IPM.Main).Shell("publish "_remoteMod_" -r "_remoteRepo_" -v -export-deps 1") do $$$AssertStatusOK(sc, "Successfully published module to remote repo") // Uninstall mod and to then be installed from remote repo - do ##class(%IPM.Main).Shell("uninstall "_remoteMod) + do ##class(%IPM.Main).Shell("uninstall -r "_remoteMod) // Publish a module to an ORAS repository set orasRepo = "lock-file-oras" @@ -174,12 +171,42 @@ Method Test05ModuleDependenciesAllRepositoryTypes() // Go back to the initial state by uninstalling the module, dependencies, and repositories used for this test do ##class(%IPM.Main).Shell("uninstall -r "_moduleName) do $$$AssertStatusOK(sc, "Uninstalled "_moduleName_" and dependencies at the end of the test") + + + // Now test installing from a lock file from a module in a remote repository + set moduleName = "lock-mod-remote" + + // TODO: Replace this with zpm "ci" once implemented + set sc = ##class(%IPM.Main).Shell("install "_remoteRepo_"/"_moduleName) + do $$$AssertStatusOK(sc, "Installed "_moduleName_" via the laock file successfully") + + // Confirm that module and dependency classes were loaded correctly + do $classmethod("LockModRemote.Class1", "MethodA") + + + // Now test installing from a lock file from a module in an oras repository + set moduleName = "lock-mod-oras" + + // TODO: Replace this with zpm "ci" once implemented + set sc = ##class(%IPM.Main).Shell("install "_orasRepo_"/"_moduleName) + do $$$AssertStatusOK(sc, "Installed "_moduleName_" via the laock file successfully") + + // Confirm that module and dependency classes were loaded correctly + do $classmethod("LockModORAS.Class1", "MethodA") + + + + + + + + do ##class(%IPM.Main).Shell("repo -delete -name "_remoteRepo) do $$$AssertStatusOK(sc, "Removed "_remoteRepo_" at the end of the test") do ##class(%IPM.Main).Shell("repo -delete -name "_orasRepo) do $$$AssertStatusOK(sc, "Removed "_orasRepo_" at the end of the test") - } catch (ex) { - set sc = ex.AsStatus() + } catch e { + do $$$AssertStatusOK(e.AsStatus(),"An exception occurred in Test05ModuleDependenciesAllRepositoryTypes.") } } @@ -204,8 +231,20 @@ Method Test06ModuleMultipleVersions() set areLockFilesEqual = ..AreLockFilesEqual(lockFilePath, ..#ExpectedFilesDir_latestLockFileName) do $$$AssertTrue(areLockFilesEqual, "Lock file contents for "_moduleName_" v"_latestVersion_" match expected values") - // Go back to the initial state by uninstalling the module and dependencies at the end of the test - do ##class(%IPM.Main).Shell("uninstall -r "_moduleName) + // Uninstall module to prepare for install from lock file + set sc = ##class(%IPM.Main).Shell("uninstall -r "_moduleName) + do $$$AssertStatusOK(sc, "Uninstalled "_moduleName) + + // Install module from lock file + // TODO: Replace this with zpm "ci" once implemented + set sc = ##class(%IPM.Main).Shell("install "_moduleName) + + // Confirm classes and dependencies get installed as well + do $classmethod("LockModH.Class1", "MethodA") + + // Uninstall module before testing with other version + set sc = ##class(%IPM.Main).Shell("uninstall -r "_moduleName) + do $$$AssertStatusOK(sc, "Uninstalled "_moduleName) @@ -224,9 +263,17 @@ Method Test06ModuleMultipleVersions() do $$$AssertTrue(areLockFilesEqual, "Lock file contents for "_moduleName_" v"_olderVersion_" match expected values") // Go back to the initial state by uninstalling the module and dependencies at the end of the test - do ##class(%IPM.Main).Shell("uninstall -r "_moduleName) + set sc = ##class(%IPM.Main).Shell("uninstall -r "_moduleName) + do $$$AssertStatusOK(sc, "Uninstalled "_moduleName) + + // Install module from lock file + // TODO: Replace this with zpm "ci" once implemented + set sc = ##class(%IPM.Main).Shell("install "_moduleName) + + // Confirm classes and dependencies get installed as well + do $classmethod("LockModH.Class1", "MethodA") } catch e { - do $$$AssertStatusOK(e.AsStatus(),"An exception occurred in Module2Dependencies1Transient.") + do $$$AssertStatusOK(e.AsStatus(),"An exception occurred in Test06ModuleMultipleVersions.") } } @@ -240,6 +287,8 @@ Method Test07ModuleNoLockFile() { try { set moduleName = ..#ModuleI + + // Install and create lock file for module do ..AssertInstallCreatesLockFileAsExpected(moduleName) // Go back to the initial state by deleting lock file for this module at the end of the test @@ -248,8 +297,16 @@ Method Test07ModuleNoLockFile() if '##class(%File).Delete(lockFilePath) { $$$ThrowStatus($$$ERROR($$$GeneralError,$$$FormatText("Failed to delete lock file located at: %1", lockFilePath))) } + + // Uninstall module to prepare to install from lock file + set sc = ##class(%IPM.Main).Shell("uninstall -r "_moduleName) + do $$$AssertStatusOK(sc, "Uninstalled "_moduleName) + + // TODO: This should fail due to no lock file + //Install module from lock file + //do ..AssertInstallFromLockFileAsExpected(moduleName) } catch e { - do $$$AssertStatusOK(e.AsStatus(),"An exception occurred in Module2Dependencies1Transient.") + do $$$AssertStatusOK(e.AsStatus(),"An exception occurred in Test07ModuleNoLockFile.") } } @@ -257,7 +314,11 @@ Method Test07ModuleNoLockFile() /// Uses Module J Method Test08ModuleDependenciesOutOfOrder() { - // TODO: Implement with zpm "ci" addition + set moduleName = ..#ModuleJ + + // TODO: Replace this with zpm "ci" once implemented + //set sc = ##class(%IPM.Main).Shell("install "_moduleName) + //do $$$AssertStatusNotOK(sc, "Installing from lock file with dependencies listed out of order fails") } /// 2 dependencies for a module have the same transient to add to the lock file @@ -265,10 +326,9 @@ Method Test08ModuleDependenciesOutOfOrder() Method Test09Module2DependenciesSameTransient() { try { - set moduleName = ..#ModuleK - do ..AssertInstallCreatesLockFileAsExpected(moduleName) + do ..ExecuteGenericLockFileTest(..#ModuleK) } catch e { - do $$$AssertStatusOK(e.AsStatus(),"An exception occurred in Module2DependenciesSameTransient.") + do $$$AssertStatusOK(e.AsStatus(),"An exception occurred in Test09Module2DependenciesSameTransient.") } } @@ -277,14 +337,32 @@ Method Test09Module2DependenciesSameTransient() Method Test10ComplexNestedDependencies() { try { - set moduleName = ..#ModuleL - do ..AssertInstallCreatesLockFileAsExpected(moduleName) + do ..ExecuteGenericLockFileTest(..#ModuleL) } catch e { - do $$$AssertStatusOK(e.AsStatus(),"An exception occurred in TestComplexNestedDependencies.") + do $$$AssertStatusOK(e.AsStatus(),"An exception occurred in Test10ComplexNestedDependencies.") } } -/// Base cases of creating and installing from a lock file +/// Generic lock file test are steps that are shared across many cases above. The steps are: +/// 1. Install and create lock file for module +/// 2. Check that the lock file generated matches expected values +/// 3. Uninstall the module (and dependencies) +/// 4. Install from lock file via zpm "ci" +/// 5. Call method in installed module (which in turn calls dependency methods) to confirm "ci" loads all modules correctly +Method ExecuteGenericLockFileTest(moduleName As %String) +{ + // Install and create lock file for module + do ..AssertInstallCreatesLockFileAsExpected(moduleName) + + // Uninstall module to prepare to install from lock file + set sc = ##class(%IPM.Main).Shell("uninstall -r "_moduleName) + do $$$AssertStatusOK(sc, "Uninstalled "_moduleName) + + // Install module from lock file + do ..AssertInstallFromLockFileAsExpected(moduleName) +} + +/// Base cases of creating a lock file upon install of a module Method AssertInstallCreatesLockFileAsExpected(moduleName As %String) { // Do an initial install of the module and create a lock file for it @@ -310,13 +388,33 @@ ClassMethod AreLockFilesEqual( expectedLockFilePath As %String) As %Boolean { try { - set actualLockFileContents = ##class(%DynamicAbstractObject).%FromJSONFile(actualLockFilePath) - set expectedLockFileContents = ##class(%DynamicAbstractObject).%FromJSONFile(expectedLockFilePath) - return actualLockFileContents.%ToJSON() = expectedLockFileContents.%ToJSON() + set actualLockFileContents = ##class(%DynamicAbstractObject).%FromJSONFile(actualLockFilePath) + set expectedLockFileContents = ##class(%DynamicAbstractObject).%FromJSONFile(expectedLockFilePath) + zwrite actualLockFileContents + zwrite expectedLockFileContents + return actualLockFileContents.%ToJSON() = expectedLockFileContents.%ToJSON() } catch (ex) { // Should error if we fail to open either the actual or expected file return ex.AsStatus() } } +/// Base case of installing a module from a lock file and confirming everything gets loaded as expected +Method AssertInstallFromLockFileAsExpected(moduleName As %String) +{ + // TODO: Replace this with zpm "ci" once implemented + set sc = ##class(%IPM.Main).Shell("install "_moduleName) + + set sc = $$$OK + try { + // Letter of the module is in the 10th place: "lock-mod-x-..." + set packageLetter = $zconvert($extract(moduleName, 10), "u") + set className = "LockMod"_packageLetter_".Class1" + do $classmethod(className, "MethodA") + } catch (ex) { + set sc = ex.AsStatus() + } + do $$$AssertStatusOK(sc, "Called MethodA() to confirm "_moduleName_" and dependency classes loaded correctly") +} + } diff --git a/tests/integration_tests/Test/PM/Integration/_data/lock-test/expected-files/lock-mod-oras.json b/tests/integration_tests/Test/PM/Integration/_data/lock-test/expected-files/lock-mod-oras.json index 4d6c5fc86..222396971 100644 --- a/tests/integration_tests/Test/PM/Integration/_data/lock-test/expected-files/lock-mod-oras.json +++ b/tests/integration_tests/Test/PM/Integration/_data/lock-test/expected-files/lock-mod-oras.json @@ -38,9 +38,19 @@ "lock-mod-b-no-deps": "^1.0.0" } }, + "lock-mod-e-1-dep-0-transient": { + "version": "1.0.0", + "repository": "lock-file-base", + "dependencies": { + "lock-mod-a-no-deps": "^1.0.0" + } + }, "lock-mod-remote": { "version": "1.0.0", - "repository": "lock-file-remote" + "repository": "lock-file-remote", + "dependencies": { + "lock-mod-e-1-dep-0-transient": "^1.0.0" + } } } } \ No newline at end of file diff --git a/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-g-all-repo-types/module.xml b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-g-all-repo-types/module.xml index 31c9fc088..8242bb9f2 100644 --- a/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-g-all-repo-types/module.xml +++ b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-g-all-repo-types/module.xml @@ -14,10 +14,6 @@ lock-mod-oras ^1.0.0 - - lock-mod-c-2-deps-0-transient - ^1.0.0 - diff --git a/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-other-repos/lock-mod-oras/module-lock.json b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-other-repos/lock-mod-oras/module-lock.json new file mode 100644 index 000000000..f5bba8066 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-other-repos/lock-mod-oras/module-lock.json @@ -0,0 +1,52 @@ +{ + "name": "lock-mod-oras", + "version": "1.0.0", + "repository": "lock-file-other-repos", + "lockFileVersion": "1", + "repositories": { + "lock-file-base": { + "type": "filesystem", + "readOnly": false, + "root": "/home/irisowner/zpm/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-base-cases/", + "depth": 0 + }, + "lock-file-other-repos": { + "type": "filesystem", + "readOnly": false, + "root": "/home/irisowner/zpm/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-other-repos/", + "depth": 0 + } + }, + "dependencies": { + "lock-mod-a-no-deps": { + "version": "1.0.0", + "repository": "lock-file-base" + }, + "lock-mod-b-no-deps": { + "version": "1.0.0", + "repository": "lock-file-base" + }, + "lock-mod-c-2-deps-0-transient": { + "version": "1.0.0", + "repository": "lock-file-base", + "dependencies": { + "lock-mod-a-no-deps": "^1.0.0", + "lock-mod-b-no-deps": "^1.0.0" + } + }, + "lock-mod-e-1-dep-0-transient": { + "version": "1.0.0", + "repository": "lock-file-base", + "dependencies": { + "lock-mod-a-no-deps": "^1.0.0" + } + }, + "lock-mod-remote": { + "version": "1.0.0", + "repository": "lock-file-other-repos", + "dependencies": { + "lock-mod-e-1-dep-0-transient": "^1.0.0" + } + } + } +} \ No newline at end of file diff --git a/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-other-repos/lock-mod-remote/module-lock.json b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-other-repos/lock-mod-remote/module-lock.json new file mode 100644 index 000000000..f18129b2f --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-other-repos/lock-mod-remote/module-lock.json @@ -0,0 +1,33 @@ +{ + "name": "lock-mod-remote", + "version": "1.0.0", + "repository": "lock-file-other-repos", + "lockFileVersion": "1", + "repositories": { + "lock-file-base": { + "type": "filesystem", + "readOnly": false, + "root": "/home/irisowner/zpm/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-base-cases/", + "depth": 0 + }, + "lock-file-other-repos": { + "type": "filesystem", + "readOnly": false, + "root": "/home/irisowner/zpm/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-other-repos/", + "depth": 0 + } + }, + "dependencies": { + "lock-mod-a-no-deps": { + "version": "1.0.0", + "repository": "lock-file-base" + }, + "lock-mod-e-1-dep-0-transient": { + "version": "1.0.0", + "repository": "lock-file-base", + "dependencies": { + "lock-mod-a-no-deps": "^1.0.0" + } + } + } +} \ No newline at end of file diff --git a/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-other-repos/lock-mod-remote/module.xml b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-other-repos/lock-mod-remote/module.xml index e3f271bf0..7a244a371 100644 --- a/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-other-repos/lock-mod-remote/module.xml +++ b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-other-repos/lock-mod-remote/module.xml @@ -5,6 +5,12 @@ lock-mod-remote 1.0.0 + + + lock-mod-e-1-dep-0-transient + ^1.0.0 + + \ No newline at end of file From dedd323c5f1e693c3849452250f1cb415970c0b1 Mon Sep 17 00:00:00 2001 From: James Lechtner Date: Thu, 5 Feb 2026 17:00:13 -0500 Subject: [PATCH 2/8] Adding bulk of remaining tests for zpm "ci" --- src/cls/IPM/Main.cls | 99 ------------------- .../Test/PM/Integration/LockFile.cls | 92 +++++++---------- .../expected-files/lock-mod-remote.json | 32 ++++++ .../cls/LockModH/Class1.cls | 3 +- .../cls/LockModH/Class1.cls | 10 +- .../lock-misordered.json | 38 +++++++ .../module-lock.json | 38 +++++++ .../lock-mod-oras/module-lock.json | 16 +-- .../lock-mod-remote/module-lock.json | 9 +- 9 files changed, 162 insertions(+), 175 deletions(-) create mode 100644 tests/integration_tests/Test/PM/Integration/_data/lock-test/expected-files/lock-mod-remote.json create mode 100644 tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-j-deps-misordered/lock-misordered.json create mode 100644 tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-j-deps-misordered/module-lock.json diff --git a/src/cls/IPM/Main.cls b/src/cls/IPM/Main.cls index 81f464734..3df11db74 100644 --- a/src/cls/IPM/Main.cls +++ b/src/cls/IPM/Main.cls @@ -2460,105 +2460,6 @@ ClassMethod CleanInstall(ByRef commandInfo) [ Internal ] // Forward execution to install do ..Install(.commandInfo, log) - - - - /* - zwrite commandInfo - return - set registry = "" - set moduleName = $get(commandInfo("parameters","module")) - if (moduleName["/") { - set $listbuild(registry, moduleName) = $listfromstring(moduleName, "/") - } - - if pLog = "" { - set log = ##class(%IPM.General.HistoryTemp).CleanInstallInit(moduleName) - } else { - set log = pLog - } - - try { - if (moduleName = "") { - $$$ThrowStatus($$$ERROR($$$GeneralError, "No module name specified.")) - } - - if moduleName = $$$IPMModuleName { - $$$ThrowOnError(..CheckModuleNamespace()) - } - // check if any registries are configured and enabled - new SQLCODE, count - &sql(SELECT COUNT(*) into :count FROM %IPM_Repo.Definition WHERE Enabled = 1) - $$$ThrowSQLIfError(SQLCODE,%message) - if (SQLCODE = 0) && (count = 0) { - $$$ThrowStatus($$$ERROR($$$GeneralError, "No repositories are configured and enabled in this namespace.")) - } - - set version = $get(commandInfo("parameters","version")) - - set searchCriteria = ##class(%IPM.Repo.SearchCriteria).%New() - set searchCriteria.Registry = registry - set searchCriteria.Name = $$$lcase(moduleName) - set searchCriteria.VersionExpression = version - $$$ThrowOnError(##class(%IPM.Repo.Utils).SearchRepositoriesForModule(searchCriteria,.results)) - - if (results.Count() > 0) { - set result = "" - #dim result As %IPM.Storage.QualifiedModuleInfo - // Results are ordered by semantic version, descending. (So the "latest" version will always be first.) - if ('$$$HasModifier(commandInfo,"prompt") || (results.Count() = 1)) { // && (tKeywords = "") { - set result = results.GetAt(1) - } elseif (results.Count() > 0) { - for i=1:1:results.Count() { - set tResultInfo = results.GetAt(i) - set tOptArray(i) = tResultInfo.DisplayName_" "_tResultInfo.VersionString_" @ "_tResultInfo.ServerName - } - - set tValue = "" - set tResponse = ##class(%Library.Prompt).GetMenu("Which version?",.tValue,.tOptArray,,$$$InitialDisplayMask+$$$EnableQuitCharMask) - if (tResponse '= $$$SuccessResponse) { - $$$ThrowStatus($$$ERROR($$$GeneralError,"Operation cancelled.")) - } - if (tValue '= "") { - set result = results.GetAt(tValue) - } - } - - if (result '= "") { - do ##class(%IPM.Lifecycle.Base).GetDefaultParameters(.params) - merge params = commandInfo("data") - set params("cmd") = "ci" - set params("CleanInstall") = 1 - set params("Verbose") = 1 // TODO: Replace this with actual new parameter instead of shoe-horning this in for logging at other parts of the loading - if result.Deployed { - set platformVersion = $system.Version.GetMajor() _ "." _$system.Version.GetMinor() - set result.PlatformVersion = platformVersion - if ('result.PlatformVersions.Find(platformVersion)) { - $$$ThrowStatus($$$ERROR($$$GeneralError, "Deployed package '"_moduleName_"' "_result.VersionString_" not supported on this platform "_platformVersion_".")) - } - } - $$$ThrowOnError(log.SetSource(result.ServerName)) - $$$ThrowOnError(log.SetVersion(result.Version)) - $$$ThrowOnError(##class(%IPM.Utils.Module).LoadQualifiedReference(result, .params, , log)) - } - } else { - set tPrefix = "" - if (moduleName '= "") { - if (version '= "") { - $$$ThrowStatus($$$ERROR($$$GeneralError, moduleName_" "_version_" not found in any repository.")) - } else { - $$$ThrowStatus($$$ERROR($$$GeneralError, "'"_moduleName_"' not found in any repository.")) - } - } else { - write !,"No modules found. Are there any repositories configured in the current namespace?" - } - } - } catch ex { - $$$ThrowOnError(log.Finalize(ex.AsStatus())) - throw ex - } - $$$ThrowOnError(log.Finalize($$$OK)) - */ } ClassMethod Reinstall(ByRef pCommandInfo) [ Internal ] diff --git a/tests/integration_tests/Test/PM/Integration/LockFile.cls b/tests/integration_tests/Test/PM/Integration/LockFile.cls index 8344fd8fe..4a7ece105 100644 --- a/tests/integration_tests/Test/PM/Integration/LockFile.cls +++ b/tests/integration_tests/Test/PM/Integration/LockFile.cls @@ -127,8 +127,7 @@ Method Test04Module2Dependencies1Transient() /// Edge cases below: /// All repository types /// Uses Module G -/// TODO: Update lock-mod-oras: Created lock file with mods-other-repos filesystem repo instead of oras + remote repo -Method Test05ModuleDependenciesAllRepositoryTypes() +Method Test05AllRepositoryTypes() { set sc = $$$OK try { @@ -165,41 +164,8 @@ Method Test05ModuleDependenciesAllRepositoryTypes() do $$$AssertStatusOK(sc,"Removed lock-file-other-repos repo successfully.") // Now that modules and repositories are set up, do the actual test - set moduleName = "lock-mod-oras" - do ..AssertInstallCreatesLockFileAsExpected(moduleName) - - // Go back to the initial state by uninstalling the module, dependencies, and repositories used for this test - do ##class(%IPM.Main).Shell("uninstall -r "_moduleName) - do $$$AssertStatusOK(sc, "Uninstalled "_moduleName_" and dependencies at the end of the test") - - - // Now test installing from a lock file from a module in a remote repository - set moduleName = "lock-mod-remote" - - // TODO: Replace this with zpm "ci" once implemented - set sc = ##class(%IPM.Main).Shell("install "_remoteRepo_"/"_moduleName) - do $$$AssertStatusOK(sc, "Installed "_moduleName_" via the laock file successfully") - - // Confirm that module and dependency classes were loaded correctly - do $classmethod("LockModRemote.Class1", "MethodA") - - - // Now test installing from a lock file from a module in an oras repository - set moduleName = "lock-mod-oras" - - // TODO: Replace this with zpm "ci" once implemented - set sc = ##class(%IPM.Main).Shell("install "_orasRepo_"/"_moduleName) - do $$$AssertStatusOK(sc, "Installed "_moduleName_" via the laock file successfully") - - // Confirm that module and dependency classes were loaded correctly - do $classmethod("LockModORAS.Class1", "MethodA") - - - - - - - + do ..ExecuteGenericLockFileTest(remoteMod, "LockModRemote.Class1") + do ..ExecuteGenericLockFileTest(orasMod, "LockModORAS.Class1") do ##class(%IPM.Main).Shell("repo -delete -name "_remoteRepo) do $$$AssertStatusOK(sc, "Removed "_remoteRepo_" at the end of the test") @@ -236,11 +202,11 @@ Method Test06ModuleMultipleVersions() do $$$AssertStatusOK(sc, "Uninstalled "_moduleName) // Install module from lock file - // TODO: Replace this with zpm "ci" once implemented - set sc = ##class(%IPM.Main).Shell("install "_moduleName) + set sc = ##class(%IPM.Main).Shell("ci "_moduleName) + do $$$AssertStatusOK(sc, "Able to install "_moduleName_" v"_latestVersion_" from the lock file") // Confirm classes and dependencies get installed as well - do $classmethod("LockModH.Class1", "MethodA") + do $classmethod("LockModH.Class1", "MethodB") // Uninstall module before testing with other version set sc = ##class(%IPM.Main).Shell("uninstall -r "_moduleName) @@ -267,8 +233,8 @@ Method Test06ModuleMultipleVersions() do $$$AssertStatusOK(sc, "Uninstalled "_moduleName) // Install module from lock file - // TODO: Replace this with zpm "ci" once implemented - set sc = ##class(%IPM.Main).Shell("install "_moduleName) + set sc = ##class(%IPM.Main).Shell("ci "_moduleName_" "_olderVersion) + do $$$AssertStatusOK(sc, "Able to install "_moduleName_" v"_olderVersion_" from the lock file") // Confirm classes and dependencies get installed as well do $classmethod("LockModH.Class1", "MethodA") @@ -278,10 +244,10 @@ Method Test06ModuleMultipleVersions() } /// Module with no pre-existing lock file -/// - [TODO, with "ci" command] zpm "ci" should fail due to not being able to locate the lock file /// - Test creation of a lock file, for a module that did not already have one. /// - Differs from other tests which overwrite pre-existing lock files /// - Both cases SHOULD be functionally equivalent +/// - zpm "ci" should fail due to not being able to locate the lock file /// Uses Module I Method Test07ModuleNoLockFile() { @@ -302,22 +268,28 @@ Method Test07ModuleNoLockFile() set sc = ##class(%IPM.Main).Shell("uninstall -r "_moduleName) do $$$AssertStatusOK(sc, "Uninstalled "_moduleName) - // TODO: This should fail due to no lock file - //Install module from lock file - //do ..AssertInstallFromLockFileAsExpected(moduleName) + // This should fail due to no lock file + set sc = ##class(%IPM.Main).Shell("ci "_moduleName) + do $$$AssertStatusNotOK(sc, "zpm ""ci"" fails on module "_moduleName_"due to not having a lock file") } catch e { do $$$AssertStatusOK(e.AsStatus(),"An exception occurred in Test07ModuleNoLockFile.") } } +/// TODO: This succeeds, compiling modules doesn't see dependency issues and by the time +/// we call LockModJ.Class1::MethodA() all dependencies are loaded so no issues there. +/// - Is that okay? +/// - I'm inclined to say yes. Dependencies out of order isn't too realistic a use case +/// anyways so maybe delete this case altogether? /// Try install on a lock file that lists dependencies out of order /// Uses Module J Method Test08ModuleDependenciesOutOfOrder() { set moduleName = ..#ModuleJ - // TODO: Replace this with zpm "ci" once implemented - //set sc = ##class(%IPM.Main).Shell("install "_moduleName) + zpm "install -lock "_moduleName + + //set sc = ##class(%IPM.Main).Shell("ci "_moduleName) //do $$$AssertStatusNotOK(sc, "Installing from lock file with dependencies listed out of order fails") } @@ -349,7 +321,9 @@ Method Test10ComplexNestedDependencies() /// 3. Uninstall the module (and dependencies) /// 4. Install from lock file via zpm "ci" /// 5. Call method in installed module (which in turn calls dependency methods) to confirm "ci" loads all modules correctly -Method ExecuteGenericLockFileTest(moduleName As %String) +Method ExecuteGenericLockFileTest( + moduleName As %String, + className As %String = "") { // Install and create lock file for module do ..AssertInstallCreatesLockFileAsExpected(moduleName) @@ -359,7 +333,7 @@ Method ExecuteGenericLockFileTest(moduleName As %String) do $$$AssertStatusOK(sc, "Uninstalled "_moduleName) // Install module from lock file - do ..AssertInstallFromLockFileAsExpected(moduleName) + do ..AssertInstallFromLockFileAsExpected(moduleName, className) } /// Base cases of creating a lock file upon install of a module @@ -390,8 +364,6 @@ ClassMethod AreLockFilesEqual( try { set actualLockFileContents = ##class(%DynamicAbstractObject).%FromJSONFile(actualLockFilePath) set expectedLockFileContents = ##class(%DynamicAbstractObject).%FromJSONFile(expectedLockFilePath) - zwrite actualLockFileContents - zwrite expectedLockFileContents return actualLockFileContents.%ToJSON() = expectedLockFileContents.%ToJSON() } catch (ex) { // Should error if we fail to open either the actual or expected file @@ -400,16 +372,20 @@ ClassMethod AreLockFilesEqual( } /// Base case of installing a module from a lock file and confirming everything gets loaded as expected -Method AssertInstallFromLockFileAsExpected(moduleName As %String) +Method AssertInstallFromLockFileAsExpected( + moduleName As %String, + className As %String = "") { - // TODO: Replace this with zpm "ci" once implemented - set sc = ##class(%IPM.Main).Shell("install "_moduleName) + set sc = ##class(%IPM.Main).Shell("ci "_moduleName) + do $$$AssertStatusOK(sc, "Installed "_moduleName_" via the lock file successfully") set sc = $$$OK try { - // Letter of the module is in the 10th place: "lock-mod-x-..." - set packageLetter = $zconvert($extract(moduleName, 10), "u") - set className = "LockMod"_packageLetter_".Class1" + if (className = "") { + // Letter of the module is in the 10th place: "lock-mod-x-..." + set packageLetter = $zconvert($extract(moduleName, 10), "u") + set className = "LockMod"_packageLetter_".Class1" + } do $classmethod(className, "MethodA") } catch (ex) { set sc = ex.AsStatus() diff --git a/tests/integration_tests/Test/PM/Integration/_data/lock-test/expected-files/lock-mod-remote.json b/tests/integration_tests/Test/PM/Integration/_data/lock-test/expected-files/lock-mod-remote.json new file mode 100644 index 000000000..bf13209a2 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/lock-test/expected-files/lock-mod-remote.json @@ -0,0 +1,32 @@ +{ + "name": "lock-mod-remote", + "version": "1.0.0", + "repository": "lock-file-remote", + "lockFileVersion": "1", + "repositories": { + "lock-file-base": { + "type": "filesystem", + "readOnly": false, + "root": "/home/irisowner/zpm/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-base-cases/", + "depth": 0 + }, + "lock-file-remote": { + "type": "remote", + "readOnly": false, + "url": "http://registry:52773/registry" + } + }, + "dependencies": { + "lock-mod-a-no-deps": { + "version": "1.0.0", + "repository": "lock-file-base" + }, + "lock-mod-e-1-dep-0-transient": { + "version": "1.0.0", + "repository": "lock-file-base", + "dependencies": { + "lock-mod-a-no-deps": "^1.0.0" + } + } + } +} \ No newline at end of file diff --git a/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-h-multiple-versions-v2/cls/LockModH/Class1.cls b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-h-multiple-versions-v2/cls/LockModH/Class1.cls index 213d1512c..7597fba68 100644 --- a/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-h-multiple-versions-v2/cls/LockModH/Class1.cls +++ b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-h-multiple-versions-v2/cls/LockModH/Class1.cls @@ -5,8 +5,7 @@ ClassMethod MethodA() { write !, "This is LockModH v2 ##class(LockModH.Class1).MethodA()" - write !, "Now calling dependency classes (LockModB, LockModE)" - do ##class(LockModB.Class1).MethodA() + write !, "Now calling dependency classes (LockModA, LockModE)" do ##class(LockModE.Class1).MethodA() } diff --git a/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-h-multiple-versions-v3/cls/LockModH/Class1.cls b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-h-multiple-versions-v3/cls/LockModH/Class1.cls index f0e91ba5e..cae94974c 100644 --- a/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-h-multiple-versions-v3/cls/LockModH/Class1.cls +++ b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-h-multiple-versions-v3/cls/LockModH/Class1.cls @@ -1,14 +1,14 @@ Class LockModH.Class1 { -ClassMethod MethodA() +/// Method renamed from MethodA in v2 to MethodB in v3 +ClassMethod MethodB() { - write !, "This is LockModH v3 ##class(LockModH.Class1).MethodA()" + write !, "This is LockModH v3 ##class(LockModH.Class1).MethodB()" - write !, "Now calling dependency classes (LockModA, LockModB, LockModE)" - do ##class(LockModA.Class1).MethodA() - do ##class(LockModB.Class1).MethodA() + write !, "Now calling dependency classes (LockModE, LockModB)" do ##class(LockModE.Class1).MethodA() + do ##class(LockModB.Class1).MethodA() } } diff --git a/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-j-deps-misordered/lock-misordered.json b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-j-deps-misordered/lock-misordered.json new file mode 100644 index 000000000..09769d2e0 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-j-deps-misordered/lock-misordered.json @@ -0,0 +1,38 @@ +{ + "name": "lock-mod-j-deps-misordered", + "version": "1.0.0", + "repository": "lock-file-edge", + "lockFileVersion": "1", + "repositories": { + "lock-file-base": { + "type": "filesystem", + "readOnly": false, + "root": "/home/irisowner/zpm/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-base-cases/", + "depth": 0 + }, + "lock-file-edge": { + "type": "filesystem", + "readOnly": false, + "root": "/home/irisowner/zpm/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/", + "depth": 0 + } + }, + "dependencies": { + "lock-mod-c-2-deps-0-transient": { + "version": "1.0.0", + "repository": "lock-file-base", + "dependencies": { + "lock-mod-a-no-deps": "^1.0.0", + "lock-mod-b-no-deps": "^1.0.0" + } + }, + "lock-mod-a-no-deps": { + "version": "1.0.0", + "repository": "lock-file-base" + }, + "lock-mod-b-no-deps": { + "version": "1.0.0", + "repository": "lock-file-base" + } + } +} \ No newline at end of file diff --git a/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-j-deps-misordered/module-lock.json b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-j-deps-misordered/module-lock.json new file mode 100644 index 000000000..09769d2e0 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-j-deps-misordered/module-lock.json @@ -0,0 +1,38 @@ +{ + "name": "lock-mod-j-deps-misordered", + "version": "1.0.0", + "repository": "lock-file-edge", + "lockFileVersion": "1", + "repositories": { + "lock-file-base": { + "type": "filesystem", + "readOnly": false, + "root": "/home/irisowner/zpm/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-base-cases/", + "depth": 0 + }, + "lock-file-edge": { + "type": "filesystem", + "readOnly": false, + "root": "/home/irisowner/zpm/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/", + "depth": 0 + } + }, + "dependencies": { + "lock-mod-c-2-deps-0-transient": { + "version": "1.0.0", + "repository": "lock-file-base", + "dependencies": { + "lock-mod-a-no-deps": "^1.0.0", + "lock-mod-b-no-deps": "^1.0.0" + } + }, + "lock-mod-a-no-deps": { + "version": "1.0.0", + "repository": "lock-file-base" + }, + "lock-mod-b-no-deps": { + "version": "1.0.0", + "repository": "lock-file-base" + } + } +} \ No newline at end of file diff --git a/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-other-repos/lock-mod-oras/module-lock.json b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-other-repos/lock-mod-oras/module-lock.json index f5bba8066..222396971 100644 --- a/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-other-repos/lock-mod-oras/module-lock.json +++ b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-other-repos/lock-mod-oras/module-lock.json @@ -1,7 +1,7 @@ { "name": "lock-mod-oras", "version": "1.0.0", - "repository": "lock-file-other-repos", + "repository": "lock-file-oras", "lockFileVersion": "1", "repositories": { "lock-file-base": { @@ -10,11 +10,15 @@ "root": "/home/irisowner/zpm/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-base-cases/", "depth": 0 }, - "lock-file-other-repos": { - "type": "filesystem", + "lock-file-oras": { + "type": "oras", "readOnly": false, - "root": "/home/irisowner/zpm/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-other-repos/", - "depth": 0 + "url": "http://oras:5000" + }, + "lock-file-remote": { + "type": "remote", + "readOnly": false, + "url": "http://registry:52773/registry" } }, "dependencies": { @@ -43,7 +47,7 @@ }, "lock-mod-remote": { "version": "1.0.0", - "repository": "lock-file-other-repos", + "repository": "lock-file-remote", "dependencies": { "lock-mod-e-1-dep-0-transient": "^1.0.0" } diff --git a/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-other-repos/lock-mod-remote/module-lock.json b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-other-repos/lock-mod-remote/module-lock.json index f18129b2f..bf13209a2 100644 --- a/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-other-repos/lock-mod-remote/module-lock.json +++ b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-other-repos/lock-mod-remote/module-lock.json @@ -1,7 +1,7 @@ { "name": "lock-mod-remote", "version": "1.0.0", - "repository": "lock-file-other-repos", + "repository": "lock-file-remote", "lockFileVersion": "1", "repositories": { "lock-file-base": { @@ -10,11 +10,10 @@ "root": "/home/irisowner/zpm/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-base-cases/", "depth": 0 }, - "lock-file-other-repos": { - "type": "filesystem", + "lock-file-remote": { + "type": "remote", "readOnly": false, - "root": "/home/irisowner/zpm/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-other-repos/", - "depth": 0 + "url": "http://registry:52773/registry" } }, "dependencies": { From 6a759be041f844fe9fefab506f926dff65e7fb50 Mon Sep 17 00:00:00 2001 From: James Lechtner Date: Fri, 27 Feb 2026 12:43:29 -0500 Subject: [PATCH 3/8] List reps + deps alphabetical. Determine dependency order at "ci" isntall time --- src/cls/IPM/General/LockFile.cls | 42 ++++++++++- src/cls/IPM/Repo/Oras/Definition.cls | 18 +++-- src/cls/IPM/Repo/Remote/Definition.cls | 17 +++-- .../Test/PM/Integration/LockFile.cls | 42 ++++------- .../lock-mod-g-all-repo-types.json | 70 +++++++++++++++++-- .../cls/LockModG/Class1.cls | 4 +- .../module-lock.json | 44 ++++++++++-- .../lock-mod-g-all-repo-types/module.xml | 4 ++ .../cls/LockModJ/Class1.cls | 12 ---- .../lock-misordered.json | 38 ---------- .../lock-mod-j-deps-misordered/module.xml | 16 ----- 11 files changed, 187 insertions(+), 120 deletions(-) rename tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/{lock-mod-j-deps-misordered => lock-mod-g-all-repo-types}/module-lock.json (51%) delete mode 100644 tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-j-deps-misordered/cls/LockModJ/Class1.cls delete mode 100644 tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-j-deps-misordered/lock-misordered.json delete mode 100644 tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-j-deps-misordered/module.xml diff --git a/src/cls/IPM/General/LockFile.cls b/src/cls/IPM/General/LockFile.cls index 4d2b3bb13..3a0f7230d 100644 --- a/src/cls/IPM/General/LockFile.cls +++ b/src/cls/IPM/General/LockFile.cls @@ -148,9 +148,12 @@ ClassMethod InstallFromLockFile( } // Install modules (even if they already exist) - set depIter = dependencies.%GetIterator() - while depIter.%GetNext(.depName, .depVals) { - //break + do ..GetOrderedDependenciesList(dependencies, .orderedDependenciesList) + set depName = "" + for i=1:1:$listlength(orderedDependenciesList) { + set depName = $list(orderedDependenciesList, i) + set depVals = dependencies.%Get(depName) + set version = depVals.version set repository = depVals.repository @@ -164,4 +167,37 @@ ClassMethod InstallFromLockFile( } } +/// The dependencies list in a lock file is listed alphabetically. +/// This method compiles the dependencies and creates an ordered list such that no module is listed +/// before one of its dependencies. Can then trace the list this outputs and install in order +ClassMethod GetOrderedDependenciesList( + dependencies As %DynamicObject, + ByRef orderedDependenciesList As %List = "") [ Internal ] +{ + set depIter = dependencies.%GetIterator() + while depIter.%GetNext(.depName, .depVals) { + do ..AddToOrderedDependenciesList(depName, dependencies, .orderedDependenciesList) + } +} + +/// For a dependency, recursively adds all dependencies to the ordered list, followed by this dependency +ClassMethod AddToOrderedDependenciesList( + dependencyName As %String, + dependencies As %DynamicObject, + ByRef orderedDependenciesList As %List) [ Internal, Private ] +{ + set depVals = dependencies.%Get(dependencyName) + set transientDeps = depVals.%Get("dependencies", {}) + set transientIter = transientDeps.%GetIterator() + while transientIter.%GetNext(.transDepName) { + // If dependency hasn't been installed yet, then recursively run this method on it + if '$listfind(orderedDependenciesList, transDepName) { + do ..AddToOrderedDependenciesList(transDepName, dependencies, .orderedDependenciesList) + } + } + if '$listfind(orderedDependenciesList, dependencyName) { + set orderedDependenciesList = orderedDependenciesList _ $listbuild(dependencyName) + } +} + } diff --git a/src/cls/IPM/Repo/Oras/Definition.cls b/src/cls/IPM/Repo/Oras/Definition.cls index 59798ffe1..0de8a7f9d 100644 --- a/src/cls/IPM/Repo/Oras/Definition.cls +++ b/src/cls/IPM/Repo/Oras/Definition.cls @@ -148,10 +148,20 @@ ClassMethod LockFileValuesToModifiers( set modifiers("name") = lockFileValues.%Get("name") set modifiers("read-only") = lockFileValues.%Get("readOnly") set modifiers("url") = lockFileValues.%Get("url") - // TODO: Implement calling system variables to get username/password/token values - set modifiers("username") = "" - set modifiers("password") = "" - set modifiers("token") = "" + set modifiers("namespace") = lockFileValues.%Get("orasNamespace") + + // The following variables are set as system level variables for us to get + // Naming convention for those follow the name of the repository, first converting any '-' to '_', + // then removing everything except for alphabetic characters, numbers, and '_' to use as variable prefix + // lastly, adds a suffix based on the modifier + // Examples: - + // "registry" - "registry" + // "ORAS!Repo(5)?" - "ORASRepo5" + // "My-Repository-2" - "My_Repository_2" + set prefix = $zstrip($replace(modifiers("name"), "-", "_"), "*E'N'A") + set modifiers("username") = $system.Util.GetEnviron(prefix_"_username") + set modifiers("password") = $system.Util.GetEnviron(prefix_"_password") + set modifiers("token") = $system.Util.GetEnviron(prefix_"_token") } XData LockFileMapping diff --git a/src/cls/IPM/Repo/Remote/Definition.cls b/src/cls/IPM/Repo/Remote/Definition.cls index 335b5340a..98eec5fd7 100644 --- a/src/cls/IPM/Repo/Remote/Definition.cls +++ b/src/cls/IPM/Repo/Remote/Definition.cls @@ -140,10 +140,19 @@ ClassMethod LockFileValuesToModifiers( set modifiers("name") = lockFileValues.%Get("name") set modifiers("url") = lockFileValues.%Get("url") set modifiers("read-only") = lockFileValues.%Get("readOnly") - // TODO: Implement calling system variables to get username/password/token values - set modifiers("username") = "" - set modifiers("password") = "" - set modifiers("token") = "" + + // The following variables are set as system level variables for us to get + // Naming convention for those follow the name of the repository, first converting any '-' to '_', + // then removing everything except for alphabetic characters, numbers, and '_' to use as variable prefix + // lastly, adds a suffix based on the modifier + // Examples: - + // "registry" - "registry" + // "ORAS!Repo(5)?" - "ORASRepo5" + // "My-Repository-2" - "My_Repository_2" + set prefix = $zstrip($replace(modifiers("name"), "-", "_"), "*E'N'A") + set modifiers("username") = $system.Util.GetEnviron(prefix_"_username") + set modifiers("password") = $system.Util.GetEnviron(prefix_"_password") + set modifiers("token") = $system.Util.GetEnviron(prefix_"_token") } Method LockFileTypeGet() diff --git a/tests/integration_tests/Test/PM/Integration/LockFile.cls b/tests/integration_tests/Test/PM/Integration/LockFile.cls index 4a7ece105..b1e53a1ac 100644 --- a/tests/integration_tests/Test/PM/Integration/LockFile.cls +++ b/tests/integration_tests/Test/PM/Integration/LockFile.cls @@ -25,7 +25,7 @@ Parameter ModuleE As String = "lock-mod-e-1-dep-0-transient"; /// Module with 1 dep w/ 1 transient & 1 dep w/o transient (E & B) Parameter ModuleF As String = "lock-mod-f-2-deps-1-transient"; -/// Module with dependencies for all repository types (Module remote, Module oras, Module http, Module filesystem, & Module perforce) +/// Module with dependencies for all repository types (Module remote, Module oras, Module http, Module filesystem) Parameter ModuleG As String = "lock-mod-g-all-repo-types"; /// Module with multiple versions @@ -36,10 +36,6 @@ Parameter ModuleH As String = "lock-mod-h-multiple-versions"; /// Base equivalent to Module F, just deleted the lock file Parameter ModuleI As String = "lock-mod-i-no-prior-lock-file"; -/// Base equivalent to Module D, but different lock file -/// Lock file as A->C->B instead of A->B->C (C is dependent on B) -Parameter ModuleJ As String = "lock-mod-j-deps-misordered"; - /// 2 dependencies: C & E, both with same transient dependency: A Parameter ModuleK As String = "lock-mod-k-common-transient"; @@ -125,7 +121,7 @@ Method Test04Module2Dependencies1Transient() } /// Edge cases below: -/// All repository types +/// Test with all repository types: Filesystem, ORAS, & Remote /// Uses Module G Method Test05AllRepositoryTypes() { @@ -163,9 +159,16 @@ Method Test05AllRepositoryTypes() set sc = ##class(%IPM.Main).Shell("repo -delete -name lock-file-other-repos") do $$$AssertStatusOK(sc,"Removed lock-file-other-repos repo successfully.") - // Now that modules and repositories are set up, do the actual test - do ..ExecuteGenericLockFileTest(remoteMod, "LockModRemote.Class1") - do ..ExecuteGenericLockFileTest(orasMod, "LockModORAS.Class1") + // Now that modules have been published to their respective repositories, run the test + do ..AssertInstallCreatesLockFileAsExpected(..#ModuleG) + + // Remove all repositories except lock-file-edge (where Module G comes from) to confirm all types get created correctly when installing from lock file + do ##class(%IPM.Main).Shell("repo -delete -name lock-file-base") + do ##class(%IPM.Main).Shell("repo -delete -name "_remoteRepo) + do ##class(%IPM.Main).Shell("repo -delete -name "_orasRepo) + + // Install from Module G's lock file + do ..AssertInstallFromLockFileAsExpected(..#ModuleG) do ##class(%IPM.Main).Shell("repo -delete -name "_remoteRepo) do $$$AssertStatusOK(sc, "Removed "_remoteRepo_" at the end of the test") @@ -276,26 +279,9 @@ Method Test07ModuleNoLockFile() } } -/// TODO: This succeeds, compiling modules doesn't see dependency issues and by the time -/// we call LockModJ.Class1::MethodA() all dependencies are loaded so no issues there. -/// - Is that okay? -/// - I'm inclined to say yes. Dependencies out of order isn't too realistic a use case -/// anyways so maybe delete this case altogether? -/// Try install on a lock file that lists dependencies out of order -/// Uses Module J -Method Test08ModuleDependenciesOutOfOrder() -{ - set moduleName = ..#ModuleJ - - zpm "install -lock "_moduleName - - //set sc = ##class(%IPM.Main).Shell("ci "_moduleName) - //do $$$AssertStatusNotOK(sc, "Installing from lock file with dependencies listed out of order fails") -} - /// 2 dependencies for a module have the same transient to add to the lock file /// Uses Module K -Method Test09Module2DependenciesSameTransient() +Method Test08Module2DependenciesSameTransient() { try { do ..ExecuteGenericLockFileTest(..#ModuleK) @@ -306,7 +292,7 @@ Method Test09Module2DependenciesSameTransient() /// Tests writing and installing from a lock file for a module with a more complex nested dependencies setup /// Uses Module L -Method Test10ComplexNestedDependencies() +Method Test09ComplexNestedDependencies() { try { do ..ExecuteGenericLockFileTest(..#ModuleL) diff --git a/tests/integration_tests/Test/PM/Integration/_data/lock-test/expected-files/lock-mod-g-all-repo-types.json b/tests/integration_tests/Test/PM/Integration/_data/lock-test/expected-files/lock-mod-g-all-repo-types.json index f9c1353bf..7d0d60846 100644 --- a/tests/integration_tests/Test/PM/Integration/_data/lock-test/expected-files/lock-mod-g-all-repo-types.json +++ b/tests/integration_tests/Test/PM/Integration/_data/lock-test/expected-files/lock-mod-g-all-repo-types.json @@ -1,14 +1,70 @@ { "name": "lock-mod-g-all-repo-types", - "version": "1.0.0", + "version": "3.0.0", "repository": "lock-file-edge", "lockFileVersion": "1", - "repositories": {}, + "repositories": { + "lock-file-base": { + "type": "filesystem", + "readOnly": false, + "root": "/home/irisowner/zpm/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-base-cases/", + "depth": 0 + }, + "lock-file-edge": { + "type": "filesystem", + "readOnly": false, + "root": "/home/irisowner/zpm/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/", + "depth": 0 + }, + "lock-file-oras": { + "type": "oras", + "readOnly": false, + "url": "http://oras:5000" + }, + "lock-file-remote": { + "type": "remote", + "readOnly": false, + "url": "http://registry:52773/registry" + } + }, "dependencies": { - "lock-mod-remote": {}, - "lock-mod-oras": {}, - "lock-mod-h-multiple-versionsttp": {}, - "lock-mod-f-2-deps-1-transientilesystem": {}, - "lock-mod-perforce": {} + "lock-mod-a-no-deps": { + "version": "1.0.0", + "repository": "lock-file-base" + }, + "lock-mod-b-no-deps": { + "version": "1.0.0", + "repository": "lock-file-base" + }, + "lock-mod-c-2-deps-0-transient": { + "version": "1.0.0", + "repository": "lock-file-base", + "dependencies": { + "lock-mod-a-no-deps": "^1.0.0", + "lock-mod-b-no-deps": "^1.0.0" + } + }, + "lock-mod-e-1-dep-0-transient": { + "version": "1.0.0", + "repository": "lock-file-base", + "dependencies": { + "lock-mod-a-no-deps": "^1.0.0" + } + }, + "lock-mod-oras": { + "version": "1.0.0", + "repository": "lock-file-oras", + "dependencies": { + "lock-mod-c-2-deps-0-transient": "^1.0.0", + "lock-mod-remote": "^1.0.0" + } + }, + "lock-mod-remote": { + "version": "1.0.0", + "repository": "lock-file-remote", + "dependencies": { + "lock-mod-e-1-dep-0-transient": "^1.0.0" + } + } } } \ No newline at end of file diff --git a/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-g-all-repo-types/cls/LockModG/Class1.cls b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-g-all-repo-types/cls/LockModG/Class1.cls index c39de71bc..65dc265d8 100644 --- a/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-g-all-repo-types/cls/LockModG/Class1.cls +++ b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-g-all-repo-types/cls/LockModG/Class1.cls @@ -5,10 +5,10 @@ ClassMethod MethodA() { write !, "This is ##class(LockModG.Class1).MethodA()" - write !, "Now calling dependency classes (LockModRemote, LockModORAS, LockModC)" + write !, "Now calling dependency classes (LockModB, LockModRemote, LockModORAS)" + do ##class(LockModB.Class1).MethodA() do ##class(LockModRemote.Class1).MethodA() do ##class(LockModORAS.Class1).MethodA() - do ##class(LockModC.Class1).MethodA() } } diff --git a/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-j-deps-misordered/module-lock.json b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-g-all-repo-types/module-lock.json similarity index 51% rename from tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-j-deps-misordered/module-lock.json rename to tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-g-all-repo-types/module-lock.json index 09769d2e0..7d0d60846 100644 --- a/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-j-deps-misordered/module-lock.json +++ b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-g-all-repo-types/module-lock.json @@ -1,6 +1,6 @@ { - "name": "lock-mod-j-deps-misordered", - "version": "1.0.0", + "name": "lock-mod-g-all-repo-types", + "version": "3.0.0", "repository": "lock-file-edge", "lockFileVersion": "1", "repositories": { @@ -15,9 +15,27 @@ "readOnly": false, "root": "/home/irisowner/zpm/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/", "depth": 0 + }, + "lock-file-oras": { + "type": "oras", + "readOnly": false, + "url": "http://oras:5000" + }, + "lock-file-remote": { + "type": "remote", + "readOnly": false, + "url": "http://registry:52773/registry" } }, "dependencies": { + "lock-mod-a-no-deps": { + "version": "1.0.0", + "repository": "lock-file-base" + }, + "lock-mod-b-no-deps": { + "version": "1.0.0", + "repository": "lock-file-base" + }, "lock-mod-c-2-deps-0-transient": { "version": "1.0.0", "repository": "lock-file-base", @@ -26,13 +44,27 @@ "lock-mod-b-no-deps": "^1.0.0" } }, - "lock-mod-a-no-deps": { + "lock-mod-e-1-dep-0-transient": { "version": "1.0.0", - "repository": "lock-file-base" + "repository": "lock-file-base", + "dependencies": { + "lock-mod-a-no-deps": "^1.0.0" + } }, - "lock-mod-b-no-deps": { + "lock-mod-oras": { "version": "1.0.0", - "repository": "lock-file-base" + "repository": "lock-file-oras", + "dependencies": { + "lock-mod-c-2-deps-0-transient": "^1.0.0", + "lock-mod-remote": "^1.0.0" + } + }, + "lock-mod-remote": { + "version": "1.0.0", + "repository": "lock-file-remote", + "dependencies": { + "lock-mod-e-1-dep-0-transient": "^1.0.0" + } } } } \ No newline at end of file diff --git a/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-g-all-repo-types/module.xml b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-g-all-repo-types/module.xml index 8242bb9f2..40d3f1e23 100644 --- a/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-g-all-repo-types/module.xml +++ b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-g-all-repo-types/module.xml @@ -6,6 +6,10 @@ 3.0.0 + + lock-mod-b + ^1.0.0 + lock-mod-remote ^1.0.0 diff --git a/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-j-deps-misordered/cls/LockModJ/Class1.cls b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-j-deps-misordered/cls/LockModJ/Class1.cls deleted file mode 100644 index 52b1281bb..000000000 --- a/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-j-deps-misordered/cls/LockModJ/Class1.cls +++ /dev/null @@ -1,12 +0,0 @@ -Class LockModJ.Class1 -{ - -ClassMethod MethodA() -{ - write !, "This is ##class(LockModJ.Class1).MethodA()" - - write !, "Now calling dependency classes (LockModC)" - do ##class(LockModC.Class1).MethodA() -} - -} diff --git a/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-j-deps-misordered/lock-misordered.json b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-j-deps-misordered/lock-misordered.json deleted file mode 100644 index 09769d2e0..000000000 --- a/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-j-deps-misordered/lock-misordered.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "lock-mod-j-deps-misordered", - "version": "1.0.0", - "repository": "lock-file-edge", - "lockFileVersion": "1", - "repositories": { - "lock-file-base": { - "type": "filesystem", - "readOnly": false, - "root": "/home/irisowner/zpm/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-base-cases/", - "depth": 0 - }, - "lock-file-edge": { - "type": "filesystem", - "readOnly": false, - "root": "/home/irisowner/zpm/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/", - "depth": 0 - } - }, - "dependencies": { - "lock-mod-c-2-deps-0-transient": { - "version": "1.0.0", - "repository": "lock-file-base", - "dependencies": { - "lock-mod-a-no-deps": "^1.0.0", - "lock-mod-b-no-deps": "^1.0.0" - } - }, - "lock-mod-a-no-deps": { - "version": "1.0.0", - "repository": "lock-file-base" - }, - "lock-mod-b-no-deps": { - "version": "1.0.0", - "repository": "lock-file-base" - } - } -} \ No newline at end of file diff --git a/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-j-deps-misordered/module.xml b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-j-deps-misordered/module.xml deleted file mode 100644 index 4139d4590..000000000 --- a/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-j-deps-misordered/module.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - lock-mod-j-deps-misordered - 1.0.0 - - - - lock-mod-c-2-deps-0-transient - ^1.0.0 - - - - - \ No newline at end of file From 15eed15eba9f2dd4adf66028c70bbe9dddad229e Mon Sep 17 00:00:00 2001 From: James Lechtner Date: Fri, 27 Feb 2026 13:30:06 -0500 Subject: [PATCH 4/8] Leftover changes to make to align with main --- src/cls/IPM/Utils/Module.cls | 1 - .../resource-test/localize/HTTPStatuses.xml | 71 ------------------- 2 files changed, 72 deletions(-) delete mode 100644 tests/integration_tests/Test/PM/Integration/_data/resource-test/localize/HTTPStatuses.xml diff --git a/src/cls/IPM/Utils/Module.cls b/src/cls/IPM/Utils/Module.cls index fb097d720..8c771a0c6 100644 --- a/src/cls/IPM/Utils/Module.cls +++ b/src/cls/IPM/Utils/Module.cls @@ -951,7 +951,6 @@ ClassMethod GetModuleNameFromXML( /// 1 /// /// ``` -/// /// Returns results as multidimensional array ClassMethod GetModuleDefaultsFromXML( pDirectory As %String, diff --git a/tests/integration_tests/Test/PM/Integration/_data/resource-test/localize/HTTPStatuses.xml b/tests/integration_tests/Test/PM/Integration/_data/resource-test/localize/HTTPStatuses.xml deleted file mode 100644 index 5089633c2..000000000 --- a/tests/integration_tests/Test/PM/Integration/_data/resource-test/localize/HTTPStatuses.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - - 100 Continue %1 - 101 Switching Protocols %1 - 102 Processing %1 - 200 OK %1 - 201 Created %1 - 202 Accepted %1 - 203 Non-Authoritative Information %1 - 204 No Content %1 - 205 Reset Content %1 - 206 Partial Content %1 - 207 Multi-Status %1 - 208 Already Reported %1 - 226 Instance Manipulation Used %1 - 300 Multiple Choices %1 - 301 Moved Permanently %1 - 302 Found %1 - 303 See Other %1 - 304 Not Modified %1 - 305 Use Proxy %1 - 306 Switch Proxy %1 - 307 Temporary Redirect %1 - 308 Permanent Redirect %1 - 400 Bad Request %1 - 401 Unauthorized %1 - 402 Payment Required %1 - 403 Forbidden %1 - 404 Not Found %1 - 405 Method Not Allowed %1 - 406 Not Acceptable %1 - 407 Proxy Authentication Required %1 - 408 Request Timeout %1 - 409 Conflict %1 - 410 Gone %1 - 411 Length Required %1 - 412 Precondition Failed %1 - 413 Payload Too Large %1 - 414 URI Too Long %1 - 415 Unsupported Media Type %1 - 416 Range Not Satisfiable %1 - 417 Expectation Failed %1 - 418 I'm a teapot %1 - 421 Misdirected Request %1 - 422 Unprocessable Entity %1 - 423 Locked %1 - 424 Failed Dependency %1 - 426 Upgrade Required %1 - 428 Precondition Required %1 - 429 Too Many Requests %1 - 431 Request Header Fields Too Large %1 - 440 Login Timeout %1 - 449 Retry With %1 - 451 Unavailable For Legal Reasons %1 - 500 Internal Server Error %1 - 501 Not Implemented %1 - 502 Bad Gateway %1 - 503 Service Unavailable %1 - 504 Gateway Timeout %1 - 505 HTTP Version Not Supported %1 - 506 Variant Also Negotiates %1 - 507 Insufficient Storage %1 - 508 Loop Detected %1 - 509 Bandwidth Limit Exceeded %1 - 510 Not Extended %1 - 511 Network Authentication Required %1 - 598 Network Read Timeout Error %1 - 599 Network Connect Timeout Error %1 - - From d5bdbc10c892b4c5726e6e4aa652b98ec00d0d47 Mon Sep 17 00:00:00 2001 From: James Lechtner Date: Fri, 27 Feb 2026 13:52:17 -0500 Subject: [PATCH 5/8] Adding changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c8c152ed..500597d96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - #1024: Added flag -export-python-deps to publish command +- #962: Adding zpm "ci" command to install from a lock file ### Fixed - #996: Ensure COS commands execute in exec under a dedicated, isolated context From bdecd8b71f3ab5893cb6c8a39277e4d1f1999e29 Mon Sep 17 00:00:00 2001 From: James Lechtner Date: Mon, 27 Apr 2026 15:24:28 -0400 Subject: [PATCH 6/8] Savepoint of current progress before I nuke the "ci" command --- CHANGELOG.md | 2 +- src/cls/IPM/General/LockFile.cls | 7 ++- src/cls/IPM/Main.cls | 33 +++++++++----- .../Test/PM/Integration/History.cls | 35 +++++++++++++++ .../Test/PM/Integration/LockFile.cls | 44 ++++++++++++++++++- 5 files changed, 106 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 964c8ac3b..93de081b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #992: Implement automatic history purge logic - #973: Enables CORS and JWT configuration for WebApplications in module.xml - #1110: Add `iriscli` and `ipm` container utility scripts that are auto-installed to `~/.local/bin/` and `~/bin/` so they work both inside and outside of containers (Unix/Linux only) +- #962: Adding zpm "ci" command to install from a lock file ### Fixed - #1001: The `unmap` and `enable` commands will now only activate CPF merge once after all namespaces have been configured instead after every namespace @@ -26,7 +27,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - #1024: Added flag -export-python-deps to publish command -- #962: Adding zpm "ci" command to install from a lock file ### Fixed - #996: Ensure COS commands execute in exec under a dedicated, isolated context diff --git a/src/cls/IPM/General/LockFile.cls b/src/cls/IPM/General/LockFile.cls index 3a0f7230d..c0e35b546 100644 --- a/src/cls/IPM/General/LockFile.cls +++ b/src/cls/IPM/General/LockFile.cls @@ -136,8 +136,7 @@ ClassMethod InstallFromLockFile( write !, "Repo: "_repoName_" already exists, skipping creating new one from lock file" } continue - } - elseif (verbose) { + } elseif (verbose) { write !, "Creating repo: "_repoName_" from lock file" } do ##class(%IPM.Repo.Definition).CollectServerTypes(.types) @@ -163,6 +162,10 @@ ClassMethod InstallFromLockFile( set commandInfo("parameters","module") = repository_"/"_depName set commandInfo("parameters", "version") = version set commandInfo("data", "LockFileInstallStarted") = 1 + + set commandInfo("data","cmd") = "ci" + set commandInfo("data","CleanInstall") = 1 + do ##class(%IPM.Main).CleanInstall(.commandInfo) } } diff --git a/src/cls/IPM/Main.cls b/src/cls/IPM/Main.cls index 81fadea5a..607baf841 100644 --- a/src/cls/IPM/Main.cls +++ b/src/cls/IPM/Main.cls @@ -293,6 +293,7 @@ load C:\module\root\path -env C:\path\to\env1.json;C:\path\to\env2.json + @@ -811,6 +812,7 @@ generate /my/path -export 00000,PacketName2,IgnorePacket2^00000,PacketName3,Igno + @@ -2398,14 +2400,18 @@ ClassMethod LoadInternal( } } - // When loading a module from a local folder, there might be a /.modules/ folder containining dependencies. - // It's easier to configure a temporary repository than to handle this case in the dependency resolution code. set tTargetDirectory = $get(tTargetDirectory, tDirectoryName) - set dotModules = ##class(%File).NormalizeDirectory(".modules", tTargetDirectory) - set tmpRepoMgr = ##class(%IPM.General.TempLocalRepoManager).%New(dotModules, 1) - set tSC = ##class(%IPM.Utils.Module).LoadNewModule(tTargetDirectory, .tParams, , pLog) - set tSC = $$$ADDSC(tSC, tmpRepoMgr.CleanUp()) - $$$ThrowOnError(tSC) + if ($get(pCommandInfo("data", "LoadFromLockFile"))) { + do ##class(%IPM.General.LockFile).InstallFromLockFile(tTargetDirectory, .tParams) + } else { + // When loading a module from a local folder, there might be a /.modules/ folder containining dependencies. + // It's easier to configure a temporary repository than to handle this case in the dependency resolution code. + set dotModules = ##class(%File).NormalizeDirectory(".modules", tTargetDirectory) + set tmpRepoMgr = ##class(%IPM.General.TempLocalRepoManager).%New(dotModules, 1) + set tSC = ##class(%IPM.Utils.Module).LoadNewModule(tTargetDirectory, .tParams, , pLog) + set tSC = $$$ADDSC(tSC, tmpRepoMgr.CleanUp()) + $$$ThrowOnError(tSC) + } } ClassMethod CheckModuleNamespace() As %Status @@ -2552,10 +2558,17 @@ ClassMethod CleanInstall(ByRef commandInfo) [ Internal ] // Indicating to commandInfo that this is a clean install command, not an install or load command. commandInfo will be passed to either Install() or Load() to continue performing the update. set commandInfo("data","cmd") = "ci" set commandInfo("data","CleanInstall") = 1 - set log = ##class(%IPM.General.HistoryTemp).UpdateInit(moduleName) + //set log = ##class(%IPM.General.HistoryTemp).UpdateInit(moduleName) - // Forward execution to install - do ..Install(.commandInfo, log) + // A path to a tarball is specified. Call ..Load() in order to load the newer version of the module. + set path = $$$GetModifier(pCommandInfo,"path") + if (path) { + set pCommandInfo("parameters","path") = path + do ..Load(.pCommandInfo, log) + } else { + // Forward execution to install + do ..Install(.commandInfo, log) + } } ClassMethod Reinstall(ByRef pCommandInfo) [ Internal ] diff --git a/tests/integration_tests/Test/PM/Integration/History.cls b/tests/integration_tests/Test/PM/Integration/History.cls index db9daf5dd..f78ae1710 100644 --- a/tests/integration_tests/Test/PM/Integration/History.cls +++ b/tests/integration_tests/Test/PM/Integration/History.cls @@ -535,6 +535,41 @@ Method TestUpdate() } } +Method TestCleanInstall() +{ + // Setup repo for "ci" test + set repoName = "lock-file-base" + do ##class(%IPM.Main).Shell("repo -n " _ repoName _ " -fs -path /home/irisowner/zpm/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-base-cases/") + + // Call zpm "ci" on a simple module + set moduleName = "lock-mod-a-no-deps" + set command = "ci " _ moduleName + set sc = ##class(%IPM.Main).Shell(command) + do $$$AssertStatusOK(sc, "Successfully installed " _ moduleName _ " from lock file") + + // get history + set rs = ##class(%IPM.General.History).GetHistory(,,1) + while rs.%Next() { + do $$$AssertTrue(rs.%Get("ID") '= "") + do $$$AssertEquals(rs.%Get("Action"), "ci") + do $$$AssertEquals(rs.%Get("Package"), moduleName) + do $$$AssertEquals(rs.%Get("Version_Major"), "1") + do $$$AssertEquals(rs.%Get("Version_Minor"), "0") + do $$$AssertEquals(rs.%Get("Version_Patch"), "0") + do $$$AssertEquals(rs.%Get("UserName"), "irisowner") + do $$$AssertTrue(rs.%Get("TimeStart") '= "") + do $$$AssertTrue(rs.%Get("TimeEnd") '= "") + do $$$AssertEquals(rs.%Get("Success"),1) + do $$$AssertEquals(rs.%Get("Committed"),1) + do $$$AssertEquals(rs.%Get("CommandString"), command) + do $$$AssertEquals(rs.%Get("SourceMoniker"), "filesystem") + do $$$AssertEquals(rs.%Get("SourceName"), repoName) + } + + // Remove this test's repo now that the test is over + do ##class(%IPM.Main).Shell("repo -n " _ repoName _ " -fs -path /home/irisowner/zpm/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-base-cases/") +} + /// Tests that history entries correctly record all phases of module operations. /// Verifies that: /// - Load and install operations record 5 phases: Initialize, Reload, Validate, Compile, Activate diff --git a/tests/integration_tests/Test/PM/Integration/LockFile.cls b/tests/integration_tests/Test/PM/Integration/LockFile.cls index b1e53a1ac..f7eadeda0 100644 --- a/tests/integration_tests/Test/PM/Integration/LockFile.cls +++ b/tests/integration_tests/Test/PM/Integration/LockFile.cls @@ -286,7 +286,7 @@ Method Test08Module2DependenciesSameTransient() try { do ..ExecuteGenericLockFileTest(..#ModuleK) } catch e { - do $$$AssertStatusOK(e.AsStatus(),"An exception occurred in Test09Module2DependenciesSameTransient.") + do $$$AssertStatusOK(e.AsStatus(),"An exception occurred in Test08Module2DependenciesSameTransient.") } } @@ -297,7 +297,47 @@ Method Test09ComplexNestedDependencies() try { do ..ExecuteGenericLockFileTest(..#ModuleL) } catch e { - do $$$AssertStatusOK(e.AsStatus(),"An exception occurred in Test10ComplexNestedDependencies.") + do $$$AssertStatusOK(e.AsStatus(),"An exception occurred in Test09ComplexNestedDependencies.") + } +} + +/// Installing from lock file by loading from a packaged tarball +/// Uses Module K +Method Test10InstallViaLoad() +{ + try { + set moduleName = ..#ModuleK + + set sc = ##class(%IPM.Main).Shell("install " _ moduleName) + do $$$AssertStatusOK(sc, "Successfully installed " _ moduleName) + + set packagePath = "/home/irisowner/zpm/tests/integration_tests/Test/PM/Integration/_data/lock-test/package/pack" + set sc = ##class(%IPM.Main).Shell("package " _ moduleName _ " -path " _ packagePath) + do $$$AssertStatusOK(sc, "Successfully packaged " _ moduleName) + + set sc = ##class(%IPM.Main).Shell("uninstall -r " _ moduleName) + do $$$AssertStatusOK(sc, "Successfully packaged " _ moduleName _ " and dependencies prior to load") + + do $$$LogMessage("GOING TO repo -list") + zpm "repo -list" + + set sc = ##class(%IPM.Main).Shell("repo -delete -name lock-file-base") + do $$$AssertStatusOK(sc, "Successfully removed repo lock-file-base prior to load") + set sc = ##class(%IPM.Main).Shell("repo -delete -name lock-file-edge") + do $$$AssertStatusOK(sc, "Successfully removed repo lock-file-edge prior to load") + + do $$$LogMessage("GOING TO repo -list") + zpm "repo -list" + + set sc = ##class(%IPM.Main).Shell("load -from-lockfile " _ packagePath _ ".tgz") + do $$$AssertStatusOK(sc, "Successfully loaded packaged module from its lock file") + + do $$$LogMessage("GOING TO repo -list") + zpm "repo -list" + } catch e { + do $$$LogMessage("GOING TO repo -list") + zpm "repo -list" + do $$$AssertStatusOK(e.AsStatus(),"An exception occurred in Test10InstallViaLoad.") } } From 405afad7c32a27c1aeb340bbdc3ee964e6d65d7f Mon Sep 17 00:00:00 2001 From: James Lechtner Date: Mon, 27 Apr 2026 16:35:02 -0400 Subject: [PATCH 7/8] Getting rid of "ci" command in place for "-from-lockfile" flag --- CHANGELOG.md | 2 +- src/cls/IPM/General/AbstractHistory.cls | 5 -- src/cls/IPM/General/LockFile.cls | 16 ++--- src/cls/IPM/Main.cls | 60 ++----------------- src/cls/IPM/Utils/Module.cls | 6 +- .../Test/PM/Integration/History.cls | 35 ----------- .../Test/PM/Integration/LockFile.cls | 21 ++----- 7 files changed, 23 insertions(+), 122 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93de081b2..dae2a66e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #992: Implement automatic history purge logic - #973: Enables CORS and JWT configuration for WebApplications in module.xml - #1110: Add `iriscli` and `ipm` container utility scripts that are auto-installed to `~/.local/bin/` and `~/bin/` so they work both inside and outside of containers (Unix/Linux only) -- #962: Adding zpm "ci" command to install from a lock file +- #962: Adding zpm -from-lockfile flag to install/load from a lock file ### Fixed - #1001: The `unmap` and `enable` commands will now only activate CPF merge once after all namespaces have been configured instead after every namespace diff --git a/src/cls/IPM/General/AbstractHistory.cls b/src/cls/IPM/General/AbstractHistory.cls index e54a4545b..beb32788f 100644 --- a/src/cls/IPM/General/AbstractHistory.cls +++ b/src/cls/IPM/General/AbstractHistory.cls @@ -66,11 +66,6 @@ ClassMethod InstallInit(Package As %IPM.DataType.ModuleName) As %IPM.General.Abs quit ..Init("install", Package) } -ClassMethod CleanInstallInit(Package As %IPM.DataType.ModuleName) As %IPM.General.AbstractHistory -{ - quit ..Init("ci", Package) -} - ClassMethod LoadInit(Package As %IPM.DataType.ModuleName = "") As %IPM.General.AbstractHistory { // Package name may not known at this point, so use a placeholder diff --git a/src/cls/IPM/General/LockFile.cls b/src/cls/IPM/General/LockFile.cls index c0e35b546..5085e1a3f 100644 --- a/src/cls/IPM/General/LockFile.cls +++ b/src/cls/IPM/General/LockFile.cls @@ -123,7 +123,11 @@ ClassMethod InstallFromLockFile( set verbose = $get(params("Verbose"), 0) set lockFilePath = ##class(%File).NormalizeFilename("module-lock.json", directory) - set lockFileJSON = ##class(%DynamicObject).%FromJSONFile(lockFilePath) + try { + set lockFileJSON = ##class(%DynamicObject).%FromJSONFile(lockFilePath) + } catch { + $$$ThrowStatus($$$ERROR($$$GeneralError,$$$FormatText("Unable to parse lock file JSON at path: %1", lockFilePath))) + } set repositories = lockFileJSON.%Get("repositories", {}) set dependencies = lockFileJSON.%Get("dependencies", {}) @@ -156,17 +160,15 @@ ClassMethod InstallFromLockFile( set version = depVals.version set repository = depVals.repository - // Call CleanInstall() on dependency modules but set flag "LockFileInstallStarted" so we don't try installing from the dependency module's lock file - set commandInfo = "ci" + // Call Install() on dependency modules but set flag "LockFileInstallStarted" so we don't try installing from the dependency module's lock file + set commandInfo = "install" set commandInfo("data", "Verbose") = verbose set commandInfo("parameters","module") = repository_"/"_depName set commandInfo("parameters", "version") = version set commandInfo("data", "LockFileInstallStarted") = 1 + set commandInfo("data","FromLockFile") = 1 - set commandInfo("data","cmd") = "ci" - set commandInfo("data","CleanInstall") = 1 - - do ##class(%IPM.Main).CleanInstall(.commandInfo) + do ##class(%IPM.Main).Install(.commandInfo) } } diff --git a/src/cls/IPM/Main.cls b/src/cls/IPM/Main.cls index 607baf841..1b3f217cc 100644 --- a/src/cls/IPM/Main.cls +++ b/src/cls/IPM/Main.cls @@ -292,8 +292,8 @@ load C:\module\root\path -env C:\path\to\env1.json;C:\path\to\env2.json - - + + @@ -420,7 +420,8 @@ install -env /path/to/env1.json;/path/to/env2.json example-package - + + @@ -795,26 +796,6 @@ generate /my/path -export 00000,PacketName2,IgnorePacket2^00000,PacketName3,Igno config get HistoryRetain - -Installs a module from a lock file - - Installs a module from its lock file. Will first install all listed repositories followed by dependency modules and then the base module. - - - - - ci mymodule 3.0.0 - - - - - - - - - - - } @@ -1103,8 +1084,6 @@ ClassMethod ShellInternal( do ..Information(.tCommandInfo) } elseif (tCommandInfo = "history") { do ..History(.tCommandInfo) - } elseif (tCommandInfo = "ci") { - do ..CleanInstall(.tCommandInfo) } } catch pException { if (pException.Code = $$$ERCTRLC) { @@ -2401,7 +2380,7 @@ ClassMethod LoadInternal( } set tTargetDirectory = $get(tTargetDirectory, tDirectoryName) - if ($get(pCommandInfo("data", "LoadFromLockFile"))) { + if ($get(pCommandInfo("data", "FromLockFile"))) { do ##class(%IPM.General.LockFile).InstallFromLockFile(tTargetDirectory, .tParams) } else { // When loading a module from a local folder, there might be a /.modules/ folder containining dependencies. @@ -2542,35 +2521,6 @@ ClassMethod Install( $$$ThrowOnError(log.Finalize($$$OK, devMode)) } -ClassMethod CleanInstall(ByRef commandInfo) [ Internal ] -{ - set moduleName = $get(commandInfo("parameters","module")) - set version = $get(commandInfo("parameters","version")) - set verbose = $get(commandInfo("data","Verbose")) - set log = ##class(%IPM.General.HistoryTemp).CleanInstallInit(moduleName) - - // TODO: Add "path"? (see Update() for more info of calling install v load) - - if verbose { - write !, "Going to run a clean install on "_moduleName - } - - // Indicating to commandInfo that this is a clean install command, not an install or load command. commandInfo will be passed to either Install() or Load() to continue performing the update. - set commandInfo("data","cmd") = "ci" - set commandInfo("data","CleanInstall") = 1 - //set log = ##class(%IPM.General.HistoryTemp).UpdateInit(moduleName) - - // A path to a tarball is specified. Call ..Load() in order to load the newer version of the module. - set path = $$$GetModifier(pCommandInfo,"path") - if (path) { - set pCommandInfo("parameters","path") = path - do ..Load(.pCommandInfo, log) - } else { - // Forward execution to install - do ..Install(.commandInfo, log) - } -} - ClassMethod Reinstall(ByRef pCommandInfo) [ Internal ] { // Overriding defaults in the "data" array: diff --git a/src/cls/IPM/Utils/Module.cls b/src/cls/IPM/Utils/Module.cls index f1472c3fb..7597f22ad 100644 --- a/src/cls/IPM/Utils/Module.cls +++ b/src/cls/IPM/Utils/Module.cls @@ -271,7 +271,7 @@ ClassMethod LoadModuleFromDirectory( { set tSC = $$$OK try { - if $get(pParams("CleanInstall"), 0) && '$get(pParams("LockFileInstallStarted"), 0) { + if $get(pParams("FromLockFile"), 0) && '$get(pParams("LockFileInstallStarted"), 0) { do ##class(%IPM.General.LockFile).InstallFromLockFile(pDirectory, .pParams) } set tVerbose = $get(pParams("Verbose")) @@ -1252,8 +1252,8 @@ ClassMethod LoadNewModule( } // If installing from a lock file, don't need to load dependencies since dependencies will be installed in order anyways - if ('$get(params("CleanInstall"), 0)) { - do ..LoadDependencies(tModule, .params) + if ('$get(params("FromLockFile"), 0)) { + do ..LoadDependencies(tModule,,.params) } diff --git a/tests/integration_tests/Test/PM/Integration/History.cls b/tests/integration_tests/Test/PM/Integration/History.cls index f78ae1710..db9daf5dd 100644 --- a/tests/integration_tests/Test/PM/Integration/History.cls +++ b/tests/integration_tests/Test/PM/Integration/History.cls @@ -535,41 +535,6 @@ Method TestUpdate() } } -Method TestCleanInstall() -{ - // Setup repo for "ci" test - set repoName = "lock-file-base" - do ##class(%IPM.Main).Shell("repo -n " _ repoName _ " -fs -path /home/irisowner/zpm/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-base-cases/") - - // Call zpm "ci" on a simple module - set moduleName = "lock-mod-a-no-deps" - set command = "ci " _ moduleName - set sc = ##class(%IPM.Main).Shell(command) - do $$$AssertStatusOK(sc, "Successfully installed " _ moduleName _ " from lock file") - - // get history - set rs = ##class(%IPM.General.History).GetHistory(,,1) - while rs.%Next() { - do $$$AssertTrue(rs.%Get("ID") '= "") - do $$$AssertEquals(rs.%Get("Action"), "ci") - do $$$AssertEquals(rs.%Get("Package"), moduleName) - do $$$AssertEquals(rs.%Get("Version_Major"), "1") - do $$$AssertEquals(rs.%Get("Version_Minor"), "0") - do $$$AssertEquals(rs.%Get("Version_Patch"), "0") - do $$$AssertEquals(rs.%Get("UserName"), "irisowner") - do $$$AssertTrue(rs.%Get("TimeStart") '= "") - do $$$AssertTrue(rs.%Get("TimeEnd") '= "") - do $$$AssertEquals(rs.%Get("Success"),1) - do $$$AssertEquals(rs.%Get("Committed"),1) - do $$$AssertEquals(rs.%Get("CommandString"), command) - do $$$AssertEquals(rs.%Get("SourceMoniker"), "filesystem") - do $$$AssertEquals(rs.%Get("SourceName"), repoName) - } - - // Remove this test's repo now that the test is over - do ##class(%IPM.Main).Shell("repo -n " _ repoName _ " -fs -path /home/irisowner/zpm/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-base-cases/") -} - /// Tests that history entries correctly record all phases of module operations. /// Verifies that: /// - Load and install operations record 5 phases: Initialize, Reload, Validate, Compile, Activate diff --git a/tests/integration_tests/Test/PM/Integration/LockFile.cls b/tests/integration_tests/Test/PM/Integration/LockFile.cls index f7eadeda0..6ee30bc59 100644 --- a/tests/integration_tests/Test/PM/Integration/LockFile.cls +++ b/tests/integration_tests/Test/PM/Integration/LockFile.cls @@ -205,7 +205,7 @@ Method Test06ModuleMultipleVersions() do $$$AssertStatusOK(sc, "Uninstalled "_moduleName) // Install module from lock file - set sc = ##class(%IPM.Main).Shell("ci "_moduleName) + set sc = ##class(%IPM.Main).Shell("install -from-lockfile "_moduleName) do $$$AssertStatusOK(sc, "Able to install "_moduleName_" v"_latestVersion_" from the lock file") // Confirm classes and dependencies get installed as well @@ -236,7 +236,7 @@ Method Test06ModuleMultipleVersions() do $$$AssertStatusOK(sc, "Uninstalled "_moduleName) // Install module from lock file - set sc = ##class(%IPM.Main).Shell("ci "_moduleName_" "_olderVersion) + set sc = ##class(%IPM.Main).Shell("install -from-lockfile "_moduleName_" "_olderVersion) do $$$AssertStatusOK(sc, "Able to install "_moduleName_" v"_olderVersion_" from the lock file") // Confirm classes and dependencies get installed as well @@ -272,8 +272,8 @@ Method Test07ModuleNoLockFile() do $$$AssertStatusOK(sc, "Uninstalled "_moduleName) // This should fail due to no lock file - set sc = ##class(%IPM.Main).Shell("ci "_moduleName) - do $$$AssertStatusNotOK(sc, "zpm ""ci"" fails on module "_moduleName_"due to not having a lock file") + set sc = ##class(%IPM.Main).Shell("install -from-lockfile "_moduleName) + do $$$AssertStatusNotOK(sc, "Installing from a lock file fails on module "_moduleName_"due to not having a lock file") } catch e { do $$$AssertStatusOK(e.AsStatus(),"An exception occurred in Test07ModuleNoLockFile.") } @@ -318,25 +318,14 @@ Method Test10InstallViaLoad() set sc = ##class(%IPM.Main).Shell("uninstall -r " _ moduleName) do $$$AssertStatusOK(sc, "Successfully packaged " _ moduleName _ " and dependencies prior to load") - do $$$LogMessage("GOING TO repo -list") - zpm "repo -list" - set sc = ##class(%IPM.Main).Shell("repo -delete -name lock-file-base") do $$$AssertStatusOK(sc, "Successfully removed repo lock-file-base prior to load") set sc = ##class(%IPM.Main).Shell("repo -delete -name lock-file-edge") do $$$AssertStatusOK(sc, "Successfully removed repo lock-file-edge prior to load") - do $$$LogMessage("GOING TO repo -list") - zpm "repo -list" - set sc = ##class(%IPM.Main).Shell("load -from-lockfile " _ packagePath _ ".tgz") do $$$AssertStatusOK(sc, "Successfully loaded packaged module from its lock file") - - do $$$LogMessage("GOING TO repo -list") - zpm "repo -list" } catch e { - do $$$LogMessage("GOING TO repo -list") - zpm "repo -list" do $$$AssertStatusOK(e.AsStatus(),"An exception occurred in Test10InstallViaLoad.") } } @@ -402,7 +391,7 @@ Method AssertInstallFromLockFileAsExpected( moduleName As %String, className As %String = "") { - set sc = ##class(%IPM.Main).Shell("ci "_moduleName) + set sc = ##class(%IPM.Main).Shell("install -from-lockfile "_moduleName) do $$$AssertStatusOK(sc, "Installed "_moduleName_" via the lock file successfully") set sc = $$$OK From 827ae62ab8aca31aafd4afde717823724a95555f Mon Sep 17 00:00:00 2001 From: James Lechtner Date: Mon, 27 Apr 2026 16:37:29 -0400 Subject: [PATCH 8/8] Forgot to remove ci reference from AbstractHistory value list --- src/cls/IPM/General/AbstractHistory.cls | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cls/IPM/General/AbstractHistory.cls b/src/cls/IPM/General/AbstractHistory.cls index beb32788f..10dd29641 100644 --- a/src/cls/IPM/General/AbstractHistory.cls +++ b/src/cls/IPM/General/AbstractHistory.cls @@ -8,8 +8,8 @@ Include (%IPM.Common, %IPM.Formatting) Class %IPM.General.AbstractHistory Extends %Persistent [ Abstract, NoExtent ] { -/// Action of this history record. Can be load, install, ci, uninstall or update -Property Action As %String(VALUELIST = ",load,install,ci,uninstall,update") [ Required ]; +/// Action of this history record. Can be load, install, uninstall or update +Property Action As %String(VALUELIST = ",load,install,uninstall,update") [ Required ]; /// Name of the package being logged. This is not necessarily required, e.g. when loading a nonexistent directory. Property Package As %IPM.DataType.ModuleName;