From eb52948b081d106b1afff76b69ad6c7861f15148 Mon Sep 17 00:00:00 2001 From: Carolina BorbonMiranda Date: Tue, 12 May 2026 17:51:24 -0400 Subject: [PATCH 1/8] dependency graph now includes information about scoped dependencies --- src/cls/IPM/Storage/Module.cls | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/cls/IPM/Storage/Module.cls b/src/cls/IPM/Storage/Module.cls index 05748a45..4a225143 100644 --- a/src/cls/IPM/Storage/Module.cls +++ b/src/cls/IPM/Storage/Module.cls @@ -159,12 +159,12 @@ Method HandleAllUpdateSteps( } /// For a given version, seed or execute update steps listed in the module's update package version class -/// +/// /// If seeding: For only steps that don't have a TimeStampEnd value or have an errored status, set it's TimeStampEnd to the current time -/// +/// /// If executing: Only start running steps at the first one found not to have a TimeStampEnd value or if an errored step is found /// After finding this step, run it and all subsequent steps, regardless of if they have been run or not. -/// +/// /// newStepToApply - Gets marked as true to tell us to run all remaining steps in the list, whether or not they have been run/seeded before /// Can carry this over to subsequent calls by passing the newStepToApply variable to those HandleUpdateStepsFromList() calls Method HandleUpdateStepsFromList( @@ -1109,7 +1109,7 @@ Method ProcessSingleDependencyIterative( } set sc = ##class(%IPM.Utils.Module).GetRequiredVersionExpression( - pDep.Name,otherDepsList,,.installedReqExpr,.installedConstraintList + pDep.Name,otherDepsList,.installedReqExpr,.installedConstraintList ) $$$ThrowOnError(sc) set searchExpr = searchExpr.And(installedReqExpr) @@ -1121,7 +1121,7 @@ Method ProcessSingleDependencyIterative( set serverName = "" set version = "" if $data(pDependencyGraph(pDep.Name),depInfo) { - set $listbuild(previousDepth,serverName,version) = depInfo + set $listbuild(previousDepth,serverName,version,scope) = depInfo } // Check if installed version satisfies requirements @@ -1133,7 +1133,7 @@ Method ProcessSingleDependencyIterative( localObj.Version.Satisfies(searchExpr) && ((version = "") || (version = localObj.VersionString)) if installedVersionValid && '(localObj.Version.IsSnapshot() && pForceSnapshotReload) { - set pDependencyGraph(pDep.Name) = $listbuild(pDepth,"",localObj.VersionString,pDep.DisplayName) + set pDependencyGraph(pDep.Name) = $listbuild(pDepth,"",localObj.VersionString,pDep.DisplayName,pDep.Scope) set pDependencyGraph(pDep.Name,pParentInfo) = pDep.VersionString // Add to work queue for next depth @@ -1195,7 +1195,7 @@ Method ProcessSingleDependencyIterative( } set pDependencyGraph(pDep.Name) = $listbuild( - pDepth, qualifiedReference.ServerName, moduleObj.VersionString, qualifiedReference.Deployed, qualifiedReference.PlatformVersion,pDep.DisplayName + pDepth, qualifiedReference.ServerName, moduleObj.VersionString, qualifiedReference.Deployed, qualifiedReference.PlatformVersion,pDep.DisplayName,pDep.Scope ) set pDependencyGraph(pDep.Name,pParentInfo) = pDep.VersionString @@ -1230,7 +1230,7 @@ Method ProcessSingleDependencyIterative( // occurs if needed. set depth = $select(previousDepth=0:pDepth,previousDepth>pDepth:previousDepth,1:pDepth) set dependencyGraph(pDep.Name) = $listbuild( - depth,qualifiedReference.ServerName,moduleObj.VersionString,pDep.DisplayName + depth,qualifiedReference.ServerName,moduleObj.VersionString,pDep.DisplayName,pDep.Scope ) set dependencyGraph(pDep.Name,pParentInfo) = pDep.VersionString @@ -1435,7 +1435,7 @@ Method OverrideLifecycleClassSet(pValue As %Dictionary.Classname) As %Status /// This callback method is invoked by the %New method to /// provide notification that a new instance of an object is being created. -/// +/// ///

If this method returns an error then the object will not be created. ///

It is passed the arguments provided in the %New call. /// When customizing this method, override the arguments with whatever variables and types you expect to receive from %New(). @@ -1451,7 +1451,7 @@ Method %OnNew() As %Status [ Private, ServerOnly = 1 ] /// This callback method is invoked by the %Open method to /// provide notification that the object specified by oid is being opened. -/// +/// ///

If this method returns an error then the object will not be opened. Method %OnOpen() As %Status [ Private, ServerOnly = 1 ] { @@ -1481,7 +1481,7 @@ Method %OnOpen() As %Status [ Private, ServerOnly = 1 ] /// This callback method is invoked by the %ValidateObject method to /// provide notification that the current object is being validated. -/// +/// ///

If this method returns an error then %ValidateObject will fail. Method %OnValidateObject() As %Status [ Private, ServerOnly = 1 ] { @@ -1570,7 +1570,7 @@ Method %OnValidateObject() As %Status [ Private, ServerOnly = 1 ] /// either because %Save() was invoked on this object or on an object that references this object. /// %OnAddToSaveSet can modify the current object. It can also add other objects to the current /// SaveSet by invoking %AddToSaveSet or remove objects by calling %RemoveFromSaveSet. -/// +/// ///

If this method returns an error status then %Save() will fail and the transaction /// will be rolled back. Method %OnAddToSaveSet( @@ -1605,7 +1605,7 @@ Method %OnAddToSaveSet( } /// Get an instance of an XML enabled class.

-/// +/// /// You may override this method to do custom processing (such as initializing /// the object instance) before returning an instance of this class. /// However, this method should not be called directly from user code.
@@ -1836,9 +1836,9 @@ Method %Evaluate( /// This callback method is invoked by the %Save method to /// provide notification that the object is being saved. It is called before /// any data is written to disk. -/// +/// ///

insert will be set to 1 if this object is being saved for the first time. -/// +/// ///

If this method returns an error then the call to %Save will fail. Method %OnBeforeSave(insert As %Boolean) As %Status [ Private, ServerOnly = 1 ] { From 8b286c7a2873e1a24b51648106754e069d7c5792 Mon Sep 17 00:00:00 2001 From: Carolina BorbonMiranda Date: Tue, 12 May 2026 17:53:31 -0400 Subject: [PATCH 2/8] fix typo, and add changelog --- CHANGELOG.md | 1 + src/cls/IPM/Storage/Module.cls | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb8e4a72..175c5827 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #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) - #971: Adds structured test output formats (JSON, YAML, Toon). Use `-f ` for a one-shot override or `config set TestReportFormat ` for a persistent default. Without either, legacy output is shown. Also adds `-output-file` for writing results to a file (including JUnit XML via `.xml` extension) and improves `-quiet` to suppress build noise. +- #1152: Adds information about scoped dependencies in the output array of BuildDependencyGraph() ### Fixed - #964: Fix poor error handling on some install failures due to incorrect error message variable in embedded SQL diff --git a/src/cls/IPM/Storage/Module.cls b/src/cls/IPM/Storage/Module.cls index 4a225143..f7844b7f 100644 --- a/src/cls/IPM/Storage/Module.cls +++ b/src/cls/IPM/Storage/Module.cls @@ -1109,7 +1109,7 @@ Method ProcessSingleDependencyIterative( } set sc = ##class(%IPM.Utils.Module).GetRequiredVersionExpression( - pDep.Name,otherDepsList,.installedReqExpr,.installedConstraintList + pDep.Name,otherDepsList,,.installedReqExpr,.installedConstraintList ) $$$ThrowOnError(sc) set searchExpr = searchExpr.And(installedReqExpr) From f171606d71128975d0e357a6ab4993169bd49f97 Mon Sep 17 00:00:00 2001 From: Carolina BorbonMiranda Date: Mon, 18 May 2026 13:09:24 -0400 Subject: [PATCH 3/8] add integration test --- .../BuildDependencyGraphScopedDeps.cls | 52 +++++++++++++++++++ .../module-a/module.xml | 15 ++++++ .../module-b/module.xml | 15 ++++++ .../module-c/module.xml | 9 ++++ 4 files changed, 91 insertions(+) create mode 100644 tests/integration_tests/Test/PM/Integration/BuildDependencyGraphScopedDeps.cls create mode 100644 tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-a/module.xml create mode 100644 tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-b/module.xml create mode 100644 tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-c/module.xml diff --git a/tests/integration_tests/Test/PM/Integration/BuildDependencyGraphScopedDeps.cls b/tests/integration_tests/Test/PM/Integration/BuildDependencyGraphScopedDeps.cls new file mode 100644 index 00000000..998634f4 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/BuildDependencyGraphScopedDeps.cls @@ -0,0 +1,52 @@ +Class Test.PM.Integration.BuildDependencyGraphScopedDeps Extends Test.PM.Integration.Base +{ + +Parameter REPONAME = "build-dependency-graph-scoped-dep"; + +Parameter MODNAME = "module-a"; + +Property RepoPath As %String; + +Method OnBeforeAllTests() As %Status +{ + // Set up the repo path - use GetModuleDir utility + set ..RepoPath = ..GetModuleDir(..#REPONAME) + + // Create filesystem repo pointing to test data + set sc = ##class(%IPM.Main).Shell("repo -n "_..#REPONAME_" -fs -path "_..RepoPath) + do $$$AssertStatusOK(sc,"Created"_..#REPONAME_"repo successfully.") + quit sc +} + +Method OnAfterAllTests() As %Status +{ + // Remove test repository + set sc = ##class(%IPM.Main).Shell("repo -delete -name "_..#REPONAME) + do $$$AssertStatusOK(sc,"Deleted "_..#REPONAME_"repo successfully.") + quit sc +} + +/// BuildDependencyGraph should include scope information for scoped transitive dependencies +Method TestDependencyGraphScopedDependency() +{ + do $$$AssertStatusOK(##class(%IPM.Utils.Module).LoadModuleFromDirectory(..RepoPath_..#MODNAME), "Successfully loaded module from directory.") + set module = ##class(%IPM.Storage.Module).NameOpen(..#MODNAME,,.sc) + do $$$AssertStatusOK(sc, "Succcessfully opened module object.") + + // Expected dependencyGraph format: dependencyGraph() = $listbuild(, , , , ) + set scopes = $listbuild("test", "verify") + set sc = module.BuildDependencyGraph(.dependencyGraph, , , ,scopes) + do $$$AssertStatusOK(sc, "BuildDependencyGraph() call was successful.") + + // Check non-scoped dependency + do $$$AssertTrue($data(dependencyGraph("module-b"))) + set $listbuild(,,,,moduleBScope) = dependencyGraph("module-b") + do $$$AssertEquals(moduleBScope, "", "module-b scope succssfully checked to be empty.") + + // Check scoped dependency + do $$$AssertTrue($data(dependencyGraph("module-c"))) + set $listbuild(,,,,moduleCScope) = dependencyGraph("module-c") + do $$$AssertEquals(moduleCScope, "test", "module-c scope succssfully checked to be 'test'.") +} + +} diff --git a/tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-a/module.xml b/tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-a/module.xml new file mode 100644 index 00000000..ebac0083 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-a/module.xml @@ -0,0 +1,15 @@ + + + + + module-a + 1.0.0+snapshot + + + module-b + ^2.0.0 + + + + + diff --git a/tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-b/module.xml b/tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-b/module.xml new file mode 100644 index 00000000..d2f0ab6e --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-b/module.xml @@ -0,0 +1,15 @@ + + + + + module-b + 2.0.0+snapshot + + + module-c + ^3.0.0 + + + + + diff --git a/tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-c/module.xml b/tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-c/module.xml new file mode 100644 index 00000000..ad56d58a --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-c/module.xml @@ -0,0 +1,9 @@ + + + + + module-c + 3.0.0+snapshot + + + From fc830d5fc69904d588bd4c1e08336c7b04ffb3c6 Mon Sep 17 00:00:00 2001 From: Carolina BorbonMiranda Date: Mon, 18 May 2026 13:46:26 -0400 Subject: [PATCH 4/8] update test files --- .../_data/build-dependency-graph-scoped-dep/module-a/module.xml | 2 +- .../_data/build-dependency-graph-scoped-dep/module-b/module.xml | 2 +- .../_data/build-dependency-graph-scoped-dep/module-c/module.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-a/module.xml b/tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-a/module.xml index ebac0083..05df32c5 100644 --- a/tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-a/module.xml +++ b/tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-a/module.xml @@ -3,7 +3,7 @@ module-a - 1.0.0+snapshot + 1.0.0 module-b diff --git a/tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-b/module.xml b/tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-b/module.xml index d2f0ab6e..026571ab 100644 --- a/tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-b/module.xml +++ b/tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-b/module.xml @@ -3,7 +3,7 @@ module-b - 2.0.0+snapshot + 2.0.0 module-c diff --git a/tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-c/module.xml b/tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-c/module.xml index ad56d58a..322ddb92 100644 --- a/tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-c/module.xml +++ b/tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-c/module.xml @@ -3,7 +3,7 @@ module-c - 3.0.0+snapshot + 3.0.0 From afbef18ff13b2ca04dffcdd649ad7ff381db38d9 Mon Sep 17 00:00:00 2001 From: Carolina BorbonMiranda Date: Mon, 18 May 2026 15:53:09 -0400 Subject: [PATCH 5/8] add transitive scoped dependency with 'verify' scope --- .../PM/Integration/BuildDependencyGraphScopedDeps.cls | 6 +++++- .../module-b/module.xml | 4 ++++ .../module-d/module.xml | 9 +++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-d/module.xml diff --git a/tests/integration_tests/Test/PM/Integration/BuildDependencyGraphScopedDeps.cls b/tests/integration_tests/Test/PM/Integration/BuildDependencyGraphScopedDeps.cls index 998634f4..1c5c0757 100644 --- a/tests/integration_tests/Test/PM/Integration/BuildDependencyGraphScopedDeps.cls +++ b/tests/integration_tests/Test/PM/Integration/BuildDependencyGraphScopedDeps.cls @@ -43,10 +43,14 @@ Method TestDependencyGraphScopedDependency() set $listbuild(,,,,moduleBScope) = dependencyGraph("module-b") do $$$AssertEquals(moduleBScope, "", "module-b scope succssfully checked to be empty.") - // Check scoped dependency + // Check scoped dependencies do $$$AssertTrue($data(dependencyGraph("module-c"))) set $listbuild(,,,,moduleCScope) = dependencyGraph("module-c") do $$$AssertEquals(moduleCScope, "test", "module-c scope succssfully checked to be 'test'.") + + do $$$AssertTrue($data(dependencyGraph("module-d"))) + set $listbuild(,,,,moduleDScope) = dependencyGraph("module-d") + do $$$AssertEquals(moduleDScope, "verify", "module-d scope succssfully checked to be 'verify'.") } } diff --git a/tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-b/module.xml b/tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-b/module.xml index 026571ab..011c8ce5 100644 --- a/tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-b/module.xml +++ b/tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-b/module.xml @@ -9,6 +9,10 @@ module-c ^3.0.0 + + module-d + ^4.0.0 + diff --git a/tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-d/module.xml b/tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-d/module.xml new file mode 100644 index 00000000..8da6b6ef --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/build-dependency-graph-scoped-dep/module-d/module.xml @@ -0,0 +1,9 @@ + + + + + module-d + 4.0.0 + + + From 34c5e8afd11b43ec332a5689a20ebdf09fa6394a Mon Sep 17 00:00:00 2001 From: Carolina BorbonMiranda Date: Tue, 19 May 2026 08:53:49 -0400 Subject: [PATCH 6/8] fix typos, and add scope to description of BuildDependencyGraph() --- src/cls/IPM/Storage/Module.cls | 2 ++ .../PM/Integration/BuildDependencyGraphScopedDeps.cls | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/cls/IPM/Storage/Module.cls b/src/cls/IPM/Storage/Module.cls index a003c44d..765694ac 100644 --- a/src/cls/IPM/Storage/Module.cls +++ b/src/cls/IPM/Storage/Module.cls @@ -879,6 +879,8 @@ ClassMethod HasScope( ///

  • Depth in the dependency tree (1 for direct dependencies, 2 for transitive, etc.)
  • ///
  • Repository from which the module will be obtained (empty if already installed)
  • ///
  • Version string of the module to be installed
  • +///
  • Display name of the module
  • +///
  • Scope of the module, or empty string if the dependency is not scoped
  • /// ///

    /// Parameters:
    diff --git a/tests/integration_tests/Test/PM/Integration/BuildDependencyGraphScopedDeps.cls b/tests/integration_tests/Test/PM/Integration/BuildDependencyGraphScopedDeps.cls index 1c5c0757..0221dba9 100644 --- a/tests/integration_tests/Test/PM/Integration/BuildDependencyGraphScopedDeps.cls +++ b/tests/integration_tests/Test/PM/Integration/BuildDependencyGraphScopedDeps.cls @@ -31,7 +31,7 @@ Method TestDependencyGraphScopedDependency() { do $$$AssertStatusOK(##class(%IPM.Utils.Module).LoadModuleFromDirectory(..RepoPath_..#MODNAME), "Successfully loaded module from directory.") set module = ##class(%IPM.Storage.Module).NameOpen(..#MODNAME,,.sc) - do $$$AssertStatusOK(sc, "Succcessfully opened module object.") + do $$$AssertStatusOK(sc, "Successfully opened module object.") // Expected dependencyGraph format: dependencyGraph() = $listbuild(, , , , ) set scopes = $listbuild("test", "verify") @@ -41,16 +41,16 @@ Method TestDependencyGraphScopedDependency() // Check non-scoped dependency do $$$AssertTrue($data(dependencyGraph("module-b"))) set $listbuild(,,,,moduleBScope) = dependencyGraph("module-b") - do $$$AssertEquals(moduleBScope, "", "module-b scope succssfully checked to be empty.") + do $$$AssertEquals(moduleBScope, "", "module-b scope successfully checked to be empty.") // Check scoped dependencies do $$$AssertTrue($data(dependencyGraph("module-c"))) set $listbuild(,,,,moduleCScope) = dependencyGraph("module-c") - do $$$AssertEquals(moduleCScope, "test", "module-c scope succssfully checked to be 'test'.") + do $$$AssertEquals(moduleCScope, "test", "module-c scope successfully checked to be 'test'.") do $$$AssertTrue($data(dependencyGraph("module-d"))) set $listbuild(,,,,moduleDScope) = dependencyGraph("module-d") - do $$$AssertEquals(moduleDScope, "verify", "module-d scope succssfully checked to be 'verify'.") + do $$$AssertEquals(moduleDScope, "verify", "module-d scope successfully checked to be 'verify'.") } } From 23ee4480fa6eb27a09d5efdcb62448009dab8039 Mon Sep 17 00:00:00 2001 From: Carolina BorbonMiranda Date: Wed, 20 May 2026 10:51:08 -0400 Subject: [PATCH 7/8] remove unused parameter in --- src/cls/IPM/Storage/Module.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cls/IPM/Storage/Module.cls b/src/cls/IPM/Storage/Module.cls index 9e3b8d5c..5d856329 100644 --- a/src/cls/IPM/Storage/Module.cls +++ b/src/cls/IPM/Storage/Module.cls @@ -1132,7 +1132,7 @@ Method ProcessSingleDependencyIterative( set serverName = "" set version = "" if $data(pDependencyGraph(pDep.Name),depInfo) { - set $listbuild(previousDepth,serverName,version,scope) = depInfo + set $listbuild(previousDepth,serverName,version) = depInfo } // Check if installed version satisfies requirements From 9b72d44ff4276a94296161b5c2d9c5882757b51c Mon Sep 17 00:00:00 2001 From: Carolina BorbonMiranda Date: Tue, 26 May 2026 10:39:52 -0400 Subject: [PATCH 8/8] remove deployed and platform version from dependency graph, which was only there in one code path and not others, so removal standardizes the graph --- src/cls/IPM/Storage/Module.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cls/IPM/Storage/Module.cls b/src/cls/IPM/Storage/Module.cls index 5d856329..90a4e643 100644 --- a/src/cls/IPM/Storage/Module.cls +++ b/src/cls/IPM/Storage/Module.cls @@ -1206,7 +1206,7 @@ Method ProcessSingleDependencyIterative( } set pDependencyGraph(pDep.Name) = $listbuild( - pDepth, qualifiedReference.ServerName, moduleObj.VersionString, qualifiedReference.Deployed, qualifiedReference.PlatformVersion,pDep.DisplayName,pDep.Scope + pDepth, qualifiedReference.ServerName, moduleObj.VersionString, pDep.DisplayName, pDep.Scope ) set pDependencyGraph(pDep.Name,pParentInfo) = pDep.VersionString