Skip to content

Commit

Permalink
Django and Flask template debugging tests (#1317)
Browse files Browse the repository at this point in the history
Fixes #1172
Fixes #1173
  • Loading branch information
DonJayamanne authored Apr 6, 2018
1 parent 94cc8b1 commit 387bb2b
Show file tree
Hide file tree
Showing 17 changed files with 369 additions and 14 deletions.
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ before_install: |
yarn global add azure-cli
export TRAVIS_PYTHON_PATH=`which python`
install:
- pip install --upgrade -r requirements.txt
- pip install -t ./pythonFiles/experimental/ptvsd git+https://github.com/Microsoft/ptvsd/
- python -m pip install --upgrade -r requirements.txt
- python -m pip install -t ./pythonFiles/experimental/ptvsd git+https://github.com/Microsoft/ptvsd/
- yarn

script:
Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ pytest
fabric
numba
rope
flask
django
11 changes: 11 additions & 0 deletions src/test/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { IS_MULTI_ROOT_TEST } from './initialize';
const fileInNonRootWorkspace = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'dummy.py');
export const rootWorkspaceUri = getWorkspaceRoot();

export const PYTHON_PATH = getPythonPath();

export type PythonSettingKeys = 'workspaceSymbols.enabled' | 'pythonPath' |
'linting.lintOnSave' |
'linting.enabled' | 'linting.pylintEnabled' |
Expand Down Expand Up @@ -118,3 +120,12 @@ const globalPythonPathSetting = workspace.getConfiguration('python').inspect('py
export const clearPythonPathInWorkspaceFolder = async (resource: string | Uri) => retryAsync(setPythonPathInWorkspace)(resource, ConfigurationTarget.WorkspaceFolder);
export const setPythonPathInWorkspaceRoot = async (pythonPath: string) => retryAsync(setPythonPathInWorkspace)(undefined, ConfigurationTarget.Workspace, pythonPath);
export const resetGlobalPythonPathSetting = async () => retryAsync(restoreGlobalPythonPathSetting)();

function getPythonPath(): string {
// tslint:disable-next-line:no-unsafe-any
if (process.env.TRAVIS_PYTHON_PATH && fs.existsSync(process.env.TRAVIS_PYTHON_PATH)) {
// tslint:disable-next-line:no-unsafe-any
return process.env.TRAVIS_PYTHON_PATH;
}
return 'python';
}
3 changes: 3 additions & 0 deletions src/test/debugger/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ export async function validateVariablesInFrame(debugClient: DebugClient,
export function makeHttpRequest(uri: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
request.get(uri, (error: any, response: request.Response, body: any) => {
if (error) {
return reject(error);
}
if (response.statusCode !== 200) {
reject(new Error(`Status code = ${response.statusCode}`));
} else {
Expand Down
148 changes: 148 additions & 0 deletions src/test/debugger/web.framework.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

// 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

import { expect } from 'chai';
import * as getFreePort from 'get-port';
import * as path from 'path';
import { DebugClient } from 'vscode-debugadapter-testsupport';
import { EXTENSION_ROOT_DIR } from '../../client/common/constants';
import { noop } from '../../client/common/core.utils';
import { DebugOptions, LaunchRequestArguments } from '../../client/debugger/Common/Contracts';
import { PYTHON_PATH, sleep } from '../common';
import { IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize';
import { DEBUGGER_TIMEOUT } from './common/constants';
import { continueDebugging, createDebugAdapter, ExpectedVariable, hitHttpBreakpoint, makeHttpRequest, validateVariablesInFrame } from './utils';

let testCounter = 0;
const debuggerType = 'pythonExperimental';
suite(`Django and Flask Debugging: ${debuggerType}`, () => {
let debugClient: DebugClient;
setup(async function () {
if (!IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) {
this.skip();
}
this.timeout(5 * DEBUGGER_TIMEOUT);
const coverageDirectory = path.join(EXTENSION_ROOT_DIR, `debug_coverage_django_flask${testCounter += 1}`);
debugClient = await createDebugAdapter(coverageDirectory);
});
teardown(async () => {
// Wait for a second before starting another test (sometimes, sockets take a while to get closed).
await sleep(1000);
try {
await debugClient.stop().catch(noop);
// tslint:disable-next-line:no-empty
} catch (ex) { }
await sleep(1000);
});
function buildLaunchArgs(workspaceDirectory: string): LaunchRequestArguments {
const env = {};
// tslint:disable-next-line:no-string-literal
env['PYTHONPATH'] = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'experimental', 'ptvsd');

// tslint:disable-next-line:no-unnecessary-local-variable
const options: LaunchRequestArguments = {
cwd: workspaceDirectory,
program: '',
debugOptions: [DebugOptions.RedirectOutput],
pythonPath: PYTHON_PATH,
args: [],
env,
envFile: '',
logToFile: true,
type: debuggerType
};

return options;
}
async function buildFlaskLaunchArgs(workspaceDirectory: string) {
const port = await getFreePort({ host: 'localhost' });
const options = buildLaunchArgs(workspaceDirectory);

options.env!['FLASK_APP'] = path.join(workspaceDirectory, 'run.py');
options.module = 'flask';
options.debugOptions = [DebugOptions.RedirectOutput, DebugOptions.Jinja];
options.args = [
'run',
'--no-debugger',
'--no-reload',
'--port',
`${port}`
];

return { options, port };
}
async function buildDjangoLaunchArgs(workspaceDirectory: string) {
const port = await getFreePort({ host: 'localhost' });
const options = buildLaunchArgs(workspaceDirectory);

options.program = path.join(workspaceDirectory, 'manage.py');
options.debugOptions = [DebugOptions.RedirectOutput, DebugOptions.Django];
options.args = [
'runserver',
'--noreload',
'--nothreading',
`${port}`
];

return { options, port };
}

async function testTemplateDebugging(launchArgs: LaunchRequestArguments, port: number, viewFile: string, viewLine: number, templateFile: string, templateLine: number) {
await Promise.all([
debugClient.configurationSequence(),
debugClient.launch(launchArgs),
debugClient.waitForEvent('initialized'),
debugClient.waitForEvent('process'),
debugClient.waitForEvent('thread')
]);

const httpResult = await makeHttpRequest(`http://localhost:${port}`);

expect(httpResult).to.contain('Hello this_is_a_value_from_server');
expect(httpResult).to.contain('Hello this_is_another_value_from_server');

await hitHttpBreakpoint(debugClient, `http://localhost:${port}`, viewFile, viewLine);

await continueDebugging(debugClient);
await debugClient.setBreakpointsRequest({ breakpoints: [], lines: [], source: { path: viewFile } });

// Template debugging.
const [stackTrace, htmlResultPromise] = await hitHttpBreakpoint(debugClient, `http://localhost:${port}`, templateFile, templateLine);

// Wait for breakpoint to hit
const expectedVariables: ExpectedVariable[] = [
{ name: 'value_from_server', type: 'str', value: '\'this_is_a_value_from_server\'' },
{ name: 'another_value_from_server', type: 'str', value: '\'this_is_another_value_from_server\'' }
];
await validateVariablesInFrame(debugClient, stackTrace, expectedVariables, 1);

await debugClient.setBreakpointsRequest({ breakpoints: [], lines: [], source: { path: templateFile } });
await continueDebugging(debugClient);

const htmlResult = await htmlResultPromise;
expect(htmlResult).to.contain('Hello this_is_a_value_from_server');
expect(htmlResult).to.contain('Hello this_is_another_value_from_server');
}

test('Test Flask Route and Template debugging', async () => {
const workspaceDirectory = path.join(EXTENSION_ROOT_DIR, 'src', 'testMultiRootWkspc', 'workspace5', 'flaskApp');
const { options, port } = await buildFlaskLaunchArgs(workspaceDirectory);

await testTemplateDebugging(options, port,
path.join(workspaceDirectory, 'run.py'), 7,
path.join(workspaceDirectory, 'templates', 'index.html'), 6);
});

test('Test Django Route and Template debugging', async () => {
const workspaceDirectory = path.join(EXTENSION_ROOT_DIR, 'src', 'testMultiRootWkspc', 'workspace5', 'djangoApp');
const { options, port } = await buildDjangoLaunchArgs(workspaceDirectory);

await testTemplateDebugging(options, port,
path.join(workspaceDirectory, 'home', 'views.py'), 10,
path.join(workspaceDirectory, 'home', 'templates', 'index.html'), 6);
});
});
13 changes: 1 addition & 12 deletions src/test/initialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as path from 'path';
import * as vscode from 'vscode';
import { PythonSettings } from '../client/common/configSettings';
import { activated } from '../client/extension';
import { clearPythonPathInWorkspaceFolder, resetGlobalPythonPathSetting, setPythonPathInWorkspaceRoot } from './common';
import { clearPythonPathInWorkspaceFolder, PYTHON_PATH, resetGlobalPythonPathSetting, setPythonPathInWorkspaceRoot } from './common';

export * from './constants';

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

const PYTHON_PATH = getPythonPath();

// Ability to use custom python environments for testing
export async function initializePython() {
await resetGlobalPythonPathSetting();
Expand Down Expand Up @@ -54,12 +52,3 @@ export async function closeActiveWindows(): Promise<void> {
}, 15000);
});
}

function getPythonPath(): string {
// tslint:disable-next-line:no-unsafe-any
if (process.env.TRAVIS_PYTHON_PATH && fs.existsSync(process.env.TRAVIS_PYTHON_PATH)) {
// tslint:disable-next-line:no-unsafe-any
return process.env.TRAVIS_PYTHON_PATH;
}
return 'python';
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!doctype html>
<html>
<body>

<h1>Hello {{ value_from_server }}!</h1>
<h1>Hello {{ another_value_from_server }}!</h1>

</body>
</html>
7 changes: 7 additions & 0 deletions src/testMultiRootWkspc/workspace5/djangoApp/home/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.conf.urls import url

from . import views

urlpatterns = [
url('', views.index, name='index'),
]
10 changes: 10 additions & 0 deletions src/testMultiRootWkspc/workspace5/djangoApp/home/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from django.shortcuts import render
from django.template import loader


def index(request):
context = {
'value_from_server':'this_is_a_value_from_server',
'another_value_from_server':'this_is_another_value_from_server'
}
return render(request, 'index.html', context)
22 changes: 22 additions & 0 deletions src/testMultiRootWkspc/workspace5/djangoApp/manage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env python
import os
import sys

if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
try:
from django.core.management import execute_from_command_line
except ImportError:
# The above import may fail for some other reason. Ensure that the
# issue is really that Django is missing to avoid masking other
# exceptions on Python 2.
try:
import django
except ImportError:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
)
raise
execute_from_command_line(sys.argv)
Empty file.
93 changes: 93 additions & 0 deletions src/testMultiRootWkspc/workspace5/djangoApp/mysite/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""
Django settings for mysite project.
Generated by 'django-admin startproject' using Django 1.11.2.
For more information on this file, see
https://docs.djangoproject.com/en/1.11/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.11/ref/settings/
"""

import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '5u06*)07dvd+=kn)zqp8#b0^qt@*$8=nnjc&&0lzfc28(wns&l'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ['localhost', '127.0.0.1']


# Application definition

INSTALLED_APPS = [
'django.contrib.contenttypes',
'django.contrib.messages',
'django.contrib.staticfiles',
]

MIDDLEWARE = [
]

ROOT_URLCONF = 'mysite.urls'

TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['home/templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.messages.context_processors.messages',
],
},
},
]

WSGI_APPLICATION = 'mysite.wsgi.application'


# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases

DATABASES = {
}


# Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
]


# Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/

STATIC_URL = '/static/'
23 changes: 23 additions & 0 deletions src/testMultiRootWkspc/workspace5/djangoApp/mysite/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""mysite URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/1.11/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf.urls import url, include
from django.contrib import admin
from django.views.generic import RedirectView

urlpatterns = [
url(r'^home/', include('home.urls')),
url('', RedirectView.as_view(url='/home/')),
]
Loading

0 comments on commit 387bb2b

Please sign in to comment.