diff --git a/.travis.yml b/.travis.yml index fee4079ea762..915c33dd9cad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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: diff --git a/requirements.txt b/requirements.txt index e7f764089ecf..439a8d6999b7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,5 @@ pytest fabric numba rope +flask +django diff --git a/src/test/common.ts b/src/test/common.ts index e399012adf2c..5f6d6640e709 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -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' | @@ -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'; +} diff --git a/src/test/debugger/utils.ts b/src/test/debugger/utils.ts index ad59a2fd98e5..adc4a532221a 100644 --- a/src/test/debugger/utils.ts +++ b/src/test/debugger/utils.ts @@ -69,6 +69,9 @@ export async function validateVariablesInFrame(debugClient: DebugClient, export function makeHttpRequest(uri: string): Promise { return new Promise((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 { diff --git a/src/test/debugger/web.framework.test.ts b/src/test/debugger/web.framework.test.ts new file mode 100644 index 000000000000..ca3ca6c6a19f --- /dev/null +++ b/src/test/debugger/web.framework.test.ts @@ -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); + }); +}); diff --git a/src/test/initialize.ts b/src/test/initialize.ts index 5914d38185e9..f3cba26217bc 100644 --- a/src/test/initialize.ts +++ b/src/test/initialize.ts @@ -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'; @@ -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(); @@ -48,12 +46,3 @@ export async function closeActiveWindows(): Promise { // tslint:disable-next-line:no-unnecessary-callback-wrapper .then(() => resolve(), reject)); } - -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'; -} diff --git a/src/testMultiRootWkspc/workspace5/djangoApp/home/__init__.py b/src/testMultiRootWkspc/workspace5/djangoApp/home/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/testMultiRootWkspc/workspace5/djangoApp/home/templates/index.html b/src/testMultiRootWkspc/workspace5/djangoApp/home/templates/index.html new file mode 100644 index 000000000000..6ca5107d23d6 --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/djangoApp/home/templates/index.html @@ -0,0 +1,9 @@ + + + + +

Hello {{ value_from_server }}!

+

Hello {{ another_value_from_server }}!

+ + + diff --git a/src/testMultiRootWkspc/workspace5/djangoApp/home/urls.py b/src/testMultiRootWkspc/workspace5/djangoApp/home/urls.py new file mode 100644 index 000000000000..70a9606e88e6 --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/djangoApp/home/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import url + +from . import views + +urlpatterns = [ + url('', views.index, name='index'), +] diff --git a/src/testMultiRootWkspc/workspace5/djangoApp/home/views.py b/src/testMultiRootWkspc/workspace5/djangoApp/home/views.py new file mode 100644 index 000000000000..0494f868dc6f --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/djangoApp/home/views.py @@ -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) diff --git a/src/testMultiRootWkspc/workspace5/djangoApp/manage.py b/src/testMultiRootWkspc/workspace5/djangoApp/manage.py new file mode 100644 index 000000000000..afbc784aafd8 --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/djangoApp/manage.py @@ -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) diff --git a/src/testMultiRootWkspc/workspace5/djangoApp/mysite/__init__.py b/src/testMultiRootWkspc/workspace5/djangoApp/mysite/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/testMultiRootWkspc/workspace5/djangoApp/mysite/settings.py b/src/testMultiRootWkspc/workspace5/djangoApp/mysite/settings.py new file mode 100644 index 000000000000..4e182517ca2a --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/djangoApp/mysite/settings.py @@ -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/' diff --git a/src/testMultiRootWkspc/workspace5/djangoApp/mysite/urls.py b/src/testMultiRootWkspc/workspace5/djangoApp/mysite/urls.py new file mode 100644 index 000000000000..9db383365e3e --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/djangoApp/mysite/urls.py @@ -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/')), +] diff --git a/src/testMultiRootWkspc/workspace5/djangoApp/mysite/wsgi.py b/src/testMultiRootWkspc/workspace5/djangoApp/mysite/wsgi.py new file mode 100644 index 000000000000..74e7daeefe76 --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/djangoApp/mysite/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for mysite project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") + +application = get_wsgi_application() diff --git a/src/testMultiRootWkspc/workspace5/flaskApp/run.py b/src/testMultiRootWkspc/workspace5/flaskApp/run.py new file mode 100644 index 000000000000..9c3172c3e918 --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/flaskApp/run.py @@ -0,0 +1,13 @@ +from flask import Flask, render_template +app = Flask(__name__) + + +@app.route('/') +def hello(): + return render_template('index.html', + value_from_server='this_is_a_value_from_server', + another_value_from_server='this_is_another_value_from_server') + + +if __name__ == '__main__': + app.run() diff --git a/src/testMultiRootWkspc/workspace5/flaskApp/templates/index.html b/src/testMultiRootWkspc/workspace5/flaskApp/templates/index.html new file mode 100644 index 000000000000..6ca5107d23d6 --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/flaskApp/templates/index.html @@ -0,0 +1,9 @@ + + + + +

Hello {{ value_from_server }}!

+

Hello {{ another_value_from_server }}!

+ + +