Add support for C# Class library projects (#161)

This supports project/function creation and local debugging. It does not include deploy logic.

A brief summary:
1. We default C# projects to beta runtime and class library (instead of C# script)
1. We use dotnet templates for project/function creation. We will automatically install the templates for the user if they are not on their machine (I don't prompt at all - let me know if you think we should prompt).
1. We use the parameter information from the functions portal (Aka C# Script templates) since it's easier to parse than the dotnet cli and it gives us more information (like validation). This requires us to assume that parameters for C# Scripts are the same as the parameters for the C# Class libraries. Since that might not always be the case, I mitigated this with unit tests and hard-coding the version of the dotnet templates.
1. Unlike JavaScript debugging, we have to attach to a specific process instead of attaching with a port. I implemented a 'pickProcess' command to search for the functions host process.
1. This only works on Windows. There's a few issues on a Mac I still need to iron out.
This commit is contained in:
Eric Jizba 2018-01-11 17:37:52 -08:00 коммит произвёл GitHub
Родитель a6f0af8e04
Коммит 7fe28b5a79
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
26 изменённых файлов: 815 добавлений и 266 удалений

Просмотреть файл

@ -3,6 +3,16 @@ language: node_js
node_js:
- 'stable'
addons:
apt:
packages:
- gettext
- libcurl4-openssl-dev
- libicu-dev
- libssl-dev
- libunwind8
- zlib1g
before_install:
- if [ $TRAVIS_OS_NAME == "linux" ]; then
export CXX="g++-4.9" CC="gcc-4.9" DISPLAY=:99.0;
@ -10,6 +20,11 @@ before_install:
sleep 3;
fi
# Install dotnet cli and add to path
- export DOTNET_INSTALL_DIR="$HOME/.dotnet"
- curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --version 2.0.0 --install-dir "$DOTNET_INSTALL_DIR"
- export PATH="$DOTNET_INSTALL_DIR:$PATH"
install:
- npm install

Просмотреть файл

@ -4,18 +4,20 @@
* For local debugging:
* [.NET Core 2.0](https://www.microsoft.com/net/download/core)
* Install the [Azure Core Function Tools 2.0](https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local) with the following command
```bash
npm install -g azure-functions-core-tools@core
```
> Note: when installing on Ubuntu, you may need to use `sudo`. On MacOS and LInux, you may need to include the `unsafe-perm` flag, as follows:
```bash
sudo npm install -g azure-functions-core-tools@core --unsafe-perm true
```
```bash
npm install -g azure-functions-core-tools@core
```
> Note: when installing on Ubuntu, you may need to use `sudo`. On MacOS and LInux, you may need to include the `unsafe-perm` flag, as follows:
```bash
sudo npm install -g azure-functions-core-tools@core --unsafe-perm true
```
* For JavaScript based Functions:
* [Node 8.0+](https://nodejs.org/)
* For C# based Functions:
* [VS Code Debugger for C#](https://marketplace.visualstudio.com/items?itemName=ms-vscode.csharp)
* For Java based Functions:
* [VS Code Debugger for Java](https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-debug)
* [JDK 1.8+](http://www.oracle.com/technetwork/java/javase/downloads/index.html)

Просмотреть файл

@ -47,6 +47,7 @@
"onCommand:azureFunctions.appSettings.edit",
"onCommand:azureFunctions.appSettings.rename",
"onCommand:azureFunctions.appSettings.delete",
"onCommand:azureFunctions.pickProcess",
"onView:azureFunctionsExplorer"
],
"main": "./out/src/extension",
@ -157,6 +158,11 @@
"command": "azureFunctions.appSettings.delete",
"title": "%azFunc.appSettings.delete%",
"category": "Azure Functions"
},
{
"command": "azureFunctions.pickProcess",
"title": "%azFunc.pickProcess%",
"category": "Azure Functions"
}
],
"views": {
@ -293,6 +299,10 @@
{
"command": "azureFunctions.loadMore",
"when": "never"
},
{
"command": "azureFunctions.pickProcess",
"when": "never"
}
]
},
@ -352,6 +362,7 @@
"enum": [
"Bash",
"Batch",
"C#",
"F#",
"Java",
"JavaScript",
@ -398,6 +409,7 @@
"fs-extra": "^4.0.2",
"ms-rest": "^2.2.2",
"ms-rest-azure": "^2.3.1",
"ps-node": "^0.1.6",
"request-promise": "^4.2.2",
"vscode-azureappservice": "~0.8.2",
"vscode-azureextensionui": "~0.5.1",

Просмотреть файл

@ -21,5 +21,6 @@
"azFunc.appSettings.add": "Add new setting...",
"azFunc.appSettings.edit": "Edit setting...",
"azFunc.appSettings.rename": "Rename setting...",
"azFunc.appSettings.delete": "Delete setting"
"azFunc.appSettings.delete": "Delete setting",
"azFunc.pickProcess": "Pick Process"
}

Просмотреть файл

@ -22,6 +22,7 @@ const previewDescription: string = localize('previewDescription', '(Preview)');
export enum ProjectLanguage {
Bash = 'Bash',
Batch = 'Batch',
CSharp = 'C#',
FSharp = 'F#',
Java = 'Java',
JavaScript = 'JavaScript',

Просмотреть файл

@ -0,0 +1,78 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fse from 'fs-extra';
import * as path from 'path';
import { OutputChannel } from "vscode";
import { IUserInterface } from "../../IUserInterface";
import { localize } from "../../localize";
import { Template } from "../../templates/Template";
import { removeLanguageFromId } from "../../templates/TemplateData";
import { cpUtils } from "../../utils/cpUtils";
import { dotnetUtils } from '../../utils/dotnetUtils';
import * as fsUtil from '../../utils/fs';
import { FunctionCreatorBase } from './FunctionCreatorBase';
export class CSharpFunctionCreator extends FunctionCreatorBase {
private _outputChannel: OutputChannel;
private _functionName: string;
constructor(functionAppPath: string, template: Template, outputChannel: OutputChannel) {
super(functionAppPath, template);
this._outputChannel = outputChannel;
}
public async promptForSettings(ui: IUserInterface, functionName: string | undefined): Promise<void> {
if (!functionName) {
const defaultFunctionName: string | undefined = await fsUtil.getUniqueFsPath(this._functionAppPath, removeLanguageFromId(this._template.id), '.cs');
const placeHolder: string = localize('azFunc.funcNamePlaceholder', 'Function name');
const prompt: string = localize('azFunc.funcNamePrompt', 'Provide a function name');
this._functionName = await ui.showInputBox(placeHolder, prompt, false, (s: string) => this.validateTemplateName(s), defaultFunctionName || this._template.defaultFunctionName);
} else {
this._functionName = functionName;
}
}
public async createFunction(userSettings: { [propertyName: string]: string }): Promise<string | undefined> {
await dotnetUtils.validateTemplatesInstalled(this._outputChannel, this._functionAppPath);
const args: string[] = [];
for (const key of Object.keys(userSettings)) {
let parameter: string = key.charAt(0).toUpperCase() + key.slice(1);
// the parameters for dotnet templates are not consistent. Hence, we have to special-case a few of them:
if (parameter === 'AuthLevel') {
parameter = 'AccessRights';
} else if (parameter === 'QueueName') {
parameter = 'Path';
}
args.push(`--${parameter}="${userSettings[key]}"`);
}
await cpUtils.executeCommand(
this._outputChannel,
this._functionAppPath,
'dotnet',
'new',
removeLanguageFromId(this._template.id),
`--name="${this._functionName}"`,
...args
);
return path.join(this._functionAppPath, `${this._functionName}.cs`);
}
private validateTemplateName(name: string | undefined): string | undefined {
if (!name) {
return localize('azFunc.emptyTemplateNameError', 'The template name cannot be empty.');
} else if (fse.existsSync(path.join(this._functionAppPath, `${name}.cs`))) {
return localize('azFunc.existingCSFile', 'A CSharp file with the name \'{0}\' already exists.', name);
} else if (!this._functionNameRegex.test(name)) {
return localize('azFunc.functionNameInvalidError', 'Function name must start with a letter and can contain letters, digits, \'_\' and \'-\'');
} else {
return undefined;
}
}
}

Просмотреть файл

@ -9,7 +9,7 @@ import { OutputChannel } from "vscode";
import { IUserInterface } from "../../IUserInterface";
import { localize } from "../../localize";
import { Template } from "../../templates/Template";
import { convertTemplateIdToJava } from "../../templates/TemplateData";
import { removeLanguageFromId } from "../../templates/TemplateData";
import { cpUtils } from "../../utils/cpUtils";
import * as fsUtil from '../../utils/fs';
import { getFullClassName, parseJavaClassName, validatePackageName } from "../../utils/javaNameUtils";
@ -36,7 +36,7 @@ export class JavaFunctionCreator extends FunctionCreatorBase {
this._packageName = await ui.showInputBox(packagePlaceHolder, packagePrompt, false, validatePackageName, 'com.function');
if (!functionName) {
const defaultFunctionName: string | undefined = await fsUtil.getUniqueJavaFsPath(this._functionAppPath, this._packageName, `${convertTemplateIdToJava(this._template.id)}Java`);
const defaultFunctionName: string | undefined = await fsUtil.getUniqueJavaFsPath(this._functionAppPath, this._packageName, `${removeLanguageFromId(this._template.id)}Java`);
const placeHolder: string = localize('azFunc.funcNamePlaceholder', 'Function name');
const prompt: string = localize('azFunc.funcNamePrompt', 'Provide a function name');
this._functionName = await ui.showInputBox(placeHolder, prompt, false, (s: string) => this.validateTemplateName(s), defaultFunctionName || this._template.defaultFunctionName);
@ -61,7 +61,7 @@ export class JavaFunctionCreator extends FunctionCreatorBase {
'-B',
`"-Dfunctions.package=${this._packageName}"`,
`"-Dfunctions.name=${this._functionName}"`,
`"-Dfunctions.template=${convertTemplateIdToJava(this._template.id)}"`,
`"-Dfunctions.template=${removeLanguageFromId(this._template.id)}"`,
...javaFuntionProperties
);

Просмотреть файл

@ -20,6 +20,7 @@ import { TemplateData } from '../../templates/TemplateData';
import * as workspaceUtil from '../../utils/workspace';
import { VSCodeUI } from '../../VSCodeUI';
import { createNewProject } from '../createNewProject/createNewProject';
import { CSharpFunctionCreator } from './CSharpFunctionCreator';
import { FunctionCreatorBase } from './FunctionCreatorBase';
import { JavaFunctionCreator } from './JavaFunctionCreator';
import { ScriptFunctionCreator } from './ScriptFunctionCreator';
@ -86,7 +87,7 @@ export async function createFunction(
functionAppPath?: string,
templateId?: string,
functionName?: string,
...functionSettings: (string)[]): Promise<void> {
...functionSettings: (string) []): Promise<void> {
if (functionAppPath === undefined) {
const folderPlaceholder: string = localize('azFunc.selectFunctionAppFolderExisting', 'Select the folder containing your function app');
@ -131,6 +132,9 @@ export async function createFunction(
case ProjectLanguage.Java:
functionCreator = new JavaFunctionCreator(functionAppPath, template, outputChannel);
break;
case ProjectLanguage.CSharp:
functionCreator = new CSharpFunctionCreator(functionAppPath, template, outputChannel);
break;
default:
functionCreator = new ScriptFunctionCreator(functionAppPath, template, language);
break;

Просмотреть файл

@ -0,0 +1,91 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { OutputChannel } from 'vscode';
import { localize } from "../../localize";
import { ProjectRuntime } from '../../ProjectSettings';
import { cpUtils } from '../../utils/cpUtils';
import { dotnetUtils } from '../../utils/dotnetUtils';
import { funcHostProblemMatcher, funcHostTaskId, IProjectCreator } from './IProjectCreator';
export class CSharpProjectCreator implements IProjectCreator {
private _outputChannel: OutputChannel;
constructor(outputChannel: OutputChannel) {
this._outputChannel = outputChannel;
}
public async addNonVSCodeFiles(functionAppPath: string): Promise<void> {
await dotnetUtils.validateTemplatesInstalled(this._outputChannel, functionAppPath);
await cpUtils.executeCommand(
this._outputChannel,
functionAppPath,
'dotnet',
'new',
dotnetUtils.funcProjectId
);
}
public getRuntime(): ProjectRuntime {
return ProjectRuntime.beta;
}
public getTasksJson(): {} {
return {
version: '2.0.0',
tasks: [
{
label: 'build',
command: 'dotnet build',
type: 'shell',
group: {
kind: 'build',
isDefault: true
},
presentation: {
reveal: 'always'
},
problemMatcher: '$msCompile'
},
{
label: localize('azFunc.runFuncHost', 'Run Functions Host'),
identifier: funcHostTaskId,
type: 'shell',
dependsOn: 'build',
options: {
cwd: '\${workspaceFolder}/bin/Debug/netstandard2.0'
},
command: 'func host start',
isBackground: true,
presentation: {
reveal: 'always'
},
problemMatcher: [
funcHostProblemMatcher
]
}
]
};
}
public getLaunchJson(): {} {
return {
version: '0.2.0',
configurations: [
{
name: localize('azFunc.attachToNetCoreFunc', "Attach to .NET Core Functions"),
type: 'coreclr',
request: 'attach',
processId: '\${command:azureFunctions.pickProcess}',
preLaunchTask: 'build'
}
]
};
}
public getRecommendedExtensions(): string[] {
return ['ms-vscode.csharp'];
}
}

Просмотреть файл

@ -3,14 +3,35 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ProjectRuntime } from "../../ProjectSettings";
import { localize } from "../../localize";
import { extensionPrefix, ProjectRuntime } from "../../ProjectSettings";
export interface IProjectCreator {
/**
* Add all project files not included in the '.vscode' folder
*/
addNonVSCodeFiles(functionAppPath: string): Promise<void>;
getTasksJson(launchTaskId: string, funcProblemMatcher: {}): {};
getLaunchJson(launchTaskId: string): {};
getTasksJson(): {};
getLaunchJson(): {};
getRecommendedExtensions?(): string[];
getRuntime(): ProjectRuntime;
}
export const funcHostTaskId: string = 'runFunctionsHost';
export const funcHostTaskLabel: string = localize('azFunc.runFuncHost', 'Run Functions Host');
export const funcHostProblemMatcher: {} = {
owner: extensionPrefix,
pattern: [
{
regexp: '\\b\\B',
file: 1,
location: 2,
message: 3
}
],
background: {
activeOnStart: true,
beginsPattern: '^.*Stopping host.*',
endsPattern: '^.*Job host started.*'
}
};

Просмотреть файл

@ -14,7 +14,7 @@ import { cpUtils } from '../../utils/cpUtils';
import * as fsUtil from '../../utils/fs';
import { validateMavenIdentifier, validatePackageName } from '../../utils/javaNameUtils';
import { mavenUtils } from '../../utils/mavenUtils';
import { IProjectCreator } from './IProjectCreator';
import { funcHostProblemMatcher, funcHostTaskId, funcHostTaskLabel, IProjectCreator } from './IProjectCreator';
export class JavaProjectCreator implements IProjectCreator {
private _javaTargetPath: string;
@ -78,13 +78,13 @@ export class JavaProjectCreator implements IProjectCreator {
return ProjectRuntime.beta;
}
public getTasksJson(launchTaskId: string, funcProblemMatcher: {}): {} {
public getTasksJson(): {} {
let tasksJson: {} = {
version: '2.0.0',
tasks: [
{
label: localize('azFunc.launchFuncApp', 'Launch Function App'),
identifier: launchTaskId,
label: funcHostTaskLabel,
identifier: funcHostTaskId,
linux: {
command: 'sh -c "mvn clean package -B && func host start --script-root \\\"%path%\\\""'
},
@ -100,7 +100,7 @@ export class JavaProjectCreator implements IProjectCreator {
reveal: 'always'
},
problemMatcher: [
funcProblemMatcher
funcHostProblemMatcher
]
}
]
@ -114,7 +114,7 @@ export class JavaProjectCreator implements IProjectCreator {
return tasksJson;
}
public getLaunchJson(launchTaskId: string): {} {
public getLaunchJson(): {} {
return {
version: '0.2.0',
configurations: [
@ -124,9 +124,13 @@ export class JavaProjectCreator implements IProjectCreator {
request: 'attach',
hostName: 'localhost',
port: 5005,
preLaunchTask: launchTaskId
preLaunchTask: funcHostTaskId
}
]
};
}
public getRecommendedExtensions(): string[] {
return ['vscjava.vscode-java-debug'];
}
}

Просмотреть файл

@ -5,6 +5,7 @@
import { localize } from "../../localize";
import { ProjectRuntime } from "../../ProjectSettings";
import { funcHostProblemMatcher, funcHostTaskId, funcHostTaskLabel } from "./IProjectCreator";
import { ScriptProjectCreatorBase } from './ScriptProjectCreatorBase';
export class JavaScriptProjectCreator extends ScriptProjectCreatorBase {
@ -12,13 +13,13 @@ export class JavaScriptProjectCreator extends ScriptProjectCreatorBase {
return ProjectRuntime.one;
}
public getTasksJson(launchTaskId: string, funcProblemMatcher: {}): {} {
public getTasksJson(): {} {
return {
version: '2.0.0',
tasks: [
{
label: localize('azFunc.launchFuncApp', 'Launch Function App'),
identifier: launchTaskId,
label: funcHostTaskLabel,
identifier: funcHostTaskId,
type: 'shell',
command: 'func host start',
isBackground: true,
@ -26,14 +27,14 @@ export class JavaScriptProjectCreator extends ScriptProjectCreatorBase {
reveal: 'always'
},
problemMatcher: [
funcProblemMatcher
funcHostProblemMatcher
]
}
]
};
}
public getLaunchJson(launchTaskId: string): {} {
public getLaunchJson(): {} {
return {
version: '0.2.0',
configurations: [
@ -43,7 +44,7 @@ export class JavaScriptProjectCreator extends ScriptProjectCreatorBase {
request: 'attach',
port: 5858,
protocol: 'inspector',
preLaunchTask: launchTaskId
preLaunchTask: funcHostTaskId
}
]
};

Просмотреть файл

@ -41,8 +41,8 @@ local.settings.json
* Base class for all projects based on a simple script (i.e. JavaScript, C# Script, Bash, etc.) that don't require compilation
*/
export abstract class ScriptProjectCreatorBase implements IProjectCreator {
public abstract getTasksJson(launchTaskId: string, funcProblemMatcher: {}): {};
public abstract getLaunchJson(launchTaskId: string): {};
public abstract getTasksJson(): {};
public abstract getLaunchJson(): {};
public abstract getRuntime(): ProjectRuntime;
public async addNonVSCodeFiles(functionAppPath: string): Promise<void> {

Просмотреть файл

@ -16,29 +16,11 @@ import { confirmOverwriteFile } from '../../utils/fs';
import { gitUtils } from '../../utils/gitUtils';
import * as workspaceUtil from '../../utils/workspace';
import { VSCodeUI } from '../../VSCodeUI';
import { CSharpProjectCreator } from './CSharpProjectCreator';
import { IProjectCreator } from './IProjectCreator';
import { JavaProjectCreator } from './JavaProjectCreator';
import { JavaScriptProjectCreator } from './JavaScriptProjectCreator';
const launchTaskId: string = 'launchFunctionApp';
const funcProblemMatcher: {} = {
owner: extensionPrefix,
pattern: [
{
regexp: '\\b\\B',
file: 1,
location: 2,
message: 3
}
],
background: {
activeOnStart: true,
beginsPattern: '^.*Stopping host.*',
endsPattern: '^.*Job host started.*'
}
};
export async function createNewProject(telemetryProperties: TelemetryProperties, outputChannel: OutputChannel, functionAppPath?: string, language?: string, openFolder: boolean = true, ui: IUserInterface = new VSCodeUI()): Promise<void> {
if (functionAppPath === undefined) {
functionAppPath = await workspaceUtil.selectWorkspaceFolder(ui, localize('azFunc.selectFunctionAppFolderNew', 'Select the folder that will contain your function app'));
@ -48,6 +30,7 @@ export async function createNewProject(telemetryProperties: TelemetryProperties,
// Only display 'supported' languages that can be debugged in VS Code
const languagePicks: Pick[] = [
new Pick(ProjectLanguage.JavaScript),
new Pick(ProjectLanguage.CSharp),
new Pick(ProjectLanguage.Java)
];
@ -64,6 +47,9 @@ export async function createNewProject(telemetryProperties: TelemetryProperties,
case ProjectLanguage.JavaScript:
projectCreator = new JavaScriptProjectCreator();
break;
case ProjectLanguage.CSharp:
projectCreator = new CSharpProjectCreator(outputChannel);
break;
default:
throw new Error(localize('unrecognizedLanguage', 'Unrecognized language "{0}"', language));
}
@ -79,12 +65,12 @@ export async function createNewProject(telemetryProperties: TelemetryProperties,
const tasksJsonPath: string = path.join(vscodePath, 'tasks.json');
if (await confirmOverwriteFile(tasksJsonPath)) {
await fsUtil.writeFormattedJson(tasksJsonPath, projectCreator.getTasksJson(launchTaskId, funcProblemMatcher));
await fsUtil.writeFormattedJson(tasksJsonPath, projectCreator.getTasksJson());
}
const launchJsonPath: string = path.join(vscodePath, 'launch.json');
if (await confirmOverwriteFile(launchJsonPath)) {
await fsUtil.writeFormattedJson(launchJsonPath, projectCreator.getLaunchJson(launchTaskId));
await fsUtil.writeFormattedJson(launchJsonPath, projectCreator.getLaunchJson());
}
const settingsJsonPath: string = path.join(vscodePath, 'settings.json');
@ -96,6 +82,15 @@ export async function createNewProject(telemetryProperties: TelemetryProperties,
await fsUtil.writeFormattedJson(settingsJsonPath, settings);
}
if (projectCreator.getRecommendedExtensions) {
const extensionsJsonPath: string = path.join(vscodePath, 'extensions.json');
if (await confirmOverwriteFile(extensionsJsonPath)) {
await fsUtil.writeFormattedJson(extensionsJsonPath, {
recommendations: projectCreator.getRecommendedExtensions()
});
}
}
if (openFolder && !workspaceUtil.isFolderOpenInWorkspace(functionAppPath)) {
// If the selected folder is not open in a workspace, open it now. NOTE: This may restart the extension host
await vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.file(functionAppPath), false);

Просмотреть файл

@ -0,0 +1,47 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// tslint:disable-next-line:no-require-imports
import ps = require('ps-node');
import * as vscode from 'vscode';
import { MessageItem } from 'vscode';
import { DialogResponses } from '../DialogResponses';
import { localize } from '../localize';
import { funcHostTaskId } from './createNewProject/IProjectCreator';
export async function pickFuncProcess(): Promise<string | undefined> {
const processList: IProcess[] = await new Promise((resolve: (processList: IProcess[]) => void, reject: (e: Error) => void): void => {
//tslint:disable-next-line:no-unsafe-any
ps.lookup(
{
command: '.*dotnet.*',
arguments: '.*Azure\.Functions.*host.*start'
},
(error: Error | undefined, result: IProcess[]): void => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
if (processList.length === 0) {
const startHost: vscode.MessageItem = { title: localize('startHost', 'Start Functions Host') };
const result: MessageItem | undefined = await vscode.window.showWarningMessage(localize('funcHostNotRunning', 'You must have the Functions host running in the background before you debug.'), startHost, DialogResponses.cancel);
if (result === startHost) {
await vscode.commands.executeCommand('workbench.action.tasks.runTask', funcHostTaskId);
}
return undefined;
} else if (processList.length === 1) {
return processList[0].pid;
} else {
throw new Error(localize('multipleFuncHost', 'Detected multiple processes running the Functions host. Stop all but one process in order to debug.'));
}
}
interface IProcess {
pid: string;
}

Просмотреть файл

@ -19,6 +19,7 @@ import { deleteNode } from './commands/deleteNode';
import { deploy } from './commands/deploy';
import { editAppSetting } from './commands/editAppSetting';
import { openInPortal } from './commands/openInPortal';
import { pickFuncProcess } from './commands/pickFuncProcess';
import { renameAppSetting } from './commands/renameAppSetting';
import { restartFunctionApp } from './commands/restartFunctionApp';
import { startFunctionApp } from './commands/startFunctionApp';
@ -57,6 +58,7 @@ export function activate(context: vscode.ExtensionContext): void {
const actionHandler: AzureActionHandler = new AzureActionHandler(context, outputChannel, reporter);
actionHandler.registerCommand('azureFunctions.refresh', async (node?: IAzureNode) => await tree.refresh(node));
actionHandler.registerCommand('azureFunctions.pickProcess', async () => await pickFuncProcess());
actionHandler.registerCommand('azureFunctions.loadMore', async (node: IAzureNode) => await tree.loadMore(node));
actionHandler.registerCommand('azureFunctions.openInPortal', async (node?: IAzureNode<FunctionAppTreeItem>) => await openInPortal(tree, node));
actionHandler.registerCommandWithCustomTelemetry('azureFunctions.createFunction', async (properties: TelemetryProperties, _measurements: TelemetryMeasurements, functionAppPath?: string, templateId?: string, functionName?: string, ...functionSettings: string[]) => {

Просмотреть файл

@ -40,7 +40,11 @@ export class TemplateData {
'HttpTriggerWithParameters-JavaScript',
'ManualTrigger-JavaScript',
'QueueTrigger-JavaScript',
'TimerTrigger-JavaScript'
'TimerTrigger-JavaScript',
'HttpTrigger-CSharp',
'BlobTrigger-CSharp',
'QueueTrigger-CSharp',
'TimerTrigger-CSharp'
];
private readonly _javaTemplates: string[] = [
@ -80,7 +84,7 @@ export class TemplateData {
// Will refactor the code here when templates HTTP API is ready.
// See issue here: https://github.com/Microsoft/vscode-azurefunctions/issues/84
const javaTemplates: Template[] = this._templatesMap[runtime].filter((t: Template) => t.language === ProjectLanguage.JavaScript);
return javaTemplates.filter((t: Template) => this._javaTemplates.find((vt: string) => vt === convertTemplateIdToJava(t.id)));
return javaTemplates.filter((t: Template) => this._javaTemplates.find((vt: string) => vt === removeLanguageFromId(t.id)));
} else {
let templates: Template[] = this._templatesMap[runtime].filter((t: Template) => t.language.toLowerCase() === language.toLowerCase());
switch (templateFilter) {
@ -181,6 +185,6 @@ export class TemplateData {
}
}
export function convertTemplateIdToJava(id: string): string {
return id.replace('-JavaScript', '');
export function removeLanguageFromId(id: string): string {
return id.split('-')[0];
}

Просмотреть файл

@ -8,7 +8,8 @@ import * as vscode from 'vscode';
import { localize } from '../localize';
export namespace cpUtils {
export async function executeCommand(outputChannel: vscode.OutputChannel | undefined, workingDirectory: string, command: string, ...args: string[]): Promise<void> {
export async function executeCommand(outputChannel: vscode.OutputChannel | undefined, workingDirectory: string, command: string, ...args: string[]): Promise<string> {
let result: string = '';
await new Promise((resolve: () => void, reject: (e: Error) => void): void => {
const options: cp.SpawnOptions = {
cwd: workingDirectory,
@ -16,8 +17,15 @@ export namespace cpUtils {
};
const childProc: cp.ChildProcess = cp.spawn(command, args, options);
childProc.stdout.on('data', (data: string | Buffer) => {
data = data.toString();
result = result.concat(data);
if (outputChannel) {
outputChannel.append(data);
}
});
if (outputChannel) {
childProc.stdout.on('data', (data: string | Buffer) => outputChannel.append(data.toString()));
childProc.stderr.on('data', (data: string | Buffer) => outputChannel.append(data.toString()));
}
@ -30,5 +38,7 @@ export namespace cpUtils {
}
});
});
return result;
}
}

71
src/utils/dotnetUtils.ts Normal file
Просмотреть файл

@ -0,0 +1,71 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fse from 'fs-extra';
import * as os from 'os';
import * as path from 'path';
import { OutputChannel } from "vscode";
import { localize } from "../localize";
import { cpUtils } from "./cpUtils";
import * as fsUtil from './fs';
export namespace dotnetUtils {
const projectPackageId: string = 'microsoft.azurefunctions.projecttemplates';
const functionsPackageId: string = 'azure.functions.templates';
const templateVersion: string = '2.0.0-beta-10138';
// tslint:disable:no-multiline-string
const packagesCsproj: string = `<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="${projectPackageId}" Version="${templateVersion}" />
<PackageReference Include="${functionsPackageId}" Version="${templateVersion}" />
</ItemGroup>
</Project>
`;
async function validateDotnetInstalled(workingDirectory: string): Promise<void> {
try {
await cpUtils.executeCommand(undefined, workingDirectory, 'dotnet', '--version');
} catch (error) {
throw new Error(localize('azFunc.dotnetNotInstalled', 'You must have the "dotnet" cli installed to perform this operation.'));
}
}
export const funcProjectId: string = 'azureFunctionsProjectTemplates';
export async function validateTemplatesInstalled(outputChannel: OutputChannel, workingDirectory: string): Promise<void> {
await validateDotnetInstalled(workingDirectory);
const listOutput: string = await cpUtils.executeCommand(undefined, workingDirectory, 'dotnet', 'new', '--list');
if (!listOutput.includes(funcProjectId) || !listOutput.includes('HttpTrigger')) {
const tempFolder: string = path.join(os.tmpdir(), fsUtil.getRandomHexString());
await fse.ensureDir(tempFolder);
try {
await fse.writeFile(path.join(tempFolder, 'packages.csproj'), packagesCsproj);
// 'dotnet new --install' doesn't allow you to specify a source when installing templates
// Once these templates are published to Nuget.org, we can just call 'dotnet new --install' directly and skip all the tempFolder/restore stuff
outputChannel.show();
outputChannel.appendLine(localize('installingTemplates', 'Downloading dotnet templates for Azure Functions...'));
await cpUtils.executeCommand(undefined, tempFolder, 'dotnet', 'restore', '--packages', '.', '--source', 'https://www.myget.org/F/azure-appservice/api/v3/index.json', '--source', 'https://api.nuget.org/v3/index.json');
const fullProjectPackageId: string = `${projectPackageId}.${templateVersion}`;
outputChannel.appendLine(localize('projectPackageId', 'Installing "{0}"', fullProjectPackageId));
await cpUtils.executeCommand(undefined, tempFolder, 'dotnet', 'new', '--install', path.join(projectPackageId, templateVersion, `${fullProjectPackageId}.nupkg`));
const fullFunctionsPackageId: string = `${functionsPackageId}.${templateVersion}`;
outputChannel.appendLine(localize('functionsPackageId', 'Installing "{0}"', fullFunctionsPackageId));
await cpUtils.executeCommand(undefined, tempFolder, 'dotnet', 'new', '--install', path.join(functionsPackageId, templateVersion, `${fullFunctionsPackageId}.nupkg`));
outputChannel.appendLine(localize('finishedInstallingTemplates', 'Finished installing Azure Functions templates.'));
} finally {
await fse.remove(tempFolder);
}
}
}
}

Просмотреть файл

@ -1,194 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as fse from 'fs-extra';
import * as os from 'os';
import * as path from 'path';
import * as vscode from 'vscode';
import { WorkspaceConfiguration } from 'vscode';
import { createFunction } from '../src/commands/createFunction/createFunction';
import { extensionPrefix, ProjectLanguage, projectLanguageSetting, ProjectRuntime, projectRuntimeSetting, TemplateFilter, templateFilterSetting } from '../src/ProjectSettings';
import { TemplateData } from '../src/templates/TemplateData';
import * as fsUtil from '../src/utils/fs';
import { TestAzureAccount } from './TestAzureAccount';
import { TestUI } from './TestUI';
// tslint:disable-next-line:max-func-body-length
suite('Create JavaScript Core Function Tests', () => {
const templateData: TemplateData = new TemplateData();
const testFolder: string = path.join(os.tmpdir(), `azFunc.createFuncTests${fsUtil.getRandomHexString()}`);
const outputChannel: vscode.OutputChannel = vscode.window.createOutputChannel('Azure Functions Test');
const projectConfiguration: WorkspaceConfiguration = vscode.workspace.getConfiguration(extensionPrefix);
// tslint:disable-next-line:no-backbone-get-set-outside-model
const oldTemplateFilter: string | undefined = projectConfiguration.get(templateFilterSetting);
const oldProjectLanguage: string | undefined = projectConfiguration.get(projectLanguageSetting);
const oldProjectRuntime: string | undefined = projectConfiguration.get(projectRuntimeSetting);
suiteSetup(async () => {
await fse.ensureDir(path.join(testFolder, '.vscode'));
// Pretend to create the parent function app
await Promise.all([
fse.writeFile(path.join(testFolder, 'host.json'), ''),
fse.writeFile(path.join(testFolder, 'local.settings.json'), '{ "Values": { "AzureWebJobsStorage": "test" } }'),
fse.writeFile(path.join(testFolder, '.vscode', 'launch.json'), '')
]);
await projectConfiguration.update(templateFilterSetting, TemplateFilter.Core, vscode.ConfigurationTarget.Global);
await projectConfiguration.update(projectLanguageSetting, ProjectLanguage.JavaScript, vscode.ConfigurationTarget.Global);
await projectConfiguration.update(projectRuntimeSetting, ProjectRuntime.one, vscode.ConfigurationTarget.Global);
});
suiteTeardown(async () => {
outputChannel.dispose();
await fse.remove(testFolder);
await projectConfiguration.update(templateFilterSetting, oldTemplateFilter, vscode.ConfigurationTarget.Global);
await projectConfiguration.update(projectLanguageSetting, oldProjectLanguage, vscode.ConfigurationTarget.Global);
await projectConfiguration.update(projectRuntimeSetting, oldProjectRuntime, vscode.ConfigurationTarget.Global);
});
const blobTrigger: string = 'Blob trigger';
test(blobTrigger, async () => {
await testCreateFunction(
blobTrigger,
undefined, // New App Setting
'connectionStringKey1',
'connectionString',
undefined // Use default path
);
}).timeout(6000);
const cosmosDBTrigger: string = 'Cosmos DB trigger';
test(cosmosDBTrigger, async () => {
await testCreateFunction(
cosmosDBTrigger,
undefined, // New App Setting
'connectionStringKey2',
'connectionString',
'dbName',
'collectionName',
undefined, // Use default for 'create leases if doesn't exist'
undefined // Use default lease name
);
});
const eventHubTrigger: string = 'Event Hub trigger';
test(eventHubTrigger, async () => {
await testCreateFunction(
eventHubTrigger,
undefined, // New App Setting
'connectionStringKey3',
'connectionString',
undefined, // Use default event hub name
undefined // Use default event hub consumer group
);
});
const genericWebhook: string = 'Generic webhook';
test(genericWebhook, async () => {
await testCreateFunction(genericWebhook);
});
const gitHubWebhook: string = 'GitHub webhook';
test(gitHubWebhook, async () => {
await testCreateFunction(gitHubWebhook);
});
const httpTrigger: string = 'HTTP trigger';
test(httpTrigger, async () => {
await testCreateFunction(
httpTrigger,
undefined // Use default Authorization level
);
});
const httpTriggerWithParameters: string = 'HTTP trigger with parameters';
test(httpTriggerWithParameters, async () => {
await testCreateFunction(
httpTriggerWithParameters,
undefined // Use default Authorization level
);
});
const manualTrigger: string = 'Manual trigger';
test(manualTrigger, async () => {
await testCreateFunction(manualTrigger);
});
const queueTrigger: string = 'Queue trigger';
test(queueTrigger, async () => {
await testCreateFunction(
queueTrigger,
undefined, // New App Setting
'connectionStringKey4',
'connectionString',
undefined // Use default queue name
);
});
const serviceBusQueueTrigger: string = 'Service Bus Queue trigger';
test(serviceBusQueueTrigger, async () => {
await testCreateFunction(
serviceBusQueueTrigger,
undefined, // New App Setting
'connectionStringKey5',
'connectionString',
undefined, // Use default access rights
undefined // Use default queue name
);
});
const serviceBusTopicTrigger: string = 'Service Bus Topic trigger';
test(serviceBusTopicTrigger, async () => {
await testCreateFunction(
serviceBusTopicTrigger,
undefined, // New App Setting
'connectionStringKey6',
'connectionString',
undefined, // Use default access rights
undefined, // Use default topic name
undefined // Use default subscription name
);
});
const timerTrigger: string = 'Timer trigger';
test(timerTrigger, async () => {
await testCreateFunction(
timerTrigger,
undefined // Use default schedule
);
});
test('createFunction API', async () => {
const templateId: string = 'HttpTrigger-JavaScript';
const functionName: string = 'createFunctionApi';
const authLevel: string = 'Anonymous';
await vscode.commands.executeCommand('azureFunctions.createFunction', testFolder, templateId, functionName, authLevel);
await validateFunction(functionName);
}).timeout(5000);
async function testCreateFunction(templateName: string, ...inputs: (string | undefined) []): Promise<void> {
// Setup common inputs
const funcName: string = templateName.replace(/ /g, '');
inputs.unshift(funcName); // Specify the function name
inputs.unshift(templateName); // Select the function template
inputs.unshift(testFolder); // Select the test func app folder
if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) {
inputs.unshift('$(file-directory) Browse...'); // If the test environment has an open workspace, select the 'Browse...' option
}
const ui: TestUI = new TestUI(inputs);
await createFunction({}, outputChannel, new TestAzureAccount(), templateData, ui);
assert.equal(inputs.length, 0, 'Not all inputs were used.');
await validateFunction(funcName);
}
async function validateFunction(funcName: string): Promise<void> {
const functionPath: string = path.join(testFolder, funcName);
assert.equal(await fse.pathExists(path.join(functionPath, 'index.js')), true, 'index.js does not exist');
assert.equal(await fse.pathExists(path.join(functionPath, 'function.json')), true, 'function.json does not exist');
}
});

Просмотреть файл

@ -0,0 +1,85 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as fse from 'fs-extra';
import * as os from 'os';
import * as path from 'path';
import * as vscode from 'vscode';
import { WorkspaceConfiguration } from 'vscode';
import { createFunction } from '../../src/commands/createFunction/createFunction';
import { extensionPrefix, ProjectLanguage, projectLanguageSetting, ProjectRuntime, projectRuntimeSetting, TemplateFilter, templateFilterSetting } from '../../src/ProjectSettings';
import { TemplateData } from '../../src/templates/TemplateData';
import * as fsUtil from '../../src/utils/fs';
import { TestAzureAccount } from '../TestAzureAccount';
import { TestUI } from '../TestUI';
export abstract class FunctionTesterBase implements vscode.Disposable {
public testFolder: string;
public outputChannel: vscode.OutputChannel;
protected abstract _language: ProjectLanguage;
protected abstract _runtime: ProjectRuntime;
private _templateData: TemplateData;
private _projectConfiguration: WorkspaceConfiguration;
private _oldTemplateFilter: string | undefined;
private _oldProjectLanguage: string | undefined;
private _oldProjectRuntime: string | undefined;
constructor() {
this.testFolder = path.join(os.tmpdir(), `azFunc.createFuncTests${fsUtil.getRandomHexString()}`);
this.outputChannel = vscode.window.createOutputChannel('Azure Functions Test');
this._templateData = new TemplateData();
this._projectConfiguration = vscode.workspace.getConfiguration(extensionPrefix);
}
public async initAsync(): Promise<void> {
// tslint:disable-next-line:no-backbone-get-set-outside-model
this._oldTemplateFilter = this._projectConfiguration.get(templateFilterSetting);
this._oldProjectLanguage = this._projectConfiguration.get(projectLanguageSetting);
this._oldProjectRuntime = this._projectConfiguration.get(projectRuntimeSetting);
await fse.ensureDir(path.join(this.testFolder, '.vscode'));
// Pretend to create the parent function app
await Promise.all([
fse.writeFile(path.join(this.testFolder, 'host.json'), ''),
fse.writeFile(path.join(this.testFolder, 'local.settings.json'), '{ "Values": { "AzureWebJobsStorage": "test" } }'),
fse.writeFile(path.join(this.testFolder, '.vscode', 'launch.json'), '')
]);
await this._projectConfiguration.update(templateFilterSetting, TemplateFilter.All, vscode.ConfigurationTarget.Global);
await this._projectConfiguration.update(projectLanguageSetting, this._language, vscode.ConfigurationTarget.Global);
await this._projectConfiguration.update(projectRuntimeSetting, this._runtime, vscode.ConfigurationTarget.Global);
}
public async dispose(): Promise<void> {
this.outputChannel.dispose();
await fse.remove(this.testFolder);
await this._projectConfiguration.update(templateFilterSetting, this._oldTemplateFilter, vscode.ConfigurationTarget.Global);
await this._projectConfiguration.update(projectLanguageSetting, this._oldProjectLanguage, vscode.ConfigurationTarget.Global);
await this._projectConfiguration.update(projectRuntimeSetting, this._oldProjectRuntime, vscode.ConfigurationTarget.Global);
}
public async testCreateFunction(templateName: string, ...inputs: (string | undefined) []): Promise<void> {
// Setup common inputs
const funcName: string = templateName.replace(/ /g, '');
inputs.unshift(funcName); // Specify the function name
inputs.unshift(templateName); // Select the function template
inputs.unshift(this.testFolder); // Select the test func app folder
if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) {
inputs.unshift('$(file-directory) Browse...'); // If the test environment has an open workspace, select the 'Browse...' option
}
const ui: TestUI = new TestUI(inputs);
await createFunction({}, this.outputChannel, new TestAzureAccount(), this._templateData, ui);
assert.equal(inputs.length, 0, 'Not all inputs were used.');
await this.validateFunction(funcName);
}
public abstract async validateFunction(funcName: string): Promise<void>;
}

Просмотреть файл

@ -0,0 +1,86 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as fse from 'fs-extra';
import { IHookCallbackContext, ISuiteCallbackContext } from 'mocha';
import * as path from 'path';
import * as vscode from 'vscode';
import { ProjectLanguage, ProjectRuntime } from '../../src/ProjectSettings';
import { dotnetUtils } from '../../src/utils/dotnetUtils';
import { FunctionTesterBase } from './FunctionTesterBase';
class CSharpFunctionTester extends FunctionTesterBase {
protected _language: ProjectLanguage = ProjectLanguage.CSharp;
protected _runtime: ProjectRuntime = ProjectRuntime.beta;
public async validateFunction(funcName: string): Promise<void> {
assert.equal(await fse.pathExists(path.join(this.testFolder, `${funcName}.cs`)), true, 'cs file does not exist');
}
}
// tslint:disable-next-line:no-function-expression
suite('Create C# Function Tests', async function (this: ISuiteCallbackContext): Promise<void> {
this.timeout(10 * 1000);
const csTester: CSharpFunctionTester = new CSharpFunctionTester();
// tslint:disable-next-line:no-function-expression
suiteSetup(async function (this: IHookCallbackContext): Promise<void> {
this.timeout(60 * 1000);
await csTester.initAsync();
await dotnetUtils.validateTemplatesInstalled(csTester.outputChannel, csTester.testFolder);
});
suiteTeardown(async () => {
await csTester.dispose();
});
const blobTrigger: string = 'Blob trigger';
test(blobTrigger, async () => {
await csTester.testCreateFunction(
blobTrigger,
undefined, // New App Setting
'connectionStringKey1',
'connectionString',
undefined // Use default path
);
});
const httpTrigger: string = 'HTTP trigger';
test(httpTrigger, async () => {
await csTester.testCreateFunction(
httpTrigger,
undefined // Use default Authorization level
);
});
const queueTrigger: string = 'Queue trigger';
test(queueTrigger, async () => {
await csTester.testCreateFunction(
queueTrigger,
undefined, // New App Setting
'connectionStringKey4',
'connectionString',
undefined // Use default queue name
);
});
const timerTrigger: string = 'Timer trigger';
test(timerTrigger, async () => {
await csTester.testCreateFunction(
timerTrigger,
undefined // Use default schedule
);
});
test('createFunction API', async () => {
const templateId: string = 'HttpTrigger-CSharp';
const functionName: string = 'createFunctionApi';
const authLevel: string = 'Anonymous';
await vscode.commands.executeCommand('azureFunctions.createFunction', csTester.testFolder, templateId, functionName, authLevel);
await csTester.validateFunction(functionName);
});
});

Просмотреть файл

@ -0,0 +1,158 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as fse from 'fs-extra';
import { ISuiteCallbackContext } from 'mocha';
import * as path from 'path';
import * as vscode from 'vscode';
import { ProjectLanguage, ProjectRuntime } from '../../src/ProjectSettings';
import { FunctionTesterBase } from './FunctionTesterBase';
class JSFunctionTester extends FunctionTesterBase {
protected _language: ProjectLanguage = ProjectLanguage.JavaScript;
protected _runtime: ProjectRuntime = ProjectRuntime.one;
public async validateFunction(funcName: string): Promise<void> {
const functionPath: string = path.join(this.testFolder, funcName);
assert.equal(await fse.pathExists(path.join(functionPath, 'index.js')), true, 'index.js does not exist');
assert.equal(await fse.pathExists(path.join(functionPath, 'function.json')), true, 'function.json does not exist');
}
}
// tslint:disable-next-line:max-func-body-length no-function-expression
suite('Create JavaScript Function Tests', async function (this: ISuiteCallbackContext): Promise<void> {
this.timeout(6 * 1000);
const jsTester: JSFunctionTester = new JSFunctionTester();
suiteSetup(async () => {
await jsTester.initAsync();
});
suiteTeardown(async () => {
await jsTester.dispose();
});
const blobTrigger: string = 'Blob trigger';
test(blobTrigger, async () => {
await jsTester.testCreateFunction(
blobTrigger,
undefined, // New App Setting
'connectionStringKey1',
'connectionString',
undefined // Use default path
);
});
const cosmosDBTrigger: string = 'Cosmos DB trigger';
test(cosmosDBTrigger, async () => {
await jsTester.testCreateFunction(
cosmosDBTrigger,
undefined, // New App Setting
'connectionStringKey2',
'connectionString',
'dbName',
'collectionName',
undefined, // Use default for 'create leases if doesn't exist'
undefined // Use default lease name
);
});
const eventHubTrigger: string = 'Event Hub trigger';
test(eventHubTrigger, async () => {
await jsTester.testCreateFunction(
eventHubTrigger,
undefined, // New App Setting
'connectionStringKey3',
'connectionString',
undefined, // Use default event hub name
undefined // Use default event hub consumer group
);
});
const genericWebhook: string = 'Generic webhook';
test(genericWebhook, async () => {
await jsTester.testCreateFunction(genericWebhook);
});
const gitHubWebhook: string = 'GitHub webhook';
test(gitHubWebhook, async () => {
await jsTester.testCreateFunction(gitHubWebhook);
});
const httpTrigger: string = 'HTTP trigger';
test(httpTrigger, async () => {
await jsTester.testCreateFunction(
httpTrigger,
undefined // Use default Authorization level
);
});
const httpTriggerWithParameters: string = 'HTTP trigger with parameters';
test(httpTriggerWithParameters, async () => {
await jsTester.testCreateFunction(
httpTriggerWithParameters,
undefined // Use default Authorization level
);
});
const manualTrigger: string = 'Manual trigger';
test(manualTrigger, async () => {
await jsTester.testCreateFunction(manualTrigger);
});
const queueTrigger: string = 'Queue trigger';
test(queueTrigger, async () => {
await jsTester.testCreateFunction(
queueTrigger,
undefined, // New App Setting
'connectionStringKey4',
'connectionString',
undefined // Use default queue name
);
});
const serviceBusQueueTrigger: string = 'Service Bus Queue trigger';
test(serviceBusQueueTrigger, async () => {
await jsTester.testCreateFunction(
serviceBusQueueTrigger,
undefined, // New App Setting
'connectionStringKey5',
'connectionString',
undefined, // Use default access rights
undefined // Use default queue name
);
});
const serviceBusTopicTrigger: string = 'Service Bus Topic trigger';
test(serviceBusTopicTrigger, async () => {
await jsTester.testCreateFunction(
serviceBusTopicTrigger,
undefined, // New App Setting
'connectionStringKey6',
'connectionString',
undefined, // Use default access rights
undefined, // Use default topic name
undefined // Use default subscription name
);
});
const timerTrigger: string = 'Timer trigger';
test(timerTrigger, async () => {
await jsTester.testCreateFunction(
timerTrigger,
undefined // Use default schedule
);
});
test('createFunction API', async () => {
const templateId: string = 'HttpTrigger-JavaScript';
const functionName: string = 'createFunctionApi';
const authLevel: string = 'Anonymous';
await vscode.commands.executeCommand('azureFunctions.createFunction', jsTester.testFolder, templateId, functionName, authLevel);
await jsTester.validateFunction(functionName);
});
});

Просмотреть файл

@ -5,17 +5,30 @@
import * as assert from 'assert';
import * as fse from 'fs-extra';
import { IHookCallbackContext, ISuiteCallbackContext } from 'mocha';
import * as os from 'os';
import * as path from 'path';
import * as vscode from 'vscode';
import { createNewProject } from '../src/commands/createNewProject/createNewProject';
import { ProjectLanguage } from '../src/ProjectSettings';
import { dotnetUtils } from '../src/utils/dotnetUtils';
import * as fsUtil from '../src/utils/fs';
import { TestUI } from './TestUI';
suite('Create New Project Tests', () => {
// tslint:disable-next-line:no-function-expression
suite('Create New Project Tests', async function (this: ISuiteCallbackContext): Promise<void> {
this.timeout(15 * 1000);
const testFolderPath: string = path.join(os.tmpdir(), `azFunc.createNewProjectTests${fsUtil.getRandomHexString()}`);
const outputChannel: vscode.OutputChannel = vscode.window.createOutputChannel('Azure Functions Test');
// tslint:disable-next-line:no-function-expression
suiteSetup(async function (this: IHookCallbackContext): Promise<void> {
this.timeout(60 * 1000);
await fse.ensureDir(testFolderPath);
await dotnetUtils.validateTemplatesInstalled(outputChannel, testFolderPath);
});
suiteTeardown(async () => {
outputChannel.dispose();
await fse.remove(testFolderPath);
@ -33,23 +46,34 @@ suite('Create New Project Tests', () => {
undefined,
undefined
);
await testJavaProjectFilesExist(projectPath);
}).timeout(60 * 1000);
await testCommonProjectFilesExist(projectPath, true);
assert.equal(await fse.pathExists(path.join(projectPath, 'src')), true, 'src folder does not exist');
assert.equal(await fse.pathExists(path.join(projectPath, 'pom.xml')), true, 'pom.xml does not exist');
});
const javaScriptProject: string = 'JavaScriptProject';
test(javaScriptProject, async () => {
const projectPath: string = path.join(testFolderPath, javaScriptProject);
await testCreateNewProject(projectPath, ProjectLanguage.JavaScript);
await testProjectFilesExist(projectPath);
await testCommonProjectFilesExist(projectPath);
});
const csharpProject: string = 'CSharpProject';
test(csharpProject, async () => {
const projectPath: string = path.join(testFolderPath, csharpProject);
await testCreateNewProject(projectPath, ProjectLanguage.CSharp);
await testCommonProjectFilesExist(projectPath, true);
const projectName: string = path.basename(projectPath);
assert.equal(await fse.pathExists(path.join(projectPath, `${projectName}.csproj`)), true, 'csproj does not exist');
});
test('createNewProject API', async () => {
const projectPath: string = path.join(testFolderPath, 'createNewProjectApi');
await vscode.commands.executeCommand('azureFunctions.createNewProject', projectPath, 'JavaScript', false /* openFolder */);
await testProjectFilesExist(projectPath);
await testCommonProjectFilesExist(projectPath);
});
async function testCreateNewProject(projectPath: string, language: string, ...inputs: (string | undefined)[]): Promise<void> {
async function testCreateNewProject(projectPath: string, language: string, ...inputs: (string | undefined) []): Promise<void> {
// Setup common inputs
inputs.unshift(language); // Specify the function name
inputs.unshift(projectPath); // Select the test func app folder
@ -62,7 +86,7 @@ suite('Create New Project Tests', () => {
assert.equal(inputs.length, 0, 'Not all inputs were used.');
}
async function testProjectFilesExist(projectPath: string): Promise<void> {
async function testCommonProjectFilesExist(projectPath: string, hasExtensionRecommendations: boolean = false): Promise<void> {
assert.equal(await fse.pathExists(path.join(projectPath, '.gitignore')), true, '.gitignore does not exist');
assert.equal(await fse.pathExists(path.join(projectPath, 'host.json')), true, 'host.json does not exist');
assert.equal(await fse.pathExists(path.join(projectPath, 'local.settings.json')), true, 'function.json does not exist');
@ -71,11 +95,8 @@ suite('Create New Project Tests', () => {
assert.equal(await fse.pathExists(path.join(vscodePath, 'settings.json')), true, 'settings.json does not exist');
assert.equal(await fse.pathExists(path.join(vscodePath, 'launch.json')), true, 'launch.json does not exist');
assert.equal(await fse.pathExists(path.join(vscodePath, 'tasks.json')), true, 'tasks.json does not exist');
}
async function testJavaProjectFilesExist(projectPath: string): Promise<void> {
await testProjectFilesExist(projectPath);
assert.equal(await fse.pathExists(path.join(projectPath, 'src')), true, 'src folder does not exist');
assert.equal(await fse.pathExists(path.join(projectPath, 'pom.xml')), true, 'pom.xml does not exist');
if (hasExtensionRecommendations) {
assert.equal(await fse.pathExists(path.join(vscodePath, 'extensions.json')), true, 'extensions.json does not exist');
}
}
});

Просмотреть файл

@ -20,4 +20,9 @@ suite('Template Data Tests', () => {
const templates: Template[] = await templateData.getTemplates(ProjectLanguage.Java, ProjectRuntime.beta, TemplateFilter.Verified);
assert.equal(templates.length, 4);
});
test('Default CSharp Templates Count', async () => {
const templates: Template[] = await templateData.getTemplates(ProjectLanguage.CSharp, ProjectRuntime.beta, TemplateFilter.Verified);
assert.equal(templates.length, 4);
});
});

Просмотреть файл

@ -9,6 +9,7 @@ are grateful to these developers for their contribution to open source.
2. request-promise (https://github.com/request/request-promise)
3. xml2js (https://github.com/Leonidas-from-XIV/node-xml2js)
4. clipboardy (https://github.com/sindresorhus/clipboardy)
5. ps-node (https://github.com/neekey/ps)
fs-extra NOTICES BEGIN HERE
=============================
@ -94,4 +95,32 @@ The above copyright notice and this permission notice shall be included in all c
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
END OF clipboardy NOTICES AND INFORMATION
==================================
==================================
ps-node NOTICES BEGIN HERE
=============================
The MIT License (MIT)
Copyright (c) 2015 Neekey
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
END OF ps-node NOTICES AND INFORMATION
==================================