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 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..3a0f7230d 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,88 @@ 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) + 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 + + // 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) + } +} + +/// 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/Lifecycle/Base.cls b/src/cls/IPM/Lifecycle/Base.cls index 8fef53d56..05e3c8a98 100644 --- a/src/cls/IPM/Lifecycle/Base.cls +++ b/src/cls/IPM/Lifecycle/Base.cls @@ -1227,6 +1227,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 5fce52dfa..3b32b55ce 100644 --- a/src/cls/IPM/Main.cls +++ b/src/cls/IPM/Main.cls @@ -791,6 +791,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 + + + + + + + + + + } @@ -1031,8 +1050,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) { @@ -2396,12 +2417,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.")) @@ -2415,10 +2435,32 @@ ClassMethod Install( } } } catch ex { - $$$ThrowOnError(log.Finalize(ex.AsStatus(), devMode)) - throw ex - } - $$$ThrowOnError(log.Finalize($$$OK, 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) } 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..0de8a7f9d 100644 --- a/src/cls/IPM/Repo/Oras/Definition.cls +++ b/src/cls/IPM/Repo/Oras/Definition.cls @@ -140,11 +140,34 @@ 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") + 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 d259b39a0..98eec5fd7 100644 --- a/src/cls/IPM/Repo/Remote/Definition.cls +++ b/src/cls/IPM/Repo/Remote/Definition.cls @@ -132,6 +132,29 @@ 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") + + // 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() { return ..#MONIKERALIAS @@ -141,7 +164,6 @@ XData LockFileMapping { - diff --git a/src/cls/IPM/Utils/Module.cls b/src/cls/IPM/Utils/Module.cls index b99970acb..8c771a0c6 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,! @@ -948,7 +951,6 @@ ClassMethod GetModuleNameFromXML( /// 1 /// /// ``` -/// /// Returns results as multidimensional array ClassMethod GetModuleDefaultsFromXML( pDirectory As %String, @@ -1205,7 +1207,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) diff --git a/tests/integration_tests/Test/PM/Integration/LockFile.cls b/tests/integration_tests/Test/PM/Integration/LockFile.cls index 15d96fa99..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"; @@ -85,10 +81,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 +92,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 +103,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,17 +114,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 +/// Test with all repository types: Filesystem, ORAS, & Remote /// Uses Module G -Method Test05ModuleDependenciesAllRepositoryTypes() +Method Test05AllRepositoryTypes() { set sc = $$$OK try { @@ -149,7 +141,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" @@ -167,19 +159,23 @@ Method Test05ModuleDependenciesAllRepositoryTypes() 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 - set moduleName = "lock-mod-oras" - do ..AssertInstallCreatesLockFileAsExpected(moduleName) + // 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) - // 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") 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 +200,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 + 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", "MethodB") + + // Uninstall module before testing with other version + set sc = ##class(%IPM.Main).Shell("uninstall -r "_moduleName) + do $$$AssertStatusOK(sc, "Uninstalled "_moduleName) @@ -224,22 +232,32 @@ 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 + 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") } catch e { - do $$$AssertStatusOK(e.AsStatus(),"An exception occurred in Module2Dependencies1Transient.") + do $$$AssertStatusOK(e.AsStatus(),"An exception occurred in 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() { 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,43 +266,63 @@ 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) + + // 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 Module2Dependencies1Transient.") + do $$$AssertStatusOK(e.AsStatus(),"An exception occurred in Test07ModuleNoLockFile.") } } -/// Try install on a lock file that lists dependencies out of order -/// Uses Module J -Method Test08ModuleDependenciesOutOfOrder() -{ - // TODO: Implement with zpm "ci" addition -} - /// 2 dependencies for a module have the same transient to add to the lock file /// Uses Module K -Method Test09Module2DependenciesSameTransient() +Method Test08Module2DependenciesSameTransient() { 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.") } } /// 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 { - 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, + className 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, className) +} + +/// 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 +348,35 @@ 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) + 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, + className As %String = "") +{ + set sc = ##class(%IPM.Main).Shell("ci "_moduleName) + do $$$AssertStatusOK(sc, "Installed "_moduleName_" via the lock file successfully") + + set sc = $$$OK + try { + 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() + } + 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-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/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/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-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-g-all-repo-types/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 new file mode 100644 index 000000000..7d0d60846 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-edge-cases/lock-mod-g-all-repo-types/module-lock.json @@ -0,0 +1,70 @@ +{ + "name": "lock-mod-g-all-repo-types", + "version": "3.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 + }, + "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", + "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/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..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 @@ -7,15 +7,15 @@ - lock-mod-remote + lock-mod-b ^1.0.0 - lock-mod-oras + lock-mod-remote ^1.0.0 - lock-mod-c-2-deps-0-transient + lock-mod-oras ^1.0.0 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/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/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 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..222396971 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-other-repos/lock-mod-oras/module-lock.json @@ -0,0 +1,56 @@ +{ + "name": "lock-mod-oras", + "version": "1.0.0", + "repository": "lock-file-oras", + "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-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", + "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-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-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..bf13209a2 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/lock-test/mods-other-repos/lock-mod-remote/module-lock.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-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