Closed dlareau closed 4 years ago
I've managed to shrink down the problem causing code quite a bit, still no idea whats happening.
The project is now super simple, its basically just the django-admin startproject/startapp with the urls, templates, tests and models inserted:
project1/
├── app1
│ ├── fixtures
│ │ └── basic.json
│ ├── __init__.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ ├── __init__.py
│ ├── models.py
│ ├── templates
│ │ └── index.html
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── manage.py
├── project1
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
└── requirements.txt
Below are the whole contents of all relevant files. app1/fixtures/basic.json
[
{
"fields": {
"name": "Example"
},
"model": "app1.model1",
"pk": 1
}
]
app1/migrations/0001_initial.py
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name='Model1',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
],
),
]
app1/models.py
from django.db import models
class Model1(models.Model):
name = models.CharField(max_length=200)
app1/templates/index.html
<html>
</html>
app1/tests.py
from django.test import TestCase
from django.urls import reverse
class InfoTests(TestCase):
fixtures = ["basic_hunt"]
def test_index(self):
response = self.client.get("/")
app1/urls.py
from django.urls import path
from . import views
app_name = "app1"
urlpatterns = [
path('', views.index, name='index'),
]
app1/views.py
from django.shortcuts import render
from .models import Model1
def index(request):
curr_model = Model1.objects.get(pk=1)
return render(request, "index.html", {'curr_model': curr_model})
project1/urls.py
from django.urls import include, path
urlpatterns = [
path('', include('app1.urls')),
]
project1/settings.py
from os.path import dirname, abspath
import codecs
codecs.register(lambda name: codecs.lookup('utf8') if name == 'utf8mb4' else None)
BASE_DIR = dirname(dirname(dirname(abspath(__file__))))
# Application definition
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'app1',
'django_nose',
)
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
MIDDLEWARE = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware',
)
ROOT_URLCONF = 'project1.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.template.context_processors.static',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'django.template.context_processors.media',
],
},
},
]
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'America/New_York'
USE_I18N = True
USE_L10N = True
USE_TZ = True
DEBUG = True
SECRET_KEY = 'q#)ASes1tP4CAOGnn0oo6N+xM%ZgT2lf1ZVTp2QO)xkF4Jv&*r'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'puzzlehunt_db',
'HOST': 'localhost',
'PORT': '3306',
'USER': 'hunt',
'PASSWORD': 'test',
'OPTIONS': {'charset': 'utf8mb4'},
}
}
INTERNAL_IPS = '127.0.0.1'
requirements.txt
Django==2.0
mysqlclient==1.4.6
django-nose==1.4.5
Thats literally every line of code in this project and it is still failing on Django 2.0, 2.1 and 2.2, with the same error as above, and still passes when removing the TEST_RUNNER line from settings.py
Because of testcases change in django 2.0, commit is True
and connection.close()
is run, but I don't know why connection.close()
cause the problem.
def _foreign_key_ignoring_handle(self, *fixture_labels, **options):
"""Wrap the the stock loaddata to ignore foreign key checks.
This allows loading circular references from fixtures, and is
monkeypatched into place in setup_databases().
"""
using = options.get('database', DEFAULT_DB_ALIAS)
commit = options.get('commit', True)
connection = connections[using]
# MySQL stinks at loading circular references:
if uses_mysql(connection):
cursor = connection.cursor()
cursor.execute('SET foreign_key_checks = 0')
_old_handle(self, *fixture_labels, **options)
if uses_mysql(connection):
cursor = connection.cursor()
cursor.execute('SET foreign_key_checks = 1')
if commit:
connection.close()
call_command('loaddata', *cls.fixtures, **{
'verbosity': 0,
'commit': False,
'database': db_name,
})
call_command('loaddata', *cls.fixtures, **{'verbosity': 0, 'database': db_name})
Thanks for tracking that down @Litrop! After a bit of looking I think I have most of it.
It would seem this just needs a code update on django-nose's side. The _foreign_key_ignoring_handle
method was meant to take the place of the handle
function for the loaddata
command. The problem is that loaddata hasn't accepted the "commit" argument since Django 1.5.
Django 2.0 just happened to include a code change that removed the passing of old unused command flags to call_command
. This meant that the TestCase's normal commit=False
got turned into django-nose's options.get('commit', True)
, breaking everything.
As for why connection.close() breaks things, some of the details go over my head, but it appears that the original if commit: connection.close
line from django 1.5 had the following logic next to it:
# Close the DB connection -- unless we're still in a transaction. This
# is required as a workaround for an edge case in MySQL: if the same
# connection is used to create tables, load data, and query, the query
# can return incorrect results. See Django #7572, MySQL #37735.
Recent versions of django use the following logic instead:
if transaction.get_autocommit(self.using):
connections[self.using].close()
It would seem that django-nose should either update to using the same logic, or just let the call to _old_handle
deal with it as I think it probably would.
Another possible note, I don't know enough about MySQL stuff to say, but is it possible that the whole _foreign_key_ignoring_handle
method might now not be needed? Maybe either MySQL or Django's loaddata.handle function have improved to the point of making the patching unnecessary.
Pull request #308 made, all functional tests appear to pass.
When will we see this change on pypi?
@zefciu This project appears to be dead, there haven't been any commits in months and my pull request was never merged. I gave up and stopped using it in my personal projects.
Hey, @jwhitlock, is this band still touring? #308 could use a little love...
Thanks @dlareau for the bug, analysis, and fix! I've filed #314 for the follow-on issue of 1) testing fixture loading in this project, and 2) evaluating if the MySQL fixture loading workarounds are still needed.
I've merged #308, it ~will be~ is in release 1.4.7.
EDIT: See update in second post, I've shrunk the example code down quite a bit and I'm still having this issue.
Double edit: Fix proposed and pull request submitted, waiting on confirmation that the fix is valid.
I ran into this while trying to upgrade from Django 1.11 to 2.2
Previously on Django 1.11 I had django-nose installed and all the tests ran without issue. When I tried to upgrade to Django 2, every test that touches the database in any way now throws the error:
This only happens on Django 2.X, and only when django-nose's test runner is enabled. Removing the line
from settings.py fixes the issue.
These errors also don't seem to come up at all outside of unit testing (which I guess makes sense if the issue is somehow related to django-nose).
Here is an example test from the test.py file:
I'm seeing these issues locally locally on Debian 9 (Stretch), but it also seems to fail in the same way on travis-ci (Which appears to be ubuntu 16.04): https://travis-ci.org/dlareau/puzzlehunt_server/builds/636368209
mysql --version:
pip freeze: (Currently at Django 2.0, but same error on 2.1 and 2.2)
Database settings:
I've looked into each of the listed errors (the Transaction issue/the "OperationalError"). A lot of what I found searching keywords wasn't applicable, though I tried many of the solutions anyway to no avail.
If it is relevant, the source code for my whole project can be found here: https://github.com/dlareau/puzzlehunt_server/tree/development
Let me know if there is any other info I can provide.
Thanks for any help!