Skip to content

Commit 59a8d03

Browse files
authored
Django Test Compatibility (#23935)
implements and thus closes #22206 resolves #73!
1 parent b872cb4 commit 59a8d03

26 files changed

+702
-26
lines changed

build/test-requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,6 @@ freezegun
2424

2525
# testing custom pytest plugin require the use of named pipes
2626
namedpipe; platform_system == "Windows"
27+
28+
# typing for Django files
29+
django-stubs

python_files/tests/pytestadapter/helpers.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -193,17 +193,35 @@ def _run_test_code(proc_args: List[str], proc_env, proc_cwd: str, completed: thr
193193

194194

195195
def runner(args: List[str]) -> Optional[List[Dict[str, Any]]]:
196-
"""Run the pytest discovery and return the JSON data from the server."""
196+
"""Run a subprocess and a named-pipe to listen for messages at the same time with threading."""
197197
print("\n Running python test subprocess with cwd set to: ", TEST_DATA_PATH)
198198
return runner_with_cwd(args, TEST_DATA_PATH)
199199

200200

201201
def runner_with_cwd(args: List[str], path: pathlib.Path) -> Optional[List[Dict[str, Any]]]:
202-
"""Run the pytest discovery and return the JSON data from the server."""
203-
process_args: List[str] = [sys.executable, "-m", "pytest", "-p", "vscode_pytest", "-s", *args]
202+
"""Run a subprocess and a named-pipe to listen for messages at the same time with threading."""
203+
return runner_with_cwd_env(args, path, {})
204+
205+
206+
def runner_with_cwd_env(
207+
args: List[str], path: pathlib.Path, env_add: Dict[str, str]
208+
) -> Optional[List[Dict[str, Any]]]:
209+
"""
210+
Run a subprocess and a named-pipe to listen for messages at the same time with threading.
211+
212+
Includes environment variables to add to the test environment.
213+
"""
214+
process_args: List[str]
215+
pipe_name: str
216+
if "MANAGE_PY_PATH" in env_add:
217+
# If we are running Django, generate a unittest-specific pipe name.
218+
process_args = [sys.executable, *args]
219+
pipe_name = generate_random_pipe_name("unittest-discovery-test")
220+
else:
221+
process_args = [sys.executable, "-m", "pytest", "-p", "vscode_pytest", "-s", *args]
222+
pipe_name = generate_random_pipe_name("pytest-discovery-test")
204223

205224
# Generate pipe name, pipe name specific per OS type.
206-
pipe_name = generate_random_pipe_name("pytest-discovery-test")
207225

208226
# Windows design
209227
if sys.platform == "win32":
@@ -216,6 +234,9 @@ def runner_with_cwd(args: List[str], path: pathlib.Path) -> Optional[List[Dict[s
216234
"PYTHONPATH": os.fspath(pathlib.Path(__file__).parent.parent.parent),
217235
}
218236
)
237+
# if additional environment variables are passed, add them to the environment
238+
if env_add:
239+
env.update(env_add)
219240

220241
completed = threading.Event()
221242

@@ -244,6 +265,9 @@ def runner_with_cwd(args: List[str], path: pathlib.Path) -> Optional[List[Dict[s
244265
"PYTHONPATH": os.fspath(pathlib.Path(__file__).parent.parent.parent),
245266
}
246267
)
268+
# if additional environment variables are passed, add them to the environment
269+
if env_add:
270+
env.update(env_add)
247271
server = UnixPipeServer(pipe_name)
248272
server.start()
249273

@@ -255,10 +279,11 @@ def runner_with_cwd(args: List[str], path: pathlib.Path) -> Optional[List[Dict[s
255279
)
256280
t1.start()
257281

258-
t2 = threading.Thread(
282+
t2: threading.Thread = threading.Thread(
259283
target=_run_test_code,
260284
args=(process_args, env, path, completed),
261285
)
286+
262287
t2.start()
263288

264289
t1.join()
Binary file not shown.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
"""Django's command-line utility for administrative tasks."""
4+
import os
5+
import sys
6+
7+
8+
def main():
9+
"""Run administrative tasks."""
10+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
11+
try:
12+
from django.core.management import execute_from_command_line
13+
except ImportError as exc:
14+
raise ImportError(
15+
"Couldn't import Django. Are you sure it's installed and "
16+
"available on your PYTHONPATH environment variable? Did you "
17+
"forget to activate a virtual environment?"
18+
) from exc
19+
execute_from_command_line(sys.argv)
20+
21+
22+
if __name__ == '__main__':
23+
main()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
import os
4+
5+
from django.core.asgi import get_asgi_application
6+
7+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
8+
9+
application = get_asgi_application()
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
"""
4+
Django settings for mysite project.
5+
6+
Generated by 'django-admin startproject' using Django 3.2.22.
7+
8+
For more information on this file, see
9+
https://docs.djangoproject.com/en/3.2/topics/settings/
10+
11+
For the full list of settings and their values, see
12+
https://docs.djangoproject.com/en/3.2/ref/settings/
13+
"""
14+
15+
from pathlib import Path
16+
17+
# Build paths inside the project like this: BASE_DIR / 'subdir'.
18+
BASE_DIR = Path(__file__).resolve().parent.parent
19+
20+
21+
ALLOWED_HOSTS = []
22+
23+
24+
# Application definition
25+
26+
INSTALLED_APPS = [
27+
"polls.apps.PollsConfig",
28+
"django.contrib.admin",
29+
"django.contrib.auth",
30+
"django.contrib.contenttypes",
31+
"django.contrib.sessions",
32+
"django.contrib.messages",
33+
"django.contrib.staticfiles",
34+
]
35+
36+
MIDDLEWARE = [
37+
'django.middleware.security.SecurityMiddleware',
38+
'django.contrib.sessions.middleware.SessionMiddleware',
39+
'django.middleware.common.CommonMiddleware',
40+
'django.middleware.csrf.CsrfViewMiddleware',
41+
'django.contrib.auth.middleware.AuthenticationMiddleware',
42+
'django.contrib.messages.middleware.MessageMiddleware',
43+
'django.middleware.clickjacking.XFrameOptionsMiddleware',
44+
]
45+
46+
ROOT_URLCONF = 'mysite.urls'
47+
48+
TEMPLATES = [
49+
{
50+
'BACKEND': 'django.template.backends.django.DjangoTemplates',
51+
'DIRS': [],
52+
'APP_DIRS': True,
53+
'OPTIONS': {
54+
'context_processors': [
55+
'django.template.context_processors.debug',
56+
'django.template.context_processors.request',
57+
'django.contrib.auth.context_processors.auth',
58+
'django.contrib.messages.context_processors.messages',
59+
],
60+
},
61+
},
62+
]
63+
64+
WSGI_APPLICATION = 'mysite.wsgi.application'
65+
66+
67+
# Database
68+
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
69+
70+
DATABASES = {
71+
'default': {
72+
'ENGINE': 'django.db.backends.sqlite3',
73+
'NAME': BASE_DIR / 'db.sqlite3',
74+
}
75+
}
76+
77+
78+
79+
80+
# Internationalization
81+
# https://docs.djangoproject.com/en/3.2/topics/i18n/
82+
83+
LANGUAGE_CODE = 'en-us'
84+
85+
TIME_ZONE = 'UTC'
86+
87+
USE_I18N = True
88+
89+
USE_L10N = True
90+
91+
USE_TZ = True
92+
93+
94+
# Static files (CSS, JavaScript, Images)
95+
# https://docs.djangoproject.com/en/3.2/howto/static-files/
96+
97+
STATIC_URL = '/static/'
98+
99+
# Default primary key field type
100+
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
101+
102+
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
from django.contrib import admin
4+
from django.urls import include, path
5+
6+
urlpatterns = [
7+
path("polls/", include("polls.urls")),
8+
path("admin/", admin.site.urls),
9+
]
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
import os
4+
5+
from django.core.wsgi import get_wsgi_application
6+
7+
application = get_wsgi_application()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from django.apps import AppConfig
5+
from django.utils.functional import cached_property
6+
7+
8+
class PollsConfig(AppConfig):
9+
@cached_property
10+
def default_auto_field(self):
11+
return "django.db.models.BigAutoField"
12+
13+
name = "polls"
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Generated by Django 5.0.8 on 2024-08-09 20:04
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
initial = True
10+
11+
dependencies = []
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name="Question",
16+
fields=[
17+
(
18+
"id",
19+
models.BigAutoField(
20+
auto_created=True,
21+
primary_key=True,
22+
serialize=False,
23+
verbose_name="ID",
24+
),
25+
),
26+
("question_text", models.CharField(max_length=200, default="")),
27+
("pub_date", models.DateTimeField(verbose_name="date published", auto_now_add=True)),
28+
],
29+
),
30+
migrations.CreateModel(
31+
name="Choice",
32+
fields=[
33+
(
34+
"id",
35+
models.BigAutoField(
36+
auto_created=True,
37+
primary_key=True,
38+
serialize=False,
39+
verbose_name="ID",
40+
),
41+
),
42+
("choice_text", models.CharField(max_length=200)),
43+
("votes", models.IntegerField(default=0)),
44+
(
45+
"question",
46+
models.ForeignKey(
47+
on_delete=django.db.models.deletion.CASCADE, to="polls.question"
48+
),
49+
),
50+
],
51+
),
52+
]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from django.db import models
5+
from django.utils import timezone
6+
import datetime
7+
8+
9+
class Question(models.Model):
10+
question_text = models.CharField(max_length=200)
11+
pub_date = models.DateTimeField("date published")
12+
def __str__(self):
13+
return self.question_text
14+
def was_published_recently(self):
15+
if self.pub_date > timezone.now():
16+
return False
17+
return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
18+
19+
20+
class Choice(models.Model):
21+
question = models.ForeignKey(Question, on_delete=models.CASCADE)
22+
choice_text = models.CharField(max_length=200)
23+
votes = models.IntegerField()
24+
def __str__(self):
25+
return self.choice_text
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from django.utils import timezone
5+
from django.test import TestCase
6+
from .models import Question
7+
import datetime
8+
9+
class QuestionModelTests(TestCase):
10+
def test_was_published_recently_with_future_question(self):
11+
"""
12+
was_published_recently() returns False for questions whose pub_date
13+
is in the future.
14+
"""
15+
time = timezone.now() + datetime.timedelta(days=30)
16+
future_question: Question = Question.objects.create(pub_date=time)
17+
self.assertIs(future_question.was_published_recently(), False)
18+
19+
def test_was_published_recently_with_future_question_2(self):
20+
"""
21+
was_published_recently() returns False for questions whose pub_date
22+
is in the future.
23+
"""
24+
time = timezone.now() + datetime.timedelta(days=30)
25+
future_question = Question.objects.create(pub_date=time)
26+
self.assertIs(future_question.was_published_recently(), True)
27+
28+
def test_question_creation_and_retrieval(self):
29+
"""
30+
Test that a Question can be created and retrieved from the database.
31+
"""
32+
time = timezone.now()
33+
question = Question.objects.create(pub_date=time, question_text="What's new?")
34+
retrieved_question = Question.objects.get(question_text=question.question_text)
35+
self.assertEqual(question, retrieved_question)
36+
self.assertEqual(retrieved_question.question_text, "What's new?")
37+
self.assertEqual(retrieved_question.pub_date, time)
38+
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from django.urls import path
5+
6+
from . import views
7+
8+
urlpatterns = [
9+
# ex: /polls/
10+
path("", views.index, name="index"),
11+
]
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
from django.http import HttpResponse
4+
from .models import Question # noqa: F401
5+
6+
def index(request):
7+
return HttpResponse("Hello, world. You're at the polls index.")

0 commit comments

Comments
 (0)