Skip to content

Commit 387bb2b

Browse files
authored
Django and Flask template debugging tests (#1317)
Fixes #1172 Fixes #1173
1 parent 94cc8b1 commit 387bb2b

File tree

17 files changed

+369
-14
lines changed

17 files changed

+369
-14
lines changed

.travis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ before_install: |
3636
yarn global add azure-cli
3737
export TRAVIS_PYTHON_PATH=`which python`
3838
install:
39-
- pip install --upgrade -r requirements.txt
40-
- pip install -t ./pythonFiles/experimental/ptvsd git+https://github.com/Microsoft/ptvsd/
39+
- python -m pip install --upgrade -r requirements.txt
40+
- python -m pip install -t ./pythonFiles/experimental/ptvsd git+https://github.com/Microsoft/ptvsd/
4141
- yarn
4242

4343
script:

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ pytest
1010
fabric
1111
numba
1212
rope
13+
flask
14+
django

src/test/common.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { IS_MULTI_ROOT_TEST } from './initialize';
77
const fileInNonRootWorkspace = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'dummy.py');
88
export const rootWorkspaceUri = getWorkspaceRoot();
99

10+
export const PYTHON_PATH = getPythonPath();
11+
1012
export type PythonSettingKeys = 'workspaceSymbols.enabled' | 'pythonPath' |
1113
'linting.lintOnSave' |
1214
'linting.enabled' | 'linting.pylintEnabled' |
@@ -118,3 +120,12 @@ const globalPythonPathSetting = workspace.getConfiguration('python').inspect('py
118120
export const clearPythonPathInWorkspaceFolder = async (resource: string | Uri) => retryAsync(setPythonPathInWorkspace)(resource, ConfigurationTarget.WorkspaceFolder);
119121
export const setPythonPathInWorkspaceRoot = async (pythonPath: string) => retryAsync(setPythonPathInWorkspace)(undefined, ConfigurationTarget.Workspace, pythonPath);
120122
export const resetGlobalPythonPathSetting = async () => retryAsync(restoreGlobalPythonPathSetting)();
123+
124+
function getPythonPath(): string {
125+
// tslint:disable-next-line:no-unsafe-any
126+
if (process.env.TRAVIS_PYTHON_PATH && fs.existsSync(process.env.TRAVIS_PYTHON_PATH)) {
127+
// tslint:disable-next-line:no-unsafe-any
128+
return process.env.TRAVIS_PYTHON_PATH;
129+
}
130+
return 'python';
131+
}

src/test/debugger/utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ export async function validateVariablesInFrame(debugClient: DebugClient,
6969
export function makeHttpRequest(uri: string): Promise<string> {
7070
return new Promise<string>((resolve, reject) => {
7171
request.get(uri, (error: any, response: request.Response, body: any) => {
72+
if (error) {
73+
return reject(error);
74+
}
7275
if (response.statusCode !== 200) {
7376
reject(new Error(`Status code = ${response.statusCode}`));
7477
} else {
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
// tslint:disable:no-suspicious-comment max-func-body-length no-invalid-this no-var-requires no-require-imports no-any no-http-string no-string-literal no-console
7+
8+
import { expect } from 'chai';
9+
import * as getFreePort from 'get-port';
10+
import * as path from 'path';
11+
import { DebugClient } from 'vscode-debugadapter-testsupport';
12+
import { EXTENSION_ROOT_DIR } from '../../client/common/constants';
13+
import { noop } from '../../client/common/core.utils';
14+
import { DebugOptions, LaunchRequestArguments } from '../../client/debugger/Common/Contracts';
15+
import { PYTHON_PATH, sleep } from '../common';
16+
import { IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize';
17+
import { DEBUGGER_TIMEOUT } from './common/constants';
18+
import { continueDebugging, createDebugAdapter, ExpectedVariable, hitHttpBreakpoint, makeHttpRequest, validateVariablesInFrame } from './utils';
19+
20+
let testCounter = 0;
21+
const debuggerType = 'pythonExperimental';
22+
suite(`Django and Flask Debugging: ${debuggerType}`, () => {
23+
let debugClient: DebugClient;
24+
setup(async function () {
25+
if (!IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) {
26+
this.skip();
27+
}
28+
this.timeout(5 * DEBUGGER_TIMEOUT);
29+
const coverageDirectory = path.join(EXTENSION_ROOT_DIR, `debug_coverage_django_flask${testCounter += 1}`);
30+
debugClient = await createDebugAdapter(coverageDirectory);
31+
});
32+
teardown(async () => {
33+
// Wait for a second before starting another test (sometimes, sockets take a while to get closed).
34+
await sleep(1000);
35+
try {
36+
await debugClient.stop().catch(noop);
37+
// tslint:disable-next-line:no-empty
38+
} catch (ex) { }
39+
await sleep(1000);
40+
});
41+
function buildLaunchArgs(workspaceDirectory: string): LaunchRequestArguments {
42+
const env = {};
43+
// tslint:disable-next-line:no-string-literal
44+
env['PYTHONPATH'] = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'experimental', 'ptvsd');
45+
46+
// tslint:disable-next-line:no-unnecessary-local-variable
47+
const options: LaunchRequestArguments = {
48+
cwd: workspaceDirectory,
49+
program: '',
50+
debugOptions: [DebugOptions.RedirectOutput],
51+
pythonPath: PYTHON_PATH,
52+
args: [],
53+
env,
54+
envFile: '',
55+
logToFile: true,
56+
type: debuggerType
57+
};
58+
59+
return options;
60+
}
61+
async function buildFlaskLaunchArgs(workspaceDirectory: string) {
62+
const port = await getFreePort({ host: 'localhost' });
63+
const options = buildLaunchArgs(workspaceDirectory);
64+
65+
options.env!['FLASK_APP'] = path.join(workspaceDirectory, 'run.py');
66+
options.module = 'flask';
67+
options.debugOptions = [DebugOptions.RedirectOutput, DebugOptions.Jinja];
68+
options.args = [
69+
'run',
70+
'--no-debugger',
71+
'--no-reload',
72+
'--port',
73+
`${port}`
74+
];
75+
76+
return { options, port };
77+
}
78+
async function buildDjangoLaunchArgs(workspaceDirectory: string) {
79+
const port = await getFreePort({ host: 'localhost' });
80+
const options = buildLaunchArgs(workspaceDirectory);
81+
82+
options.program = path.join(workspaceDirectory, 'manage.py');
83+
options.debugOptions = [DebugOptions.RedirectOutput, DebugOptions.Django];
84+
options.args = [
85+
'runserver',
86+
'--noreload',
87+
'--nothreading',
88+
`${port}`
89+
];
90+
91+
return { options, port };
92+
}
93+
94+
async function testTemplateDebugging(launchArgs: LaunchRequestArguments, port: number, viewFile: string, viewLine: number, templateFile: string, templateLine: number) {
95+
await Promise.all([
96+
debugClient.configurationSequence(),
97+
debugClient.launch(launchArgs),
98+
debugClient.waitForEvent('initialized'),
99+
debugClient.waitForEvent('process'),
100+
debugClient.waitForEvent('thread')
101+
]);
102+
103+
const httpResult = await makeHttpRequest(`http://localhost:${port}`);
104+
105+
expect(httpResult).to.contain('Hello this_is_a_value_from_server');
106+
expect(httpResult).to.contain('Hello this_is_another_value_from_server');
107+
108+
await hitHttpBreakpoint(debugClient, `http://localhost:${port}`, viewFile, viewLine);
109+
110+
await continueDebugging(debugClient);
111+
await debugClient.setBreakpointsRequest({ breakpoints: [], lines: [], source: { path: viewFile } });
112+
113+
// Template debugging.
114+
const [stackTrace, htmlResultPromise] = await hitHttpBreakpoint(debugClient, `http://localhost:${port}`, templateFile, templateLine);
115+
116+
// Wait for breakpoint to hit
117+
const expectedVariables: ExpectedVariable[] = [
118+
{ name: 'value_from_server', type: 'str', value: '\'this_is_a_value_from_server\'' },
119+
{ name: 'another_value_from_server', type: 'str', value: '\'this_is_another_value_from_server\'' }
120+
];
121+
await validateVariablesInFrame(debugClient, stackTrace, expectedVariables, 1);
122+
123+
await debugClient.setBreakpointsRequest({ breakpoints: [], lines: [], source: { path: templateFile } });
124+
await continueDebugging(debugClient);
125+
126+
const htmlResult = await htmlResultPromise;
127+
expect(htmlResult).to.contain('Hello this_is_a_value_from_server');
128+
expect(htmlResult).to.contain('Hello this_is_another_value_from_server');
129+
}
130+
131+
test('Test Flask Route and Template debugging', async () => {
132+
const workspaceDirectory = path.join(EXTENSION_ROOT_DIR, 'src', 'testMultiRootWkspc', 'workspace5', 'flaskApp');
133+
const { options, port } = await buildFlaskLaunchArgs(workspaceDirectory);
134+
135+
await testTemplateDebugging(options, port,
136+
path.join(workspaceDirectory, 'run.py'), 7,
137+
path.join(workspaceDirectory, 'templates', 'index.html'), 6);
138+
});
139+
140+
test('Test Django Route and Template debugging', async () => {
141+
const workspaceDirectory = path.join(EXTENSION_ROOT_DIR, 'src', 'testMultiRootWkspc', 'workspace5', 'djangoApp');
142+
const { options, port } = await buildDjangoLaunchArgs(workspaceDirectory);
143+
144+
await testTemplateDebugging(options, port,
145+
path.join(workspaceDirectory, 'home', 'views.py'), 10,
146+
path.join(workspaceDirectory, 'home', 'templates', 'index.html'), 6);
147+
});
148+
});

src/test/initialize.ts

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as path from 'path';
55
import * as vscode from 'vscode';
66
import { PythonSettings } from '../client/common/configSettings';
77
import { activated } from '../client/extension';
8-
import { clearPythonPathInWorkspaceFolder, resetGlobalPythonPathSetting, setPythonPathInWorkspaceRoot } from './common';
8+
import { clearPythonPathInWorkspaceFolder, PYTHON_PATH, resetGlobalPythonPathSetting, setPythonPathInWorkspaceRoot } from './common';
99

1010
export * from './constants';
1111

@@ -16,8 +16,6 @@ const workspace3Uri = vscode.Uri.file(path.join(multirootPath, 'workspace3'));
1616
//First thing to be executed.
1717
process.env['VSC_PYTHON_CI_TEST'] = '1';
1818

19-
const PYTHON_PATH = getPythonPath();
20-
2119
// Ability to use custom python environments for testing
2220
export async function initializePython() {
2321
await resetGlobalPythonPathSetting();
@@ -54,12 +52,3 @@ export async function closeActiveWindows(): Promise<void> {
5452
}, 15000);
5553
});
5654
}
57-
58-
function getPythonPath(): string {
59-
// tslint:disable-next-line:no-unsafe-any
60-
if (process.env.TRAVIS_PYTHON_PATH && fs.existsSync(process.env.TRAVIS_PYTHON_PATH)) {
61-
// tslint:disable-next-line:no-unsafe-any
62-
return process.env.TRAVIS_PYTHON_PATH;
63-
}
64-
return 'python';
65-
}

src/testMultiRootWkspc/workspace5/djangoApp/home/__init__.py

Whitespace-only changes.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<!doctype html>
2+
<html>
3+
<body>
4+
5+
<h1>Hello {{ value_from_server }}!</h1>
6+
<h1>Hello {{ another_value_from_server }}!</h1>
7+
8+
</body>
9+
</html>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from django.conf.urls import url
2+
3+
from . import views
4+
5+
urlpatterns = [
6+
url('', views.index, name='index'),
7+
]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from django.shortcuts import render
2+
from django.template import loader
3+
4+
5+
def index(request):
6+
context = {
7+
'value_from_server':'this_is_a_value_from_server',
8+
'another_value_from_server':'this_is_another_value_from_server'
9+
}
10+
return render(request, 'index.html', context)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/usr/bin/env python
2+
import os
3+
import sys
4+
5+
if __name__ == "__main__":
6+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
7+
try:
8+
from django.core.management import execute_from_command_line
9+
except ImportError:
10+
# The above import may fail for some other reason. Ensure that the
11+
# issue is really that Django is missing to avoid masking other
12+
# exceptions on Python 2.
13+
try:
14+
import django
15+
except ImportError:
16+
raise ImportError(
17+
"Couldn't import Django. Are you sure it's installed and "
18+
"available on your PYTHONPATH environment variable? Did you "
19+
"forget to activate a virtual environment?"
20+
)
21+
raise
22+
execute_from_command_line(sys.argv)

src/testMultiRootWkspc/workspace5/djangoApp/mysite/__init__.py

Whitespace-only changes.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""
2+
Django settings for mysite project.
3+
4+
Generated by 'django-admin startproject' using Django 1.11.2.
5+
6+
For more information on this file, see
7+
https://docs.djangoproject.com/en/1.11/topics/settings/
8+
9+
For the full list of settings and their values, see
10+
https://docs.djangoproject.com/en/1.11/ref/settings/
11+
"""
12+
13+
import os
14+
15+
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
16+
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17+
18+
19+
# Quick-start development settings - unsuitable for production
20+
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
21+
22+
# SECURITY WARNING: keep the secret key used in production secret!
23+
SECRET_KEY = '5u06*)07dvd+=kn)zqp8#b0^qt@*$8=nnjc&&0lzfc28(wns&l'
24+
25+
# SECURITY WARNING: don't run with debug turned on in production!
26+
DEBUG = True
27+
28+
ALLOWED_HOSTS = ['localhost', '127.0.0.1']
29+
30+
31+
# Application definition
32+
33+
INSTALLED_APPS = [
34+
'django.contrib.contenttypes',
35+
'django.contrib.messages',
36+
'django.contrib.staticfiles',
37+
]
38+
39+
MIDDLEWARE = [
40+
]
41+
42+
ROOT_URLCONF = 'mysite.urls'
43+
44+
TEMPLATES = [
45+
{
46+
'BACKEND': 'django.template.backends.django.DjangoTemplates',
47+
'DIRS': ['home/templates'],
48+
'APP_DIRS': True,
49+
'OPTIONS': {
50+
'context_processors': [
51+
'django.template.context_processors.debug',
52+
'django.template.context_processors.request',
53+
'django.contrib.messages.context_processors.messages',
54+
],
55+
},
56+
},
57+
]
58+
59+
WSGI_APPLICATION = 'mysite.wsgi.application'
60+
61+
62+
# Database
63+
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
64+
65+
DATABASES = {
66+
}
67+
68+
69+
# Password validation
70+
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
71+
72+
AUTH_PASSWORD_VALIDATORS = [
73+
]
74+
75+
76+
# Internationalization
77+
# https://docs.djangoproject.com/en/1.11/topics/i18n/
78+
79+
LANGUAGE_CODE = 'en-us'
80+
81+
TIME_ZONE = 'UTC'
82+
83+
USE_I18N = True
84+
85+
USE_L10N = True
86+
87+
USE_TZ = True
88+
89+
90+
# Static files (CSS, JavaScript, Images)
91+
# https://docs.djangoproject.com/en/1.11/howto/static-files/
92+
93+
STATIC_URL = '/static/'
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""mysite URL Configuration
2+
3+
The `urlpatterns` list routes URLs to views. For more information please see:
4+
https://docs.djangoproject.com/en/1.11/topics/http/urls/
5+
Examples:
6+
Function views
7+
1. Add an import: from my_app import views
8+
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
9+
Class-based views
10+
1. Add an import: from other_app.views import Home
11+
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
12+
Including another URLconf
13+
1. Import the include() function: from django.conf.urls import url, include
14+
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
15+
"""
16+
from django.conf.urls import url, include
17+
from django.contrib import admin
18+
from django.views.generic import RedirectView
19+
20+
urlpatterns = [
21+
url(r'^home/', include('home.urls')),
22+
url('', RedirectView.as_view(url='/home/')),
23+
]

0 commit comments

Comments
 (0)