Repository: irods/irods
Pull request #8267: [#8265] Restore support for KeyValPair in Python REP (4-3-stable)
Author: login: korydraughn
Created: 2025-03-08T00:25:50Z | Merged: 2025-04-23T21:19:54Z

I've confirmed the change works at the bench.

Still need to run this change against the PREP via the testing environment.


Diff:
diff --git a/lib/core/src/msParam.cpp b/lib/core/src/msParam.cpp
index 457e3eabdf..25611c5811 100644
--- a/lib/core/src/msParam.cpp
+++ b/lib/core/src/msParam.cpp
@@ -570,6 +570,12 @@ int clearMsParam(msParam_t* msParam, int freeInOutStruct)
                 clearExecCmdOut(msParam->inOutStruct);
                 std::free(msParam->inOutStruct);
             }
+            // The following code block is disabled because it breaks support for KeyValPair's
+            // in the Python REP (even though it addresses a memory leak). If you're here
+            // because of memory leaks reported by C++ tools and decide to enable this code block
+            // or modify this function in any way, make sure to test the data types using the
+            // Python REP.
+#if 0
             // Do not free KeyValPair_MS_T unconditionally as this will lead
             // to a use-after-free error. Additionally, this behavior matches the default
             // of checking freeInOutStruct before freeing the data.
@@ -578,6 +584,7 @@ int clearMsParam(msParam_t* msParam, int freeInOutStruct)
                 // NOLINTNEXTLINE(cppcoreguidelines-no-malloc,cppcoreguidelines-owning-memory)
                 std::free(msParam->inOutStruct);
             }
+#endif
             // This else-block must always be the final block in this if-ladder.
             // Changing the order of these if-blocks can result in memory leaks.
             else if (freeInOutStruct > 0) {
diff --git a/scripts/irods/test/test_python_rule_engine_plugin.py b/scripts/irods/test/test_python_rule_engine_plugin.py
index 060f045e98..ea9ffcbd92 100644
--- a/scripts/irods/test/test_python_rule_engine_plugin.py
+++ b/scripts/irods/test/test_python_rule_engine_plugin.py
@@ -1,14 +1,11 @@
-from __future__ import print_function
+import os
 import sys
-if sys.version_info < (2, 7):
-    import unittest2 as unittest
-else:
-    import unittest
-
-import time
 import tempfile
+import time
+import unittest
 from textwrap import dedent
 
+from . import session
 from . import resource_suite
 from .. import paths
 from .. import lib
@@ -58,12 +55,14 @@ def append_native_re_to_server_config (with_backup=False):
         IrodsController().start()
 
 
-class Test_Python_Rule_Engine_Plugin(resource_suite.ResourceBase, unittest.TestCase):
+class Test_Python_Rule_Engine_Plugin(session.make_sessions_mixin([('otherrods', 'rods')], [('alice', 'apass')]), unittest.TestCase):
     plugin_name = IrodsConfig().default_rule_engine_plugin
     class_name = 'Test_Python_Rule_Engine_Plugin'
 
     def setUp(self):
         super(Test_Python_Rule_Engine_Plugin, self).setUp()
+        self.admin = self.admin_sessions[0]
+        self.user = self.user_sessions[0]
 
     def tearDown(self):
         super(Test_Python_Rule_Engine_Plugin, self).tearDown()
@@ -103,8 +102,35 @@ def test_python_error_to_native_58 (self):
                 def myrule(*x):
                     return irods_errors.SYS_INVALID_FILE_PATH
                 '''))
-                self.user0.assert_icommand(
+                self.user.assert_icommand(
                     [ 'irule','-r', 'irods_rule_engine_plugin-irods_rule_language-instance',
                       '''writeLine("stdout", error("SYS_INVALID_FILE_PATH") == errorcode(myrule()))''',
                       'null', 'ruleExecOut' ],
                     'STDOUT_SINGLELINE',['true'] )
+
+    @unittest.skipUnless(plugin_name == 'irods_rule_engine_plugin-python', 'only applicable for python REP')
+    def test_REPF_does_not_deallocate_KeyValPair__issue_8265(self):
+        # Create a rule file which attempts to add an AVU to the session collection.
+        avu_name = 'issue_8265_attr_name'
+        avu_value = 'issue_8265_attr_value'
+        rule_file = os.path.join(self.admin.local_session_dir, 'issue_8265.r')
+        with open(rule_file, 'w') as f:
+            f.write(dedent(
+                f"""
+                import irods_types
+                def main(rule_args, callback, rei):
+                    collection = '{self.admin.session_collection}'
+                    avu = '{avu_name}={avu_value}'
+                    res = callback.msiString2KeyValPair(avu, irods_types.BytesBuf())
+                    callback.msiSetKeyValuePairsToObj(res['arguments'][1], collection, '-C')
+
+                INPUT null
+                OUTPUT ruleExecOut
+                """))
+
+        # Run the rule and show that it added the expected AVU.
+        self.admin.assert_icommand(['irule', '-r', 'irods_rule_engine_plugin-python-instance', '-F', rule_file])
+        self.admin.assert_icommand(['imeta', 'ls', '-C', self.admin.session_collection], 'STDOUT', [
+            f'attribute: {avu_name}\n',
+            f'value: {avu_value}\n'
+        ])


Reviews:
user: login: alanking
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T15:26:05Z
user: login: korydraughn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T16:30:32Z
user: login: alanking
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T16:32:45Z
user: login: alanking
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T16:34:11Z
user: login: korydraughn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T16:42:18Z
user: login: alanking
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T16:43:51Z
user: login: korydraughn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T16:44:59Z
user: login: korydraughn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T16:54:02Z
user: login: korydraughn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T17:00:35Z
user: login: alanking
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T17:03:20Z
user: login: korydraughn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T17:11:08Z
user: login: korydraughn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T17:27:29Z
user: login: alanking
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T17:39:38Z
user: login: korydraughn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T22:54:15Z
user: login: korydraughn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T22:54:35Z
user: login: korydraughn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T22:54:43Z
user: login: korydraughn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T23:00:14Z
user: login: alanking
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T19:09:09Z
user: login: korydraughn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T19:24:35Z
user: login: korydraughn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T20:00:45Z
user: login: korydraughn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T20:38:13Z
user: login: korydraughn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T20:38:55Z
user: login: alanking
body: Seems good. Squash em
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T21:01:54Z
user: login: alanking
body: \# it
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T21:17:29Z

Review comments:
diff_hunk: @@ -108,3 +103,55 @@ def myrule(*x):
                       '''writeLine("stdout", error("SYS_INVALID_FILE_PATH") == errorcode(myrule()))''',
                       'null', 'ruleExecOut' ],
                     'STDOUT_SINGLELINE',['true'] )
+
+class Test_Python_Rule_Engine_Plugin_No_ResourceBase(session.make_sessions_mixin([('otherrods', 'rods')], []), unittest.TestCase):
path: scripts/irods/test/test_python_rule_engine_plugin.py
user: login: alanking
body: Unless I'm missing something, it looks like removing the `ResourceBase` inheritance in `Test_Python_Rule_Engine_Plugin` would be trivial. It only seems to use `self.user0` and `self.admin`. Then, you wouldn't need this class and you could just add the new test case to that class.
created_at: 2025-03-10T14:37:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -108,3 +103,55 @@ def myrule(*x):
                       '''writeLine("stdout", error("SYS_INVALID_FILE_PATH") == errorcode(myrule()))''',
                       'null', 'ruleExecOut' ],
                     'STDOUT_SINGLELINE',['true'] )
+
+class Test_Python_Rule_Engine_Plugin_No_ResourceBase(session.make_sessions_mixin([('otherrods', 'rods')], []), unittest.TestCase):
+
+    plugin_name = IrodsConfig().default_rule_engine_plugin
+
+    def setUp(self):
+        super(Test_Python_Rule_Engine_Plugin_No_ResourceBase, self).setUp()
+        self.admin = self.admin_sessions[0]
+
+    def tearDown(self):
+        super(Test_Python_Rule_Engine_Plugin_No_ResourceBase, self).tearDown()
+
+    @unittest.skipUnless(plugin_name == 'irods_rule_engine_plugin-python', 'Requires the PREP and NREP')
+    def test_PREP_receives_KeyValPair_with_expected_metadata_from_NREP__issue_8265(self):
+        config = IrodsConfig()
+
+        with lib.file_backed_up(config.server_config_path):
+            # Add the NREP to the list of configured rule engines. It provides the
+            # MSIs needed by our PREP rule.
path: scripts/irods/test/test_python_rule_engine_plugin.py
user: login: alanking
body: Is this true? iRODS MSIs cannot be used without having NREP configured?
created_at: 2025-03-10T14:50:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -57,7 +53,6 @@ def append_native_re_to_server_config (with_backup=False):
         irods_config.commit(irods_config.server_config, irods_config.server_config_path, make_backup=with_backup)
         IrodsController().start()
 
-
path: scripts/irods/test/test_python_rule_engine_plugin.py
user: login: alanking
body: From PEP 8 (https://peps.python.org/pep-0008/#blank-lines):

> Surround top-level function and class definitions with two blank lines.
created_at: 2025-03-10T15:01:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -19,7 +16,6 @@
 from ..controller import IrodsController
 import contextlib
 
-
path: scripts/irods/test/test_python_rule_engine_plugin.py
user: login: alanking
body: From PEP 8 (https://peps.python.org/pep-0008/#blank-lines):

> Surround top-level function and class definitions with two blank lines.
created_at: 2025-03-10T15:02:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -57,7 +53,6 @@ def append_native_re_to_server_config (with_backup=False):
         irods_config.commit(irods_config.server_config, irods_config.server_config_path, make_backup=with_backup)
         IrodsController().start()
 
-
path: scripts/irods/test/test_python_rule_engine_plugin.py
user: login: korydraughn
body: Are you proposing we follow that style guide?
Does the Black formatter follow those rules too?
created_at: 2025-03-10T16:30:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -57,7 +53,6 @@ def append_native_re_to_server_config (with_backup=False):
         irods_config.commit(irods_config.server_config, irods_config.server_config_path, make_backup=with_backup)
         IrodsController().start()
 
-
path: scripts/irods/test/test_python_rule_engine_plugin.py
user: login: alanking
body: Black is PEP 8 compliant, yes.
created_at: 2025-03-10T16:32:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -57,7 +53,6 @@ def append_native_re_to_server_config (with_backup=False):
         irods_config.commit(irods_config.server_config, irods_config.server_config_path, make_backup=with_backup)
         IrodsController().start()
 
-
path: scripts/irods/test/test_python_rule_engine_plugin.py
user: login: alanking
body: Was mostly confused why the line was being removed (here and the line earlier in the file). The reason it's there is *likely* because of PEP 8 style guidelines.
created_at: 2025-03-10T16:34:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -108,3 +103,55 @@ def myrule(*x):
                       '''writeLine("stdout", error("SYS_INVALID_FILE_PATH") == errorcode(myrule()))''',
                       'null', 'ruleExecOut' ],
                     'STDOUT_SINGLELINE',['true'] )
+
+class Test_Python_Rule_Engine_Plugin_No_ResourceBase(session.make_sessions_mixin([('otherrods', 'rods')], []), unittest.TestCase):
+
+    plugin_name = IrodsConfig().default_rule_engine_plugin
+
+    def setUp(self):
+        super(Test_Python_Rule_Engine_Plugin_No_ResourceBase, self).setUp()
+        self.admin = self.admin_sessions[0]
+
+    def tearDown(self):
+        super(Test_Python_Rule_Engine_Plugin_No_ResourceBase, self).tearDown()
+
+    @unittest.skipUnless(plugin_name == 'irods_rule_engine_plugin-python', 'Requires the PREP and NREP')
+    def test_PREP_receives_KeyValPair_with_expected_metadata_from_NREP__issue_8265(self):
+        config = IrodsConfig()
+
+        with lib.file_backed_up(config.server_config_path):
+            # Add the NREP to the list of configured rule engines. It provides the
+            # MSIs needed by our PREP rule.
path: scripts/irods/test/test_python_rule_engine_plugin.py
user: login: korydraughn
body: I tried invoking a few MSIs without the NREP and they worked.

Will update the test to not configure the NREP and see how it behaves.
created_at: 2025-03-10T16:42:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -108,3 +103,55 @@ def myrule(*x):
                       '''writeLine("stdout", error("SYS_INVALID_FILE_PATH") == errorcode(myrule()))''',
                       'null', 'ruleExecOut' ],
                     'STDOUT_SINGLELINE',['true'] )
+
+class Test_Python_Rule_Engine_Plugin_No_ResourceBase(session.make_sessions_mixin([('otherrods', 'rods')], []), unittest.TestCase):
+
+    plugin_name = IrodsConfig().default_rule_engine_plugin
+
+    def setUp(self):
+        super(Test_Python_Rule_Engine_Plugin_No_ResourceBase, self).setUp()
+        self.admin = self.admin_sessions[0]
+
+    def tearDown(self):
+        super(Test_Python_Rule_Engine_Plugin_No_ResourceBase, self).tearDown()
+
+    @unittest.skipUnless(plugin_name == 'irods_rule_engine_plugin-python', 'Requires the PREP and NREP')
+    def test_PREP_receives_KeyValPair_with_expected_metadata_from_NREP__issue_8265(self):
+        config = IrodsConfig()
+
+        with lib.file_backed_up(config.server_config_path):
+            # Add the NREP to the list of configured rule engines. It provides the
+            # MSIs needed by our PREP rule.
path: scripts/irods/test/test_python_rule_engine_plugin.py
user: login: alanking
body: Okay. It may be worth having one test for each configuration if it seems to change the behavior. This was just surprising to me.
created_at: 2025-03-10T16:43:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -57,7 +53,6 @@ def append_native_re_to_server_config (with_backup=False):
         irods_config.commit(irods_config.server_config, irods_config.server_config_path, make_backup=with_backup)
         IrodsController().start()
 
-
path: scripts/irods/test/test_python_rule_engine_plugin.py
user: login: korydraughn
body: That's fair. Will restore the lines.
created_at: 2025-03-10T16:44:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -108,3 +103,55 @@ def myrule(*x):
                       '''writeLine("stdout", error("SYS_INVALID_FILE_PATH") == errorcode(myrule()))''',
                       'null', 'ruleExecOut' ],
                     'STDOUT_SINGLELINE',['true'] )
+
+class Test_Python_Rule_Engine_Plugin_No_ResourceBase(session.make_sessions_mixin([('otherrods', 'rods')], []), unittest.TestCase):
path: scripts/irods/test/test_python_rule_engine_plugin.py
user: login: korydraughn
body: You're correct. The new class was added to dodge `ResourceBase` and avoid changing an existing test.

Will confirm what you've identified and merge if trivial.
created_at: 2025-03-10T16:54:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -108,3 +103,55 @@ def myrule(*x):
                       '''writeLine("stdout", error("SYS_INVALID_FILE_PATH") == errorcode(myrule()))''',
                       'null', 'ruleExecOut' ],
                     'STDOUT_SINGLELINE',['true'] )
+
+class Test_Python_Rule_Engine_Plugin_No_ResourceBase(session.make_sessions_mixin([('otherrods', 'rods')], []), unittest.TestCase):
+
+    plugin_name = IrodsConfig().default_rule_engine_plugin
+
+    def setUp(self):
+        super(Test_Python_Rule_Engine_Plugin_No_ResourceBase, self).setUp()
+        self.admin = self.admin_sessions[0]
+
+    def tearDown(self):
+        super(Test_Python_Rule_Engine_Plugin_No_ResourceBase, self).tearDown()
+
+    @unittest.skipUnless(plugin_name == 'irods_rule_engine_plugin-python', 'Requires the PREP and NREP')
+    def test_PREP_receives_KeyValPair_with_expected_metadata_from_NREP__issue_8265(self):
+        config = IrodsConfig()
+
+        with lib.file_backed_up(config.server_config_path):
+            # Add the NREP to the list of configured rule engines. It provides the
+            # MSIs needed by our PREP rule.
path: scripts/irods/test/test_python_rule_engine_plugin.py
user: login: korydraughn
body: I'm still brainstorming why the NREP isn't needed. I thought things like `writeLine` were part of the NREP.

Will need to look into that to gain a better understanding. And then document it somewhere.

> It may be worth having one test for each configuration

You're saying a test for the NREP and another for the PREP? Please elaborate.
created_at: 2025-03-10T17:00:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -108,3 +103,55 @@ def myrule(*x):
                       '''writeLine("stdout", error("SYS_INVALID_FILE_PATH") == errorcode(myrule()))''',
                       'null', 'ruleExecOut' ],
                     'STDOUT_SINGLELINE',['true'] )
+
+class Test_Python_Rule_Engine_Plugin_No_ResourceBase(session.make_sessions_mixin([('otherrods', 'rods')], []), unittest.TestCase):
+
+    plugin_name = IrodsConfig().default_rule_engine_plugin
+
+    def setUp(self):
+        super(Test_Python_Rule_Engine_Plugin_No_ResourceBase, self).setUp()
+        self.admin = self.admin_sessions[0]
+
+    def tearDown(self):
+        super(Test_Python_Rule_Engine_Plugin_No_ResourceBase, self).tearDown()
+
+    @unittest.skipUnless(plugin_name == 'irods_rule_engine_plugin-python', 'Requires the PREP and NREP')
+    def test_PREP_receives_KeyValPair_with_expected_metadata_from_NREP__issue_8265(self):
+        config = IrodsConfig()
+
+        with lib.file_backed_up(config.server_config_path):
+            # Add the NREP to the list of configured rule engines. It provides the
+            # MSIs needed by our PREP rule.
path: scripts/irods/test/test_python_rule_engine_plugin.py
user: login: alanking
body: > You're saying a test for the NREP and another for the PREP? Please elaborate.

I was suggesting two tests for these microservices this test suite (it's for PREP). One wherein NREP is configured, and one where NREP is not configured. Both tests would have PREP configured. Not sure if that has any value, though.
created_at: 2025-03-10T17:03:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -108,3 +103,55 @@ def myrule(*x):
                       '''writeLine("stdout", error("SYS_INVALID_FILE_PATH") == errorcode(myrule()))''',
                       'null', 'ruleExecOut' ],
                     'STDOUT_SINGLELINE',['true'] )
+
+class Test_Python_Rule_Engine_Plugin_No_ResourceBase(session.make_sessions_mixin([('otherrods', 'rods')], []), unittest.TestCase):
+
+    plugin_name = IrodsConfig().default_rule_engine_plugin
+
+    def setUp(self):
+        super(Test_Python_Rule_Engine_Plugin_No_ResourceBase, self).setUp()
+        self.admin = self.admin_sessions[0]
+
+    def tearDown(self):
+        super(Test_Python_Rule_Engine_Plugin_No_ResourceBase, self).tearDown()
+
+    @unittest.skipUnless(plugin_name == 'irods_rule_engine_plugin-python', 'Requires the PREP and NREP')
+    def test_PREP_receives_KeyValPair_with_expected_metadata_from_NREP__issue_8265(self):
+        config = IrodsConfig()
+
+        with lib.file_backed_up(config.server_config_path):
+            # Add the NREP to the list of configured rule engines. It provides the
+            # MSIs needed by our PREP rule.
path: scripts/irods/test/test_python_rule_engine_plugin.py
user: login: korydraughn
body: I see now. I can't think of the benefits for doing that, but I also don't see a downside to covering both cases.

Will add another test doing what you've suggested.
created_at: 2025-03-10T17:11:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -108,3 +103,55 @@ def myrule(*x):
                       '''writeLine("stdout", error("SYS_INVALID_FILE_PATH") == errorcode(myrule()))''',
                       'null', 'ruleExecOut' ],
                     'STDOUT_SINGLELINE',['true'] )
+
+class Test_Python_Rule_Engine_Plugin_No_ResourceBase(session.make_sessions_mixin([('otherrods', 'rods')], []), unittest.TestCase):
+
+    plugin_name = IrodsConfig().default_rule_engine_plugin
+
+    def setUp(self):
+        super(Test_Python_Rule_Engine_Plugin_No_ResourceBase, self).setUp()
+        self.admin = self.admin_sessions[0]
+
+    def tearDown(self):
+        super(Test_Python_Rule_Engine_Plugin_No_ResourceBase, self).tearDown()
+
+    @unittest.skipUnless(plugin_name == 'irods_rule_engine_plugin-python', 'Requires the PREP and NREP')
+    def test_PREP_receives_KeyValPair_with_expected_metadata_from_NREP__issue_8265(self):
+        config = IrodsConfig()
+
+        with lib.file_backed_up(config.server_config_path):
+            # Add the NREP to the list of configured rule engines. It provides the
+            # MSIs needed by our PREP rule.
path: scripts/irods/test/test_python_rule_engine_plugin.py
user: login: korydraughn
body: So, a little experimentation has confirmed that the NREP is required for some MSIs. For example, `msiStrlen`.

We'll need to document which MSIs are independent of the NREP. I'll open an issue for that soon.
created_at: 2025-03-10T17:27:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -108,3 +103,55 @@ def myrule(*x):
                       '''writeLine("stdout", error("SYS_INVALID_FILE_PATH") == errorcode(myrule()))''',
                       'null', 'ruleExecOut' ],
                     'STDOUT_SINGLELINE',['true'] )
+
+class Test_Python_Rule_Engine_Plugin_No_ResourceBase(session.make_sessions_mixin([('otherrods', 'rods')], []), unittest.TestCase):
+
+    plugin_name = IrodsConfig().default_rule_engine_plugin
+
+    def setUp(self):
+        super(Test_Python_Rule_Engine_Plugin_No_ResourceBase, self).setUp()
+        self.admin = self.admin_sessions[0]
+
+    def tearDown(self):
+        super(Test_Python_Rule_Engine_Plugin_No_ResourceBase, self).tearDown()
+
+    @unittest.skipUnless(plugin_name == 'irods_rule_engine_plugin-python', 'Requires the PREP and NREP')
+    def test_PREP_receives_KeyValPair_with_expected_metadata_from_NREP__issue_8265(self):
+        config = IrodsConfig()
+
+        with lib.file_backed_up(config.server_config_path):
+            # Add the NREP to the list of configured rule engines. It provides the
+            # MSIs needed by our PREP rule.
path: scripts/irods/test/test_python_rule_engine_plugin.py
user: login: alanking
body: Well that is quite surprising
created_at: 2025-03-10T17:39:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -108,3 +103,55 @@ def myrule(*x):
                       '''writeLine("stdout", error("SYS_INVALID_FILE_PATH") == errorcode(myrule()))''',
                       'null', 'ruleExecOut' ],
                     'STDOUT_SINGLELINE',['true'] )
+
+class Test_Python_Rule_Engine_Plugin_No_ResourceBase(session.make_sessions_mixin([('otherrods', 'rods')], []), unittest.TestCase):
path: scripts/irods/test/test_python_rule_engine_plugin.py
user: login: korydraughn
body: Not verified, but updated.

Will confirm test passes with changes.
created_at: 2025-03-19T22:54:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -57,7 +53,6 @@ def append_native_re_to_server_config (with_backup=False):
         irods_config.commit(irods_config.server_config, irods_config.server_config_path, make_backup=with_backup)
         IrodsController().start()
 
-
path: scripts/irods/test/test_python_rule_engine_plugin.py
user: login: korydraughn
body: Restored.
created_at: 2025-03-19T22:54:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -19,7 +16,6 @@
 from ..controller import IrodsController
 import contextlib
 
-
path: scripts/irods/test/test_python_rule_engine_plugin.py
user: login: korydraughn
body: Done.
created_at: 2025-03-19T22:54:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -108,3 +103,55 @@ def myrule(*x):
                       '''writeLine("stdout", error("SYS_INVALID_FILE_PATH") == errorcode(myrule()))''',
                       'null', 'ruleExecOut' ],
                     'STDOUT_SINGLELINE',['true'] )
+
+class Test_Python_Rule_Engine_Plugin_No_ResourceBase(session.make_sessions_mixin([('otherrods', 'rods')], []), unittest.TestCase):
+
+    plugin_name = IrodsConfig().default_rule_engine_plugin
+
+    def setUp(self):
+        super(Test_Python_Rule_Engine_Plugin_No_ResourceBase, self).setUp()
+        self.admin = self.admin_sessions[0]
+
+    def tearDown(self):
+        super(Test_Python_Rule_Engine_Plugin_No_ResourceBase, self).tearDown()
+
+    @unittest.skipUnless(plugin_name == 'irods_rule_engine_plugin-python', 'Requires the PREP and NREP')
+    def test_PREP_receives_KeyValPair_with_expected_metadata_from_NREP__issue_8265(self):
+        config = IrodsConfig()
+
+        with lib.file_backed_up(config.server_config_path):
+            # Add the NREP to the list of configured rule engines. It provides the
+            # MSIs needed by our PREP rule.
path: scripts/irods/test/test_python_rule_engine_plugin.py
user: login: korydraughn
body: Removed the NREP logic.

Created #8306.
created_at: 2025-03-19T23:00:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,8 +102,35 @@ def test_python_error_to_native_58 (self):
                 def myrule(*x):
                     return irods_errors.SYS_INVALID_FILE_PATH
                 '''))
-                self.user0.assert_icommand(
+                self.user.assert_icommand(
                     [ 'irule','-r', 'irods_rule_engine_plugin-irods_rule_language-instance',
                       '''writeLine("stdout", error("SYS_INVALID_FILE_PATH") == errorcode(myrule()))''',
                       'null', 'ruleExecOut' ],
                     'STDOUT_SINGLELINE',['true'] )
+
+    @unittest.skipUnless(plugin_name == 'irods_rule_engine_plugin-python', 'Requires the PREP and NREP')
+    def test_PREP_receives_KeyValPair_with_expected_metadata_from_NREP__issue_8265(self):
path: scripts/irods/test/test_python_rule_engine_plugin.py
user: login: alanking
body: Need to remove mentions of NREP
created_at: 2025-03-31T19:08:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,8 +102,35 @@ def test_python_error_to_native_58 (self):
                 def myrule(*x):
                     return irods_errors.SYS_INVALID_FILE_PATH
                 '''))
-                self.user0.assert_icommand(
+                self.user.assert_icommand(
                     [ 'irule','-r', 'irods_rule_engine_plugin-irods_rule_language-instance',
                       '''writeLine("stdout", error("SYS_INVALID_FILE_PATH") == errorcode(myrule()))''',
                       'null', 'ruleExecOut' ],
                     'STDOUT_SINGLELINE',['true'] )
+
+    @unittest.skipUnless(plugin_name == 'irods_rule_engine_plugin-python', 'Requires the PREP and NREP')
+    def test_PREP_receives_KeyValPair_with_expected_metadata_from_NREP__issue_8265(self):
path: scripts/irods/test/test_python_rule_engine_plugin.py
user: login: korydraughn
body: Done.
created_at: 2025-04-23T19:24:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -108,3 +103,55 @@ def myrule(*x):
                       '''writeLine("stdout", error("SYS_INVALID_FILE_PATH") == errorcode(myrule()))''',
                       'null', 'ruleExecOut' ],
                     'STDOUT_SINGLELINE',['true'] )
+
+class Test_Python_Rule_Engine_Plugin_No_ResourceBase(session.make_sessions_mixin([('otherrods', 'rods')], []), unittest.TestCase):
+
+    plugin_name = IrodsConfig().default_rule_engine_plugin
+
+    def setUp(self):
+        super(Test_Python_Rule_Engine_Plugin_No_ResourceBase, self).setUp()
+        self.admin = self.admin_sessions[0]
+
+    def tearDown(self):
+        super(Test_Python_Rule_Engine_Plugin_No_ResourceBase, self).tearDown()
+
+    @unittest.skipUnless(plugin_name == 'irods_rule_engine_plugin-python', 'Requires the PREP and NREP')
+    def test_PREP_receives_KeyValPair_with_expected_metadata_from_NREP__issue_8265(self):
+        config = IrodsConfig()
+
+        with lib.file_backed_up(config.server_config_path):
+            # Add the NREP to the list of configured rule engines. It provides the
+            # MSIs needed by our PREP rule.
path: scripts/irods/test/test_python_rule_engine_plugin.py
user: login: korydraughn
body: Given the target issue doesn't involve any interactions with the NREP (see https://github.com/irods/irods/pull/8267#discussion_r1987732428), the test has been updated to only cover the PREP.
created_at: 2025-04-23T20:00:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -95,6 +95,8 @@ namespace irods{
             }
             ~all_resources() {
                 for(auto itr= begin(myArgv);itr != end(myArgv); ++itr) {
+                    // TODO Could this be where the keyvalpair_t is causing an issue?
+                    // Notice the second argument is about freeing the inOutStruct.
path: server/re/src/irods_re_plugin.cpp
user: login: korydraughn
body: Remove this.
created_at: 2025-04-23T20:38:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -95,6 +95,8 @@ namespace irods{
             }
             ~all_resources() {
                 for(auto itr= begin(myArgv);itr != end(myArgv); ++itr) {
+                    // TODO Could this be where the keyvalpair_t is causing an issue?
+                    // Notice the second argument is about freeing the inOutStruct.
path: server/re/src/irods_re_plugin.cpp
user: login: korydraughn
body: Done.
created_at: 2025-04-23T20:38:55Z
author_association: CONTRIBUTOR

PR comments:
user: login: alanking
created_at: 2025-04-23T20:37:12Z
body: Looks like there may be some changes yet to come, so will await signal before fully reviewing.
author_association: CONTRIBUTOR
user: login: korydraughn
created_at: 2025-04-23T20:58:30Z
body: The test passes/fails in the expected way under the correct conditions.

The logs contain the error messages for the failure case too.

This is now ready for final review.
author_association: CONTRIBUTOR
user: login: korydraughn
created_at: 2025-04-23T21:15:00Z
body: Updated commit message and squashed.
author_association: CONTRIBUTOR
user: login: korydraughn
created_at: 2025-04-23T21:18:59Z
body: Pounded.
author_association: CONTRIBUTOR