diff --git a/CHANGELOG.md b/CHANGELOG.md index 350420e2..b5978061 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Deprecated - +## [Unreleased - 0.9.2+snapshot] + +### Added +- #682 When downloading IPM via the `enable` command from a remote registry, allow user to pass in the registry name (or get the only existent one), instead of the deployment enabled registry. + +### Fixed +- #684 Fixed banner display issues in interactive `zpm` shell. +- #682 When enabling IPM in a namespace using local IPM caches, check for existence of `/lib/ipm/` beforing querying it. +- #682 Use more standard wording of mapping when enabling IPM +- #681 Convert specified namespaces to upper case for `enable` and `unmap` commands. +- #680 Always export static files (README.md, LICENSE, requirements.txt) if existent +- #678 Only update comment-flagged part of the language extension, allowing users to keep their custom code when upgrading +- #680, #683 Always export static files (README.md, LICENSE, requirements.txt, CHANGELOG.md) if existent + +### Security +- #697 When publishing modules, will get an status with error message (instead of just a boolean) in case of failures. + +## [0.9.1] - 2024-12-18 + +### Added +- #663 Added support for mapping of repository settings along with, or in addition to, IPM package and routines +- #663 Added functionality to always unmap repository settings when IPM package and routines are unmapped +- #663 Added support for unmapping of repository settings alone +- #663 Added support for `enable -community`, which resets repository settings to default and maps IPM along with repo settings globally + +### Fixed +- #663 Improved error output and instructions in the language extension when "zpm" is run from a namespace without IPM + ## [0.9.0] - 2024-12-16 ### Added diff --git a/README.md b/README.md index 223dc8bf..34408ef2 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,29 @@ -# ObjectScript Package Manager Client - ZPM +# InterSystems Package Manager - IPM -Helps to install ObjectScript classes and routines, globals, Embedded Python modules, CSP and Frontend packages, and any files into InterSystems IRIS published on the official [ZPM Registry](https://pm.community.intersystems.com/packages/-/all) or private ZPM registry of your own. +Helps to install ObjectScript classes and routines, globals, Embedded Python modules, CSP and Frontend packages, and any files into InterSystems IRIS published on the official [Registry](https://pm.community.intersystems.com/packages/-/all) or private registry of your own. ## Documentation * [The official documentation in the wiki](https://github.com/intersystems-community/zpm/wiki/) * [Articles on the InterSystems Developer Community](https://community.intersystems.com/tags/objectscript-package-manager-zpm) * [Videos on YouTube](https://www.youtube.com/playlist?list=PLKb2cBVphNQRcmxt4LtYDyLJEPfF4X4-4) +## Compatibility Note +With the release of IPM v0.9.0 on Dec 2024, IPM is no longer mapped across namespaces. +This is an intentional change so that users can have different IPM versions and configurations in different namespaces. +If you install IPM on an instance without the legacy 0.7.x version, IPM is only installed to the current namespace. + +* To retain the old behavior where %IPM routines and classes mapped across all namespaces, run `zpm "enable -map -globally`. This is automatically performed when upgrading from a legacy version and can be undone by running `zpm "unmap -globally"`. +* You can optionally choose to map IPM repositories across namespaces with `zpm "enable -map -repos -namespaces NS1,NS2,NS3` or `zpm "enable -map -repos -globally`. Repositories are only mapped if %IPM classes and routines are also mapped from the same namespace. +* As a convenience command, `zpm "enable -community"` will make IPM behave essentially the same as legacy versions (v0.7.x) by setting up the the community registry and maping %IPM routines and classes, as well IPM repository settings to all namespaces. ## Installing ObjectScript Package Manager Client: 0. Use one-liner in terminal call or programmatically: ``` -s r=##class(%Net.HttpRequest).%New(),r.Server="pm.community.intersystems.com",r.SSLConfiguration="ISC.FeatureTracker.SSL.Config" d r.Get("/packages/zpm/latest/installer"),$system.OBJ.LoadStream(r.HttpResponse.Data,"c") +s version="latest" s r=##class(%Net.HttpRequest).%New(),r.Server="pm.community.intersystems.com",r.SSLConfiguration="ISC.FeatureTracker.SSL.Config" d r.Get("/packages/zpm/"_version_"/installer"),$system.OBJ.LoadStream(r.HttpResponse.Data,"c") ``` +**If you want the legacy behavior of mapping IPM classes, routines, and repository settings to all namespaces, run `zpm "enable -community"` after installing IPM. See `zpm "help enable"` for details.** +**In a CI script, for deterministic behavior, you should replace version="latest" with the IPM version you wish to use.** OR: @@ -65,4 +75,4 @@ Here is the [alternative supported folder structure.](https://openexchange.inter ## Support and Collaboration ObjectScript Package Manager is a community supported project and thus open to collaboration via Pull Requests. -Issues and feature requests [are very welcome](https://github.com/intersystems-community/zpm/issues) \ No newline at end of file +Issues and feature requests [are very welcome](https://github.com/intersystems-community/zpm/issues) diff --git a/src/cls/IPM/Lifecycle/Base.cls b/src/cls/IPM/Lifecycle/Base.cls index 5c886f21..5b390b7c 100644 --- a/src/cls/IPM/Lifecycle/Base.cls +++ b/src/cls/IPM/Lifecycle/Base.cls @@ -1211,9 +1211,10 @@ Method %Export(ByRef pParams, ByRef pTargetDirectory As %String, Output pDepende } /// Always keep these files Set staticFiles = $ListBuild( - // "readme.md", - // "license", - // "requirements.txt", + "readme.md", + "changelog.md", + "license", + "requirements.txt", ) 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 85673db2..d4fd0635 100644 --- a/src/cls/IPM/Main.cls +++ b/src/cls/IPM/Main.cls @@ -680,33 +680,42 @@ generate /my/path -export 00000,PacketName2,IgnorePacket2^00000,PacketName3,Igno + + + enable -map -globally - - enable -v 0.3.4 -q -ns NS1,NS2,NS3 + + enable -v 0.3.4 -q -ns NS1,NS2,NS3 -remote registry - + enable -globally - + enable -v latest -globally - + enable -v latest -allow-upgrade NS1,NS2,NS3 + + enable -community + + Unmap %IPM package and routines in specified namespaces. Unmap %IPM package and routines in specified namespaces. Will Skip non-mapped namespaces. + If repository settings are mapped, will also unmap repository settings. + unmap -ns NS1,NS2,NS3 @@ -799,10 +808,15 @@ ClassMethod ShellInternal(pCommand As %String, Output pException As %Exception.A Set tOneCommand = 1 } do ##class(%IPM.Main).GetVersion("zpm",.out) + If $Get(out) = "" { + Set registryInfo = $$$FormattedLine($$$Cyan, "No registry configured") + } Else { + Set registryInfo = "Current registry "_$$$FormattedLine($$$Cyan, out) + } Set introMessageList = $ListBuild( "Welcome to the Package Manager Shell (ZPM). Version: "_$$$FormattedLine($$$Green, ..GetVersionModule("zpm")), "Enter q/quit to exit the shell. Enter ?/help to view available commands", - "Current registry "_$$$FormattedLine($$$Cyan,$g(out)) + registryInfo ) Set tInShell = 0 For { @@ -1236,11 +1250,12 @@ ClassMethod GetVersionModule(name, namespace = {$namespace}) { New $Namespace Set $Namespace=namespace - Do ..GetListModules(name,.list) - If $Data(list(name)) { - quit $ListGet(list(name),1) + Try { + Do ..GetListModules(,name,.list) + Return $ListGet(list(1), 2) + } Catch ex { + Return "" } - quit "" } /// @API.Method @@ -2786,7 +2801,6 @@ ClassMethod UpdateLanguageExtensionsOne(RoutineName As %String, pTestOnly As %Bo { Set tRtn = ##class(%Routine).%New(RoutineName) If ##class(%Routine).Exists(RoutineName) { - Set tEnded = 1 While 'tRtn.AtEnd { Set tLine = tRtn.ReadLine() If $Match(tLine,$$$STARTTAGREGEX) { @@ -2798,11 +2812,12 @@ ClassMethod UpdateLanguageExtensionsOne(RoutineName As %String, pTestOnly As %Bo For i=1:1:tGenLines { Set tRtnLines($Increment(tRtnLines)) = tGenLines(i) } - } + } Else { + // outside of the tags, just copy the lines to preserve users' custom code + Set tRtnLines($Increment(tRtnLines)) = tLine + } } - If 'tEnded { - Set tRtnLines($Increment(tRtnLines)) = $$$ENDTAGQ - } ElseIf 'pFound { + If 'pFound { For i=1:1:tGenLines { Set tRtnLines($Increment(tRtnLines)) = tGenLines(i) } @@ -2878,9 +2893,12 @@ ZPM(pArgs...) Set found = 0 While rs.%Next() { Set $Namespace = $Zstrip(rs.%Get("Nsp"), "<>WC") - If $System.CLS.IsMthd("%IPM.Main", "Shell") { + // Some I4H containers come with %IPM.Main but the "version" command doesn't work ?! + If $System.CLS.IsMthd("%IPM.Main", "Shell") && ($Namespace '= "HSLIB") && ($Namespace '= "HSSYS") { Write !, "Change namepace to one of the following to run the ""zpm"" command" Do ##class(%IPM.Main).Shell("version") + Write !, "If you want to map IPM globally, switch to one of the namespaces above and run: zpm ""enable -map -globally""." + Write !, "If you want to reset repository and map IPM globally along with repository settings, switch to one of the namespaces above and run: zpm ""enable -community""." Set found = 1 Quit } @@ -2915,6 +2933,19 @@ ClassMethod EnableIPM(ByRef pCommandInfo) Write !,($namespace)_"> "_$$$FormattedLine($$$Green,$$$IPMModuleName_" ")_modDef.VersionString } + If $$$HasModifier(pCommandInfo,"community") { + For cmd = "repo -delete-all", "repo -reset-defaults", "enable -map -repos -globally" { + Write !!, "RUNNING command: """, cmd, """" + Do ..ShellInternal(cmd, .exc) + If exc '= $$$NULLOREF { + Write !, $$$FormattedLine($$$Red, $$$FormatText("Error running command ""%1"" - %2", cmd, exc.DisplayString())) + } Else { + Write !, $$$FormattedLine($$$Green, $$$FormatText("Command ""%1"" finished successfull", cmd)) + } + } + Return + } + Set quiet = $$$HasModifier(pCommandInfo,"quiet") Set preview = $$$HasModifier(pCommandInfo,"preview") Set map = $$$HasModifier(pCommandInfo,"map") @@ -2922,7 +2953,10 @@ ClassMethod EnableIPM(ByRef pCommandInfo) Set localOnly = $$$HasModifier(pCommandInfo, "local-only") Set version = $$$GetModifier(pCommandInfo, "version") Set namespaces = $$$GetModifier(pCommandInfo, "namespaces") + Set namespaces = $$$ucase(namespaces) Set allowUpgrade = $$$HasModifier(pCommandInfo,"allow-upgrade") + Set mapRepos = $$$HasModifier(pCommandInfo,"repos") + Set remoteName = $$$GetModifier(pCommandInfo,"remote") Set useLocal = 1 // var to store the final decision of whether to use local manifest or get from server Set targetVersion = "" // var to store the final version of IPM to be installed Kill targetNamespaces // multi-dim array to store the final namespaces that need to install IPM @@ -2941,6 +2975,9 @@ ClassMethod EnableIPM(ByRef pCommandInfo) If map && 'globally && (namespaces = "") { $$$ThrowOnError($$$ERROR($$$GeneralError,"If mapping from the current namespace's routine database with -map, must specify either -globally or a list of namespaces with -ns")) } + If ('map) && (mapRepos) { + $$$ThrowOnError($$$ERROR($$$GeneralError,"Cannot specify -repos without -map")) + } If map { If globally { @@ -2959,13 +2996,15 @@ ClassMethod EnableIPM(ByRef pCommandInfo) } Else { Set namespaces = $ListFromString(namespaces) } + + // First try to map IPM itself Set pointer = 0 While $ListNext(namespaces,pointer,namespace) { Set namespace = $Zstrip(namespace, "<>WC") Set $Namespace = namespace If ..IPMInstalled() { If 'quiet || preview { - Write !,"Skipping "_namespace_" - IPM already installed." + Write !,"Skipping IPM mapping of "_namespace_" - IPM already installed." } Continue } @@ -2975,14 +3014,50 @@ ClassMethod EnableIPM(ByRef pCommandInfo) Continue } If 'quiet { - Write !,"Mapping %IPM package in "_namespace_" equivalently to "_initNamespace + Write !,"Mapping %IPM package in "_initNamespace_" equivalently to "_namespace } $$$ThrowOnError(##class(%IPM.Utils.Build).MapPackageEquivalently("%IPM",initNamespace,namespace)) If 'quiet { - Write !,"Mapping %IPM.* routines in "_namespace_" equivalently to "_initNamespace + Write !,"Mapping %IPM.* routines in "_initNamespace_" equivalently to "_namespace } $$$ThrowOnError(##class(%IPM.Utils.Build).MapRoutineEquivalently("%IPM.*",initNamespace,,namespace)) } + + // Then try to map repositories if -repos is specified + If mapRepos { + Do ..GetMapInfo(.isMappedFrom) + Set pointer = 0 + While $ListNext(namespaces,pointer,namespace) { + Set namespace = $ZStrip(namespace, "<>WC") + // If IPM is not mapped from source namespace, skip repo mapping + If $Get(isMappedFrom(namespace)) '= initNamespace { + If 'quiet || preview { + Write !,"Skipping repository mapping of "_namespace_" - IPM not mapped from source namespace." + } + Continue + } + // If repository are already present, also skip repo mapping avoid override + Set $Namespace = namespace + If $Data(^IPM.Repo.DefinitionD) \ 2 { + If 'quiet || preview { + Write !,"Skipping repository mapping of "_namespace_" - IPM repositories found." + } + Continue + } + Set $Namespace = initNamespace + If preview { + Write !,"Would add IPM repository mappings to "_namespace + Continue + } + If 'quiet { + Write !,"Mapping IPM repository in "_initNamespace_" equivalently to "_namespace + } + For suffix = "D", "S", "I" { + $$$ThrowOnError(##class(%IPM.Utils.Build).MapGlobalEquivalently("IPM.Repo.Definition"_suffix, initNamespace, namespace)) + } + } + } + Set $Namespace = initNamespace If preview { Write !,"Preview mode; no configuration changes were made." @@ -3003,23 +3078,25 @@ ClassMethod EnableIPM(ByRef pCommandInfo) Set sc = statement.%PrepareClassQuery("%File", "FileSet") $$$ThrowOnError(sc) // Valid IPM installation manifest should be of format: ipm-0.0.1.xml - Set resultSet = statement.%Execute(XMLDir, "ipm-*.xml") - If (resultSet.%SQLCODE < 0) { - $$$ThrowSQLIfError(resultSet.%SQLCODE,resultSet.%Message) - } - Kill ipmLocalArray // multi-dim array to store local ipm manifests; ipmLocalArray()= - Kill menuList // menu to let user choose which version of local ipm to install (if not in quiet mode and no version is specified) - While resultSet.%Next(.sc) { - $$$ThrowOnError(sc) - Set fileName = resultSet.%Get("Name") - If (resultSet.%Get("Type") = "F") { - // get the exact version string from filename - Set exactIPMVer = $Piece($Piece(fileName, "ipm-", 2, *), ".xml", 1, *-1) - Set ipmLocalArray(exactIPMVer) = fileName - Set menuList($Increment(menuList)) = exactIPMVer_" (local version)" + If ##class(%File).DirectoryExists(XMLDir) { + Set resultSet = statement.%Execute(XMLDir, "ipm-*.xml") + If (resultSet.%SQLCODE < 0) { + $$$ThrowSQLIfError(resultSet.%SQLCODE,resultSet.%Message) + } + Kill ipmLocalArray // multi-dim array to store local ipm manifests; ipmLocalArray()= + Kill menuList // menu to let user choose which version of local ipm to install (if not in quiet mode and no version is specified) + While resultSet.%Next(.sc) { + $$$ThrowOnError(sc) + Set fileName = resultSet.%Get("Name") + If (resultSet.%Get("Type") = "F") { + // get the exact version string from filename + Set exactIPMVer = $Piece($Piece(fileName, "ipm-", 2, *), ".xml", 1, *-1) + Set ipmLocalArray(exactIPMVer) = fileName + Set menuList($Increment(menuList)) = exactIPMVer_" (local version)" + } } + $$$ThrowOnError(sc) } - $$$ThrowOnError(sc) If ('$DATA(menuList) && localOnly) { $$$ThrowOnError($$$ERROR($$$GeneralError,"No ipm-.xml installer file is found locally in directory: "__XMLDir)) @@ -3065,7 +3142,7 @@ ClassMethod EnableIPM(ByRef pCommandInfo) Set localIPMCount = $Get(menuList, 0) If 'localOnly { // Add remote version options to menuList - Set server = ##class(%IPM.Repo.Remote.Definition).DeploymentServerOpen(1,,.sc) + Set server = ##class(%IPM.Repo.Remote.Definition).GetOne(remoteName, .sc) $$$ThrowOnError(sc) If $IsObject(server) { Set latestVersion = server.GetPackageService().GetLatestModuleVersion($$$IPMModuleName) @@ -3136,7 +3213,7 @@ ClassMethod EnableIPM(ByRef pCommandInfo) // 4. Now that we got all the namespaces that need to install IPM, do the actual installation If 'useLocal { - Set server = ##class(%IPM.Repo.Remote.Definition).DeploymentServerOpen(1,,.sc) + Set server = ##class(%IPM.Repo.Remote.Definition).GetOne(remoteName, .sc) $$$ThrowOnError(sc) If $IsObject(server) { Set packageService = server.GetPackageService() @@ -3145,7 +3222,7 @@ ClassMethod EnableIPM(ByRef pCommandInfo) Set ipmRef.Name = $$$IPMModuleName If (targetVersion = "latest") { // convert latest to semantic version so that it can be undestood by ModuleInfo class - Set server = ##class(%IPM.Repo.Remote.Definition).DeploymentServerOpen(1,,.sc) + Set server = ##class(%IPM.Repo.Remote.Definition).GetOne(remoteName, .sc) $$$ThrowOnError(sc) If $IsObject(server) { Set targetVersion = server.GetPackageService().GetLatestModuleVersion($$$IPMModuleName) @@ -3215,7 +3292,9 @@ ClassMethod UnmapIPM(ByRef pCommandInfo) { Set globally = $$$HasModifier(pCommandInfo,"globally") Set namespaces = $ListFromString($$$GetModifier(pCommandInfo,"namespaces"), ",") + Set namespaces = $$$ucase(namespaces) Set verbose = '$$$HasModifier(pCommandInfo,"quiet") + set reposOnly = $$$HasModifier(pCommandInfo,"repos-only") // Sanity check If (globally && (namespaces '= "")) { $$$ThrowOnError($$$ERROR($$$GeneralError,"Cannot specify namespaces and global unmap flag at the same time.")) @@ -3236,7 +3315,11 @@ ClassMethod UnmapIPM(ByRef pCommandInfo) } } If verbose { - Write !,"Will attempt to unmap %IPM package and routines from: "_ $ListToString(namespaces, ", ") + If reposOnly { + Write !,"Will attempt to unmap IPM repository settings from: "_ $ListToString(namespaces, ", ") + } Else { + Write !,"Will attempt to unmap IPM package, routines, and repository settings from: "_ $ListToString(namespaces, ", ") + } } // Gather namespaces where %IPM is mapped into @@ -3251,11 +3334,21 @@ ClassMethod UnmapIPM(ByRef pCommandInfo) } Continue } + If verbose { - Write !,"Unmapping %IPM package and routines from "_ns_" (mapped from "_src_")" + Write !,"Unmapping repository settings from "_ns_" (mapped from "_src_")" + } + For suffix = "D", "S", "I" { + $$$ThrowOnError(##class(%IPM.Utils.Module).RemoveGlobalMapping(ns, "IPM.Repo.Definition"_suffix)) + } + + If 'reposOnly { + If verbose { + Write !,"Unmapping IPM package and routines from "_ns_" (mapped from "_src_")" + } + $$$ThrowOnError(##class(%IPM.Utils.Module).RemovePackageMapping(ns, "%IPM")) + $$$ThrowOnError(##class(%IPM.Utils.Module).RemoveRoutineMapping(ns, "%IPM.*")) } - $$$ThrowOnError(##class(%IPM.Utils.Module).RemovePackageMapping(ns, "%IPM")) - $$$ThrowOnError(##class(%IPM.Utils.Module).RemoveRoutineMapping(ns, "%IPM.*")) } } diff --git a/src/cls/IPM/Repo/Definition.cls b/src/cls/IPM/Repo/Definition.cls index 8ed2d48f..0e981dc8 100644 --- a/src/cls/IPM/Repo/Definition.cls +++ b/src/cls/IPM/Repo/Definition.cls @@ -229,6 +229,29 @@ SELECT Name FROM %IPM_Repo.Definition ORDER BY %IPM_Repo.Definition_SortOrder(ID) DESC } +/// If a name is provided, return the repo of this type with the name +/// Otherwise, return the only repo of this type in the system +/// If there are multiple repos (or no repos) of this type, return an error +ClassMethod GetOne(name As %String = "", Output sc As %Status) As %IPM.Repo.Definition +{ + If $Get(name) '= "" { + Quit ..ServerDefinitionKeyOpen(name, , .sc) + } + Set tablename = $$$comClassKeyGet($classname(), $$$cCLASSsqlqualifiednameQ) + Set query = "SELECT id FROM " _ tablename // should be safe from SQL injection, since $classname() is safe + Set rs = ##class(%SQL.Statement).%ExecDirect(, query) + $$$ThrowSQLIfError(rs.%SQLCODE, rs.%Message) + Set list = "" + While rs.%Next() { + Set list = list _ $lb(rs.%Get("id")) + } + If $ListLength(list) = 1 { + Quit ..%OpenId($ListGet(list, 1), , .sc) + } + Set sc = $$$ERROR($$$GeneralError, "Unable to find a unique repo of type " _ $CLASSNAME()) + Quit "" +} + Storage Default { diff --git a/tests/integration_tests/Test/PM/Integration/StaticFileExport.cls b/tests/integration_tests/Test/PM/Integration/StaticFileExport.cls new file mode 100644 index 00000000..44e2ce34 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/StaticFileExport.cls @@ -0,0 +1,29 @@ +Include %IPM.Common + +Class Test.PM.Integration.StaticFileExport Extends Test.PM.Integration.Base +{ + +Method TestWSGIApp() +{ + Set tSC = $$$OK + Try { + Set tTestRoot = ##class(%File).NormalizeDirectory($Get(^UnitTestRoot)) + set tModuleDir = ##class(%File).NormalizeDirectory(##class(%File).GetDirectory(tTestRoot)_"/_data/static-file-export-test/") + Set tSC = ##class(%IPM.Main).Shell("load -verbose " _ tModuleDir) + Do $$$AssertStatusOK(tSC,"Module successfully. " _ tModuleDir) + Set exportDir = ##class(%File).NormalizeDirectory($$$FileTempDirSys) + Set tSC = ##class(%IPM.Main).Shell("static-file-export-test package -DPath="_exportDir) + Do $$$AssertStatusOK(tSC,"Exported to directory " _ exportDir _ " successfully.") + Do $$$AssertTrue(##class(%File).DirectoryExists(exportDir)) + For file = "LICENSE","README.md","requirements.txt","CHANGELOG.md" { + Set tFile = ##class(%File).NormalizeFilename(file, exportDir) + If '$$$AssertTrue(##class(%File).Exists(tFile)) { + Do $$$LogMessage("File "_tFile_" does not exist.") + } + } + } Catch e { + Do $$$AssertStatusOK(e.AsStatus(), "An exception occurred.") + } +} + +} diff --git a/tests/integration_tests/Test/PM/Integration/_data/static-file-export-test/CHANGELOG.md b/tests/integration_tests/Test/PM/Integration/_data/static-file-export-test/CHANGELOG.md new file mode 100644 index 00000000..fd4cc0b0 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/static-file-export-test/CHANGELOG.md @@ -0,0 +1 @@ +This is a changelog \ No newline at end of file diff --git a/tests/integration_tests/Test/PM/Integration/_data/static-file-export-test/LICENSE b/tests/integration_tests/Test/PM/Integration/_data/static-file-export-test/LICENSE new file mode 100644 index 00000000..8e7b50d6 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/static-file-export-test/LICENSE @@ -0,0 +1 @@ +This is a license \ No newline at end of file diff --git a/tests/integration_tests/Test/PM/Integration/_data/static-file-export-test/README.md b/tests/integration_tests/Test/PM/Integration/_data/static-file-export-test/README.md new file mode 100644 index 00000000..1e6db8dd --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/static-file-export-test/README.md @@ -0,0 +1 @@ +This is a readme \ No newline at end of file diff --git a/tests/integration_tests/Test/PM/Integration/_data/static-file-export-test/module.xml b/tests/integration_tests/Test/PM/Integration/_data/static-file-export-test/module.xml new file mode 100644 index 00000000..e9a9bf1b --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/static-file-export-test/module.xml @@ -0,0 +1,12 @@ + + + + + static-file-export-test + 1.0.0 + Test whether static files (readme.md, changelog.md, licence, requirements.txt) are properly exported + module + Module installed successfully! + + + diff --git a/tests/integration_tests/Test/PM/Integration/_data/static-file-export-test/requirements.txt b/tests/integration_tests/Test/PM/Integration/_data/static-file-export-test/requirements.txt new file mode 100644 index 00000000..663bd1f6 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/static-file-export-test/requirements.txt @@ -0,0 +1 @@ +requests \ No newline at end of file diff --git a/tests/unit_tests/Test/PM/Unit/LanguageExtension/Abstract.cls b/tests/unit_tests/Test/PM/Unit/LanguageExtension/Abstract.cls new file mode 100644 index 00000000..0460bdac --- /dev/null +++ b/tests/unit_tests/Test/PM/Unit/LanguageExtension/Abstract.cls @@ -0,0 +1,171 @@ +Include %IPM.Common + +Class Test.PM.Unit.LanguageExtension.Abstract Extends %UnitTest.TestCase +{ + +/// Controls whether the test case will be run. Only the Abstract case should have this set to 1. +Parameter IsAbstractTestCase As Boolean = 1; + +/// Name of the routine to be tested. +Parameter TestRoutine = "DummyRoutineForIPMTest.MAC"; + +/// Constant string to be used as custom code in the test cases. +Parameter CustomCode = " // This is some custom code"; + +/// Run by RunTest immediately before each test method in the test class is run.
+///
+///
testname +///
Name of the test to be run. Required. +///
+Method OnBeforeOneTest(testname As %String) As %Status +{ + If ##class(%Routine).Exists(..#TestRoutine) { + Quit ##class(%Routine).Delete(..#TestRoutine) + } + Quit $$$OK +} + +/// Run by RunTest immediately after each test method in the test class is run.
+///
+///
testname +///
Name of the test to be run. Required. +///
+Method OnAfterOneTest(testname As %String) As %Status +{ + Quit ..OnBeforeOneTest(testname) +} + +ClassMethod AppendLangExt1(ByRef content) +{ + Set content($Increment(content)) = "ZPMDUMMYLANGUAGEEXTENSION(pArgs...)" + Set content($Increment(content)) = " Set x = 1" +} + +ClassMethod AppendLangExt2(ByRef content) +{ + Set content($Increment(content)) = "ZPMDUMMYLANGUAGEEXTENSION(pArgs...)" + Set content($Increment(content)) = " Set y = 2" +} + +ClassMethod AppendLegacyStart(ByRef content) +{ + Set content($Increment(content)) = $Replace($$$STARTTAGQ, "%IPM.Main", "%ZPM.PackageManager") +} + +ClassMethod AppendLegacyEnd(ByRef content) +{ + Set content($Increment(content)) = $Replace($$$ENDTAGQ, "%IPM.Main", "%ZPM.PackageManager") +} + +ClassMethod AppendStart(ByRef content) +{ + Set content($Increment(content)) = $$$STARTTAGQ +} + +ClassMethod AppendEnd(ByRef content) +{ + Set content($Increment(content)) = $$$ENDTAGQ +} + +ClassMethod AppendContent(ByRef dest, ByRef src) +{ + For i = 1:1:$Get(src) { + Set dest($Increment(dest)) = src(i) + } +} + +/// Open TestRoutine, clear it, and write to it. The input should be a multi-dimensional array. +ClassMethod WriteToRoutine(ByRef input) +{ + // Creating and saving an empty routine causes the routine to contain a whitespace, which causes tests to fail. + If '$Data(input) { + Quit + } + Set routine = ##class(%Routine).%New(..#TestRoutine) + Do routine.Clear() + For i = 1:1:$Get(input) { + Do routine.WriteLine(input(i)) + } + $$$ThrowOnError(routine.Save()) +} + +/// Get the content of TestRoutine as a multi-dimensional array +ClassMethod ReadFromRoutine(Output output) +{ + Kill output + + If '##class(%Routine).Exists(..#TestRoutine) { + Quit + } + Set routine = ##class(%Routine).%New(..#TestRoutine) + While ('routine.AtEnd) { + Set output($Increment(output)) = routine.ReadLine() + } +} + +ClassMethod CompareContents(ByRef content1, ByRef content2) As %Boolean +{ + If $Data(content1) '= $Data(content2) { + Quit 0 + } + If $Get(content1) '= $Get(content2) { + Quit 0 + } + For i = 1:1:$Get(content1) { + If content1(i) '= content2(i) { + Return 0 + } + } + Return 1 +} + +ClassMethod ContentToString(ByRef content, LineSep As %String = "\n") As %String +{ + Set str = "" + For i = 1:1:$Get(content) { + Set str = str _ content(i) _ LineSep + } + Quit str +} + +Method TestLanguageExtension() +{ + If ..#IsAbstractTestCase { + Do $$$AssertSkipped($classname() _ "is an abstract test case") + Quit + } + // Get initial content and write to it + Do ..GetInitial(.initialContent) + Do ..WriteToRoutine(.initialContent) + + // Update language extension + Do ..AppendStart(.routineContent) + Do ..AppendLangExt2(.routineContent) + Do ..AppendEnd(.routineContent) + Do ##class(%IPM.Main).UpdateLanguageExtensionsOne(..#TestRoutine, 0, .found, .routineContent) + + // Get expected content, and expected found + Do ..GetExpected(.expectedContent, .expectedFound) + + // Compare against routine content + Do ..ReadFromRoutine(.outputContent) + Do $$$AssertEquals((''found), (''expectedFound)) + If '$$$AssertTrue(..CompareContents(.outputContent, .expectedContent)) { + Do $$$LogMessage("Output and expected content do not match !!!") + Do $$$LogMessage("Output : " _ ..ContentToString(.outputContent)) + Do $$$LogMessage("Expected : " _ ..ContentToString(.expectedContent)) + } +} + +/// Construct the initial content of the routine. Could be empty. +ClassMethod GetInitial(Output content) +{ +} + +/// Construct the expected content of the routine after the language extension is updated. +/// Also construct the expected "found" value indicating if an older language extension was found. +ClassMethod GetExpected(Output content, Output found) +{ +} + +} diff --git a/tests/unit_tests/Test/PM/Unit/LanguageExtension/FreshInstall.cls b/tests/unit_tests/Test/PM/Unit/LanguageExtension/FreshInstall.cls new file mode 100644 index 00000000..25a7bee8 --- /dev/null +++ b/tests/unit_tests/Test/PM/Unit/LanguageExtension/FreshInstall.cls @@ -0,0 +1,23 @@ +/// Simulate a fresh install of the language extension, assuming the routine is nonexistent at the time of install. +Class Test.PM.Unit.LanguageExtension.FreshInstall Extends Test.PM.Unit.LanguageExtension.Abstract +{ + +Parameter IsAbstractTestCase As Boolean = 0; + +/// Construct the expected content of the routine after the language extension is updated. +/// Also construct the expected "found" value indicating if an older language extension was found. +ClassMethod GetExpected(Output content, Output found) +{ + Set found = 0 + + Do ..AppendStart(.content) + Do ..AppendLangExt2(.content) + Do ..AppendEnd(.content) +} + +/// Construct the initial content of the routine. Could be empty. +ClassMethod GetInitial(Output content) +{ +} + +} diff --git a/tests/unit_tests/Test/PM/Unit/LanguageExtension/FreshInstallWithCustomCode.cls b/tests/unit_tests/Test/PM/Unit/LanguageExtension/FreshInstallWithCustomCode.cls new file mode 100644 index 00000000..a9efcba2 --- /dev/null +++ b/tests/unit_tests/Test/PM/Unit/LanguageExtension/FreshInstallWithCustomCode.cls @@ -0,0 +1,25 @@ +/// Simulate a fresh install of the language extension, assuming the routine contains only custom code at the time of install. +Class Test.PM.Unit.LanguageExtension.FreshInstallWithCustomCode Extends Test.PM.Unit.LanguageExtension.Abstract +{ + +Parameter IsAbstractTestCase As Boolean = 0; + +/// Construct the expected content of the routine after the language extension is updated. +/// Also construct the expected "found" value indicating if an older language extension was found. +ClassMethod GetExpected(Output content, Output found) +{ + Set found = 0 + + Set content($Increment(content)) = ..#CustomCode + Do ..AppendStart(.content) + Do ..AppendLangExt2(.content) + Do ..AppendEnd(.content) +} + +/// Construct the initial content of the routine. Could be empty. +ClassMethod GetInitial(Output content) +{ + Set content($Increment(content)) = ..#CustomCode +} + +} diff --git a/tests/unit_tests/Test/PM/Unit/LanguageExtension/IllFormedTags.cls b/tests/unit_tests/Test/PM/Unit/LanguageExtension/IllFormedTags.cls new file mode 100644 index 00000000..b0584b45 --- /dev/null +++ b/tests/unit_tests/Test/PM/Unit/LanguageExtension/IllFormedTags.cls @@ -0,0 +1,28 @@ +/// Ill-formed test case with only start tag but no end tag. +Class Test.PM.Unit.LanguageExtension.IllFormedTags Extends Test.PM.Unit.LanguageExtension.Abstract +{ + +Parameter IsAbstractTestCase As Boolean = 0; + +/// Construct the expected content of the routine after the language extension is updated. +/// Also construct the expected "found" value indicating if an older language extension was found. +ClassMethod GetExpected(Output content, Output found) +{ + Set found = 1 + + Set content($Increment(content)) = ..#CustomCode + Do ..AppendStart(.content) + Do ..AppendLangExt2(.content) + Do ..AppendEnd(.content) +} + +/// Construct the initial content of the routine. Could be empty. +ClassMethod GetInitial(Output content) +{ + Set content($Increment(content)) = ..#CustomCode + Do ..AppendStart(.content) + Do ..AppendLangExt1(.content) + Set content($Increment(content)) = ..#CustomCode +} + +} diff --git a/tests/unit_tests/Test/PM/Unit/LanguageExtension/UpgradeFromLegacy.cls b/tests/unit_tests/Test/PM/Unit/LanguageExtension/UpgradeFromLegacy.cls new file mode 100644 index 00000000..21a0498a --- /dev/null +++ b/tests/unit_tests/Test/PM/Unit/LanguageExtension/UpgradeFromLegacy.cls @@ -0,0 +1,26 @@ +/// Simulate upgrading from an older version of the language extension, assuming the routine contains only the older language extension. +Class Test.PM.Unit.LanguageExtension.UpgradeFromLegacy Extends Test.PM.Unit.LanguageExtension.Abstract +{ + +Parameter IsAbstractTestCase As Boolean = 0; + +/// Construct the expected content of the routine after the language extension is updated. +/// Also construct the expected "found" value indicating if an older language extension was found. +ClassMethod GetExpected(Output content, Output found) +{ + Set found = 1 + + Do ..AppendStart(.content) + Do ..AppendLangExt2(.content) + Do ..AppendEnd(.content) +} + +/// Construct the initial content of the routine. Could be empty. +ClassMethod GetInitial(Output content) +{ + Do ..AppendStart(.content) + Do ..AppendLangExt1(.content) + Do ..AppendEnd(.content) +} + +} diff --git a/tests/unit_tests/Test/PM/Unit/LanguageExtension/UpgradeFromLegacyWithCustomCode.cls b/tests/unit_tests/Test/PM/Unit/LanguageExtension/UpgradeFromLegacyWithCustomCode.cls new file mode 100644 index 00000000..bc40ef9d --- /dev/null +++ b/tests/unit_tests/Test/PM/Unit/LanguageExtension/UpgradeFromLegacyWithCustomCode.cls @@ -0,0 +1,30 @@ +/// Simulate upgrading from an older version of the language extension, assuming the routine contains both the older language extension and some custom code. +Class Test.PM.Unit.LanguageExtension.UpgradeFromLegacyWithCustomCode Extends Test.PM.Unit.LanguageExtension.Abstract +{ + +Parameter IsAbstractTestCase As Boolean = 0; + +/// Construct the expected content of the routine after the language extension is updated. +/// Also construct the expected "found" value indicating if an older language extension was found. +ClassMethod GetExpected(Output content, Output found) +{ + Set found = 1 + + Set content($Increment(content)) = ..#CustomCode + Do ..AppendStart(.content) + Do ..AppendLangExt2(.content) + Do ..AppendEnd(.content) + Set content($Increment(content)) = ..#CustomCode +} + +/// Construct the initial content of the routine. Could be empty. +ClassMethod GetInitial(Output content) +{ + Set content($Increment(content)) = ..#CustomCode + Do ..AppendStart(.content) + Do ..AppendLangExt1(.content) + Do ..AppendEnd(.content) + Set content($Increment(content)) = ..#CustomCode +} + +}