summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
authorSimon Charette <charette.s@gmail.com>2018-11-23 21:22:09 -0500
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2020-05-15 20:22:52 +0200
commit3cf80d3fcf7446afdde16a2be515c423f720e54d (patch)
treef68aacd6e8f3dab3e250504d16bb1a51ed929bf4 /django
parent1dd96f731df3ca9df0e3ff83a9a8e723dc7e8685 (diff)
Fixed #31395 -- Made setUpTestData enforce in-memory data isolation.
Since it's introduction in Django 1.8 setUpTestData has been suffering from a documented but confusing caveat due to its sharing of attributes assigned during its execution with all test instances. By keeping track of class attributes assigned during the setUpTestData phase its possible to ensure only deep copies are provided to test instances on attribute retreival and prevent manual setUp gymnastic to work around the previous lack of in-memory data isolation. Thanks Adam Johnson for the extensive review.
Diffstat (limited to 'django')
-rw-r--r--django/test/testcases.py61
1 files changed, 60 insertions, 1 deletions
diff --git a/django/test/testcases.py b/django/test/testcases.py
index 7ebddf80e5..5787dc0115 100644
--- a/django/test/testcases.py
+++ b/django/test/testcases.py
@@ -5,9 +5,10 @@ import posixpath
import sys
import threading
import unittest
+import warnings
from collections import Counter
from contextlib import contextmanager
-from copy import copy
+from copy import copy, deepcopy
from difflib import get_close_matches
from functools import wraps
from unittest.suite import _DebugResult
@@ -40,6 +41,7 @@ from django.test.utils import (
CaptureQueriesContext, ContextList, compare_xml, modify_settings,
override_settings,
)
+from django.utils.deprecation import RemovedInDjango41Warning
from django.utils.functional import classproperty
from django.views.static import serve
@@ -1071,6 +1073,59 @@ def connections_support_transactions(aliases=None):
return all(conn.features.supports_transactions for conn in conns)
+class TestData:
+ """
+ Descriptor to provide TestCase instance isolation for attributes assigned
+ during the setUpTestData() phase.
+
+ Allow safe alteration of objects assigned in setUpTestData() by test
+ methods by exposing deep copies instead of the original objects.
+
+ Objects are deep copied using a memo kept on the test case instance in
+ order to maintain their original relationships.
+ """
+ memo_attr = '_testdata_memo'
+
+ def __init__(self, name, data):
+ self.name = name
+ self.data = data
+
+ def get_memo(self, testcase):
+ try:
+ memo = getattr(testcase, self.memo_attr)
+ except AttributeError:
+ memo = {}
+ setattr(testcase, self.memo_attr, memo)
+ return memo
+
+ def __get__(self, instance, owner):
+ if instance is None:
+ return self.data
+ memo = self.get_memo(instance)
+ try:
+ data = deepcopy(self.data, memo)
+ except TypeError:
+ # RemovedInDjango41Warning.
+ msg = (
+ "Assigning objects which don't support copy.deepcopy() during "
+ "setUpTestData() is deprecated. Either assign the %s "
+ "attribute during setUpClass() or setUp(), or add support for "
+ "deepcopy() to %s.%s.%s."
+ ) % (
+ self.name,
+ owner.__module__,
+ owner.__qualname__,
+ self.name,
+ )
+ warnings.warn(msg, category=RemovedInDjango41Warning, stacklevel=2)
+ data = self.data
+ setattr(instance, self.name, data)
+ return data
+
+ def __repr__(self):
+ return '<TestData: name=%r, data=%r>' % (self.name, self.data)
+
+
class TestCase(TransactionTestCase):
"""
Similar to TransactionTestCase, but use `transaction.atomic()` to achieve
@@ -1119,12 +1174,16 @@ class TestCase(TransactionTestCase):
cls._rollback_atomics(cls.cls_atomics)
cls._remove_databases_failures()
raise
+ pre_attrs = cls.__dict__.copy()
try:
cls.setUpTestData()
except Exception:
cls._rollback_atomics(cls.cls_atomics)
cls._remove_databases_failures()
raise
+ for name, value in cls.__dict__.items():
+ if value is not pre_attrs.get(name):
+ setattr(cls, name, TestData(name, value))
@classmethod
def tearDownClass(cls):