tests.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. import re
  2. from io import StringIO
  3. from unittest import mock, skipUnless
  4. from django.core.management import call_command
  5. from django.db import connection
  6. from django.test import TestCase, skipUnlessDBFeature
  7. from django.utils.encoding import force_text
  8. from .models import ColumnTypes
  9. class InspectDBTestCase(TestCase):
  10. def test_stealth_table_name_filter_option(self):
  11. out = StringIO()
  12. # Lets limit the introspection to tables created for models of this
  13. # application
  14. call_command('inspectdb',
  15. table_name_filter=lambda tn: tn.startswith('inspectdb_'),
  16. stdout=out)
  17. error_message = "inspectdb has examined a table that should have been filtered out."
  18. # contrib.contenttypes is one of the apps always installed when running
  19. # the Django test suite, check that one of its tables hasn't been
  20. # inspected
  21. self.assertNotIn("class DjangoContentType(models.Model):", out.getvalue(), msg=error_message)
  22. def test_table_option(self):
  23. """
  24. inspectdb can inspect a subset of tables by passing the table names as
  25. arguments.
  26. """
  27. out = StringIO()
  28. call_command('inspectdb', 'inspectdb_people', stdout=out)
  29. output = out.getvalue()
  30. self.assertIn('class InspectdbPeople(models.Model):', output)
  31. self.assertNotIn("InspectdbPeopledata", output)
  32. def make_field_type_asserter(self):
  33. """Call inspectdb and return a function to validate a field type in its output"""
  34. out = StringIO()
  35. call_command('inspectdb', 'inspectdb_columntypes', stdout=out)
  36. output = out.getvalue()
  37. def assertFieldType(name, definition):
  38. out_def = re.search(r'^\s*%s = (models.*)$' % name, output, re.MULTILINE).groups()[0]
  39. self.assertEqual(definition, out_def)
  40. return assertFieldType
  41. def test_field_types(self):
  42. """Test introspection of various Django field types"""
  43. assertFieldType = self.make_field_type_asserter()
  44. # Inspecting Oracle DB doesn't produce correct results (#19884):
  45. # - it reports fields as blank=True when they aren't.
  46. if not connection.features.interprets_empty_strings_as_nulls:
  47. assertFieldType('char_field', "models.CharField(max_length=10)")
  48. assertFieldType('null_char_field', "models.CharField(max_length=10, blank=True, null=True)")
  49. assertFieldType('email_field', "models.CharField(max_length=254)")
  50. assertFieldType('file_field', "models.CharField(max_length=100)")
  51. assertFieldType('file_path_field', "models.CharField(max_length=100)")
  52. assertFieldType('slug_field', "models.CharField(max_length=50)")
  53. assertFieldType('text_field', "models.TextField()")
  54. assertFieldType('url_field', "models.CharField(max_length=200)")
  55. assertFieldType('date_field', "models.DateField()")
  56. assertFieldType('date_time_field', "models.DateTimeField()")
  57. if connection.features.can_introspect_ip_address_field:
  58. assertFieldType('gen_ip_address_field', "models.GenericIPAddressField()")
  59. elif not connection.features.interprets_empty_strings_as_nulls:
  60. assertFieldType('gen_ip_address_field', "models.CharField(max_length=39)")
  61. if connection.features.can_introspect_time_field:
  62. assertFieldType('time_field', "models.TimeField()")
  63. if connection.features.has_native_uuid_field:
  64. assertFieldType('uuid_field', "models.UUIDField()")
  65. elif not connection.features.interprets_empty_strings_as_nulls:
  66. assertFieldType('uuid_field', "models.CharField(max_length=32)")
  67. def test_number_field_types(self):
  68. """Test introspection of various Django field types"""
  69. assertFieldType = self.make_field_type_asserter()
  70. if not connection.features.can_introspect_autofield:
  71. assertFieldType('id', "models.IntegerField(primary_key=True) # AutoField?")
  72. if connection.features.can_introspect_big_integer_field:
  73. assertFieldType('big_int_field', "models.BigIntegerField()")
  74. else:
  75. assertFieldType('big_int_field', "models.IntegerField()")
  76. bool_field = ColumnTypes._meta.get_field('bool_field')
  77. bool_field_type = connection.features.introspected_boolean_field_type(bool_field)
  78. assertFieldType('bool_field', "models.{}()".format(bool_field_type))
  79. null_bool_field = ColumnTypes._meta.get_field('null_bool_field')
  80. null_bool_field_type = connection.features.introspected_boolean_field_type(null_bool_field)
  81. if 'BooleanField' in null_bool_field_type:
  82. assertFieldType('null_bool_field', "models.{}()".format(null_bool_field_type))
  83. else:
  84. if connection.features.can_introspect_null:
  85. assertFieldType('null_bool_field', "models.{}(blank=True, null=True)".format(null_bool_field_type))
  86. else:
  87. assertFieldType('null_bool_field', "models.{}()".format(null_bool_field_type))
  88. if connection.features.can_introspect_decimal_field:
  89. assertFieldType('decimal_field', "models.DecimalField(max_digits=6, decimal_places=1)")
  90. else: # Guessed arguments on SQLite, see #5014
  91. assertFieldType('decimal_field', "models.DecimalField(max_digits=10, decimal_places=5) "
  92. "# max_digits and decimal_places have been guessed, "
  93. "as this database handles decimal fields as float")
  94. assertFieldType('float_field', "models.FloatField()")
  95. assertFieldType('int_field', "models.IntegerField()")
  96. if connection.features.can_introspect_positive_integer_field:
  97. assertFieldType('pos_int_field', "models.PositiveIntegerField()")
  98. else:
  99. assertFieldType('pos_int_field', "models.IntegerField()")
  100. if connection.features.can_introspect_positive_integer_field:
  101. if connection.features.can_introspect_small_integer_field:
  102. assertFieldType('pos_small_int_field', "models.PositiveSmallIntegerField()")
  103. else:
  104. assertFieldType('pos_small_int_field', "models.PositiveIntegerField()")
  105. else:
  106. if connection.features.can_introspect_small_integer_field:
  107. assertFieldType('pos_small_int_field', "models.SmallIntegerField()")
  108. else:
  109. assertFieldType('pos_small_int_field', "models.IntegerField()")
  110. if connection.features.can_introspect_small_integer_field:
  111. assertFieldType('small_int_field', "models.SmallIntegerField()")
  112. else:
  113. assertFieldType('small_int_field', "models.IntegerField()")
  114. @skipUnlessDBFeature('can_introspect_foreign_keys')
  115. def test_attribute_name_not_python_keyword(self):
  116. out = StringIO()
  117. # Lets limit the introspection to tables created for models of this
  118. # application
  119. call_command('inspectdb',
  120. table_name_filter=lambda tn: tn.startswith('inspectdb_'),
  121. stdout=out)
  122. output = out.getvalue()
  123. error_message = "inspectdb generated an attribute name which is a python keyword"
  124. # Recursive foreign keys should be set to 'self'
  125. self.assertIn("parent = models.ForeignKey('self', models.DO_NOTHING)", output)
  126. self.assertNotIn(
  127. "from = models.ForeignKey(InspectdbPeople, models.DO_NOTHING)",
  128. output,
  129. msg=error_message,
  130. )
  131. # As InspectdbPeople model is defined after InspectdbMessage, it should be quoted
  132. self.assertIn(
  133. "from_field = models.ForeignKey('InspectdbPeople', models.DO_NOTHING, db_column='from_id')",
  134. output,
  135. )
  136. self.assertIn(
  137. "people_pk = models.ForeignKey(InspectdbPeople, models.DO_NOTHING, primary_key=True)",
  138. output,
  139. )
  140. self.assertIn(
  141. "people_unique = models.ForeignKey(InspectdbPeople, models.DO_NOTHING, unique=True)",
  142. output,
  143. )
  144. def test_digits_column_name_introspection(self):
  145. """Introspection of column names consist/start with digits (#16536/#17676)"""
  146. out = StringIO()
  147. call_command('inspectdb', 'inspectdb_digitsincolumnname', stdout=out)
  148. output = out.getvalue()
  149. error_message = "inspectdb generated a model field name which is a number"
  150. self.assertNotIn(" 123 = models.CharField", output, msg=error_message)
  151. self.assertIn("number_123 = models.CharField", output)
  152. error_message = "inspectdb generated a model field name which starts with a digit"
  153. self.assertNotIn(" 4extra = models.CharField", output, msg=error_message)
  154. self.assertIn("number_4extra = models.CharField", output)
  155. self.assertNotIn(" 45extra = models.CharField", output, msg=error_message)
  156. self.assertIn("number_45extra = models.CharField", output)
  157. def test_special_column_name_introspection(self):
  158. """
  159. Introspection of column names containing special characters,
  160. unsuitable for Python identifiers
  161. """
  162. out = StringIO()
  163. call_command('inspectdb',
  164. table_name_filter=lambda tn: tn.startswith('inspectdb_special'),
  165. stdout=out)
  166. output = out.getvalue()
  167. base_name = 'Field' if not connection.features.uppercases_column_names else 'field'
  168. self.assertIn("field = models.IntegerField()", output)
  169. self.assertIn("field_field = models.IntegerField(db_column='%s_')" % base_name, output)
  170. self.assertIn("field_field_0 = models.IntegerField(db_column='%s__')" % base_name, output)
  171. self.assertIn("field_field_1 = models.IntegerField(db_column='__field')", output)
  172. self.assertIn("prc_x = models.IntegerField(db_column='prc(%) x')", output)
  173. self.assertIn("tamaño = models.IntegerField()", output)
  174. def test_table_name_introspection(self):
  175. """
  176. Introspection of table names containing special characters,
  177. unsuitable for Python identifiers
  178. """
  179. out = StringIO()
  180. call_command('inspectdb',
  181. table_name_filter=lambda tn: tn.startswith('inspectdb_special'),
  182. stdout=out)
  183. output = out.getvalue()
  184. self.assertIn("class InspectdbSpecialTableName(models.Model):", output)
  185. def test_managed_models(self):
  186. """By default the command generates models with `Meta.managed = False` (#14305)"""
  187. out = StringIO()
  188. call_command('inspectdb', 'inspectdb_columntypes', stdout=out)
  189. output = out.getvalue()
  190. self.longMessage = False
  191. self.assertIn(" managed = False", output, msg='inspectdb should generate unmanaged models.')
  192. def test_unique_together_meta(self):
  193. out = StringIO()
  194. call_command('inspectdb', 'inspectdb_uniquetogether', stdout=out)
  195. output = out.getvalue()
  196. unique_re = re.compile(r'.*unique_together = \((.+),\).*')
  197. unique_together_match = re.findall(unique_re, output)
  198. # There should be one unique_together tuple.
  199. self.assertEqual(len(unique_together_match), 1)
  200. fields = unique_together_match[0]
  201. # Fields with db_column = field name.
  202. self.assertIn("('field1', 'field2')", fields)
  203. # Fields from columns whose names are Python keywords.
  204. self.assertIn("('field1', 'field2')", fields)
  205. # Fields whose names normalize to the same Python field name and hence
  206. # are given an integer suffix.
  207. self.assertIn("('non_unique_column', 'non_unique_column_0')", fields)
  208. @skipUnless(connection.vendor == 'sqlite',
  209. "Only patched sqlite's DatabaseIntrospection.data_types_reverse for this test")
  210. def test_custom_fields(self):
  211. """
  212. Introspection of columns with a custom field (#21090)
  213. """
  214. out = StringIO()
  215. orig_data_types_reverse = connection.introspection.data_types_reverse
  216. try:
  217. connection.introspection.data_types_reverse = {
  218. 'text': 'myfields.TextField',
  219. 'bigint': 'BigIntegerField',
  220. }
  221. call_command('inspectdb', 'inspectdb_columntypes', stdout=out)
  222. output = out.getvalue()
  223. self.assertIn("text_field = myfields.TextField()", output)
  224. self.assertIn("big_int_field = models.BigIntegerField()", output)
  225. finally:
  226. connection.introspection.data_types_reverse = orig_data_types_reverse
  227. def test_introspection_errors(self):
  228. """
  229. Introspection errors should not crash the command, and the error should
  230. be visible in the output.
  231. """
  232. out = StringIO()
  233. with mock.patch('django.db.backends.base.introspection.BaseDatabaseIntrospection.table_names',
  234. return_value=['nonexistent']):
  235. call_command('inspectdb', stdout=out)
  236. output = force_text(out.getvalue())
  237. self.assertIn("# Unable to inspect table 'nonexistent'", output)
  238. # The error message depends on the backend
  239. self.assertIn("# The error was:", output)