|
|
@@ -1,10 +1,14 @@
|
|
|
+import inspect
|
|
|
import io
|
|
|
+import os
|
|
|
import sys
|
|
|
+import traceback
|
|
|
import uuid
|
|
|
from datetime import UTC, date, datetime
|
|
|
from decimal import Decimal
|
|
|
from unittest.mock import PropertyMock, patch
|
|
|
|
|
|
+from django.conf import settings
|
|
|
from django.contrib.auth import get_user_model
|
|
|
from django.core.exceptions import ValidationError
|
|
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
|
@@ -477,6 +481,100 @@ class ScriptModuleLoadingTestCase(TestCase):
|
|
|
script.log_success('Start')
|
|
|
self.assertIn('Start', captured.output[0])
|
|
|
|
|
|
+ def test_module_file_is_filesystem_path_for_local_storage(self):
|
|
|
+ """
|
|
|
+ For the local file-system storage backend the display location must be the script's
|
|
|
+ actual filesystem path (<SCRIPTS_ROOT>/<file_path>), not a path derived from the
|
|
|
+ current working directory (#23511).
|
|
|
+ """
|
|
|
+ script_content = (
|
|
|
+ b"from extras.scripts import Script\n\n\n"
|
|
|
+ b"class TestScript(Script):\n pass\n"
|
|
|
+ )
|
|
|
+
|
|
|
+ class _Storage:
|
|
|
+ def open(self, name, mode='rb'):
|
|
|
+ return io.BytesIO(script_content)
|
|
|
+
|
|
|
+ module = ScriptModule(file_root='scripts', file_path='example.py')
|
|
|
+ namespaced_key = f'{SCRIPT_MODULE_NAME_PREFIX}example'
|
|
|
+ self.addCleanup(lambda: sys.modules.pop(namespaced_key, None))
|
|
|
+
|
|
|
+ with patch('extras.models.mixins.storages') as mock_storages:
|
|
|
+ mock_storages.__getitem__.return_value = _Storage()
|
|
|
+ loaded = module.get_module()
|
|
|
+
|
|
|
+ self.assertEqual(loaded.__file__, module.full_path)
|
|
|
+ self.assertEqual(loaded.__file__, os.path.join(settings.SCRIPTS_ROOT, 'example.py'))
|
|
|
+ self.assertEqual(inspect.getfile(loaded.TestScript), module.full_path)
|
|
|
+
|
|
|
+ def test_module_file_is_storage_name_for_remote_storage(self):
|
|
|
+ """
|
|
|
+ For non-local storage backends (e.g. S3) the display location must be the storage name
|
|
|
+ (the key within the storage), not a local filesystem path (#23511).
|
|
|
+ """
|
|
|
+ script_content = (
|
|
|
+ b"from extras.scripts import Script\n\n\n"
|
|
|
+ b"class TestScript(Script):\n pass\n"
|
|
|
+ )
|
|
|
+
|
|
|
+ class _Storage:
|
|
|
+ # Deliberately not a ScriptFileSystemStorage: emulates a remote backend (e.g. S3)
|
|
|
+ def open(self, name, mode='rb'):
|
|
|
+ return io.BytesIO(script_content)
|
|
|
+
|
|
|
+ storage = _Storage()
|
|
|
+ module = ScriptModule(file_root='scripts', file_path='example.py')
|
|
|
+ namespaced_key = f'{SCRIPT_MODULE_NAME_PREFIX}example'
|
|
|
+ self.addCleanup(lambda: sys.modules.pop(namespaced_key, None))
|
|
|
+
|
|
|
+ with (
|
|
|
+ patch('core.models.files.storages') as core_storages,
|
|
|
+ patch('extras.models.mixins.storages') as mock_storages,
|
|
|
+ ):
|
|
|
+ core_storages.backends = {'scripts': {}}
|
|
|
+ core_storages.create_storage.return_value = storage
|
|
|
+ mock_storages.__getitem__.return_value = storage
|
|
|
+ # No local root is prepended for non-local backends
|
|
|
+ self.assertEqual(module.full_path, 'example.py')
|
|
|
+ loaded = module.get_module()
|
|
|
+
|
|
|
+ self.assertEqual(loaded.__file__, 'example.py')
|
|
|
+
|
|
|
+ def test_code_objects_named_after_storage_location(self):
|
|
|
+ """
|
|
|
+ Code objects of a loaded script must be named after the script's storage location,
|
|
|
+ so tracebacks identify the script instead of showing '<string>' (#23511).
|
|
|
+ """
|
|
|
+ script_content = (
|
|
|
+ b"from extras.scripts import Script\n\n\n"
|
|
|
+ b"class TestScript(Script):\n"
|
|
|
+ b" def run(self):\n"
|
|
|
+ b" raise ValueError('boom')\n"
|
|
|
+ )
|
|
|
+
|
|
|
+ class _Storage:
|
|
|
+ def open(self, name, mode='rb'):
|
|
|
+ return io.BytesIO(script_content)
|
|
|
+
|
|
|
+ module = ScriptModule(file_root='scripts', file_path='example.py')
|
|
|
+ namespaced_key = f'{SCRIPT_MODULE_NAME_PREFIX}example'
|
|
|
+ self.addCleanup(lambda: sys.modules.pop(namespaced_key, None))
|
|
|
+
|
|
|
+ with patch('extras.models.mixins.storages') as mock_storages:
|
|
|
+ mock_storages.__getitem__.return_value = _Storage()
|
|
|
+ loaded = module.get_module()
|
|
|
+
|
|
|
+ self.assertEqual(loaded.TestScript.run.__code__.co_filename, module.full_path)
|
|
|
+
|
|
|
+ # The storage location also appears in tracebacks from script code
|
|
|
+ try:
|
|
|
+ loaded.TestScript().run()
|
|
|
+ self.fail('expected ValueError from script code')
|
|
|
+ except ValueError:
|
|
|
+ formatted = traceback.format_exc()
|
|
|
+ self.assertIn(f'File "{module.full_path}"', formatted)
|
|
|
+
|
|
|
|
|
|
class ScriptMetaValidationTestCase(TestCase):
|
|
|
"""
|