Add publish profile to sql proj and tree (#22008)

* Read publish profiles stored in sqlproj file and present it in the projects tree

* Save publish profile and add it to sqlproj file, and present it in the tree

* Fix context menu operations

* Add tests

* Address comments
This commit is contained in:
Sakshi Sharma
2023-02-23 22:32:12 -08:00
committed by GitHub
parent 91cdd610fd
commit 41e2767880
11 changed files with 267 additions and 14 deletions

View File

@@ -14,6 +14,7 @@ const localize = nls.loadMessageBundle();
export const dataSourcesFileName = 'datasources.json';
export const sqlprojExtension = '.sqlproj';
export const sqlFileExtension = '.sql';
export const publishProfileExtension = '.publish.xml';
export const openApiSpecFileExtensions = ['yaml', 'yml', 'json'];
export const schemaCompareExtensionId = 'microsoft.schema-compare';
export const master = 'master';
@@ -485,6 +486,7 @@ export const ImportElements = localize('importElements', "Import Elements");
export const ProjectReferenceNameElement = localize('projectReferenceNameElement', "Project reference name element");
export const ProjectReferenceElement = localize('projectReferenceElement', "Project reference");
export const DacpacReferenceElement = localize('dacpacReferenceElement', "Dacpac reference");
export const PublishProfileElements = localize('publishProfileElements', "Publish profile elements");
/** Name of the property item in the project file that defines default database collation. */
export const DefaultCollationProperty = 'DefaultCollation';

View File

@@ -771,3 +771,13 @@ export function isValidBasename(name?: string): boolean {
export function isValidBasenameErrorMessage(name?: string): string {
return getDataWorkspaceExtensionApi().isValidBasenameErrorMessage(name);
}
/**
* Checks if the provided file is a publish profile
* @param fileName filename to check
* @returns True if it is a publish profile, otherwise false
*/
export function isPublishProfile(fileName: string): boolean {
const hasPublishExtension = fileName.trim().toLowerCase().endsWith(constants.publishProfileExtension);
return hasPublishExtension;
}

View File

@@ -1444,7 +1444,7 @@ export class ProjectsController {
if (fileOrFolder) {
// use relative path and not tree paths for files and folder
const allFileEntries = project.files.concat(project.preDeployScripts).concat(project.postDeployScripts).concat(project.noneDeployScripts);
const allFileEntries = project.files.concat(project.preDeployScripts).concat(project.postDeployScripts).concat(project.noneDeployScripts).concat(project.publishProfiles);
// trim trailing slash since folders with and without a trailing slash are allowed in a sqlproj
const trimmedUri = utils.trimChars(utils.getPlatformSafeFileEntryPath(utils.trimUri(fileOrFolder.projectFileUri, fileOrFolder.fileSystemUri)), '/');

View File

@@ -947,6 +947,9 @@ export class PublishDatabaseDialog {
this.profileUsed = true;
this.publishProfileUri = filePath;
await this.project.addPublishProfileToProjFile(filePath.fsPath);
void vscode.commands.executeCommand(constants.refreshDataWorkspaceCommand); //refresh data workspace to load the newly added profile to the tree
});
return saveProfileAsButton;

View File

@@ -46,6 +46,7 @@ export class Project implements ISqlProject {
private _isSdkStyleProject: boolean = false; // https://docs.microsoft.com/en-us/dotnet/core/project-sdk/overview
private _outputPath: string = '';
private _configuration: Configuration = Configuration.Debug;
private _publishProfiles: FileProjectEntry[] = [];
public get dacpacOutputPath(): string {
return path.join(this.outputPath, `${this._projectFileName}.dacpac`);
@@ -111,6 +112,10 @@ export class Project implements ISqlProject {
return this._configuration;
}
public get publishProfiles(): FileProjectEntry[] {
return this._publishProfiles;
}
private projFileXmlDoc: Document | undefined = undefined;
constructor(projectFilePath: string) {
@@ -153,6 +158,9 @@ export class Project implements ISqlProject {
this._databaseReferences = this.readDatabaseReferences();
this._importedTargets = this.readImportedTargets();
// get publish profiles specified in the sqlproj
this._publishProfiles = this.readPublishProfiles();
// find all SQLCMD variables to include
try {
this._sqlCmdVariables = utils.readSqlCmdVariables(this.projFileXmlDoc, false);
@@ -468,7 +476,7 @@ export class Project implements ISqlProject {
const noneItems = itemGroup.getElementsByTagName(constants.None);
for (let n = 0; n < noneItems.length; n++) {
const includeAttribute = noneItems[n].getAttribute(constants.Include);
if (includeAttribute) {
if (includeAttribute && !utils.isPublishProfile(includeAttribute)) {
noneDeployScripts.push(this.createFileProjectEntry(includeAttribute, EntryType.File));
}
}
@@ -505,6 +513,34 @@ export class Project implements ISqlProject {
return noneRemoveScripts;
}
/**
*
* @returns all the publish profiles (ending with *.publish.xml) specified as <None Include="file.publish.xml" /> in the sqlproj
*/
private readPublishProfiles(): FileProjectEntry[] {
const publishProfiles: FileProjectEntry[] = [];
for (let ig = 0; ig < this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ItemGroup).length; ig++) {
const itemGroup = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ItemGroup)[ig];
// find all publish profile scripts to include
try {
const noneItems = itemGroup.getElementsByTagName(constants.None);
for (let n = 0; n < noneItems.length; n++) {
const includeAttribute = noneItems[n].getAttribute(constants.Include);
if (includeAttribute && utils.isPublishProfile(includeAttribute)) {
publishProfiles.push(this.createFileProjectEntry(includeAttribute, EntryType.File));
}
}
} catch (e) {
void window.showErrorMessage(constants.errorReadingProject(constants.PublishProfileElements, this.projectFilePath));
console.error(utils.getErrorMessage(e));
}
}
return publishProfiles;
}
private readDatabaseReferences(): IDatabaseReferenceProjectEntry[] {
const databaseReferenceEntries: IDatabaseReferenceProjectEntry[] = [];
@@ -949,18 +985,19 @@ export class Project implements ISqlProject {
}
public async exclude(entry: FileProjectEntry): Promise<void> {
const toExclude: FileProjectEntry[] = this._files.concat(this._preDeployScripts).concat(this._postDeployScripts).concat(this._noneDeployScripts).filter(x => x.fsUri.fsPath.startsWith(entry.fsUri.fsPath));
const toExclude: FileProjectEntry[] = this._files.concat(this._preDeployScripts).concat(this._postDeployScripts).concat(this._noneDeployScripts).concat(this._publishProfiles).filter(x => x.fsUri.fsPath.startsWith(entry.fsUri.fsPath));
await this.removeFromProjFile(toExclude);
this._files = this._files.filter(x => !x.fsUri.fsPath.startsWith(entry.fsUri.fsPath));
this._preDeployScripts = this._preDeployScripts.filter(x => !x.fsUri.fsPath.startsWith(entry.fsUri.fsPath));
this._postDeployScripts = this._postDeployScripts.filter(x => !x.fsUri.fsPath.startsWith(entry.fsUri.fsPath));
this._noneDeployScripts = this._noneDeployScripts.filter(x => !x.fsUri.fsPath.startsWith(entry.fsUri.fsPath));
this._publishProfiles = this._publishProfiles.filter(x => !x.fsUri.fsPath.startsWith(entry.fsUri.fsPath));
}
public async deleteFileFolder(entry: FileProjectEntry): Promise<void> {
// compile a list of folder contents to delete; if entry is a file, contents will contain only itself
const toDeleteFiles: FileProjectEntry[] = this._files.concat(this._preDeployScripts).concat(this._postDeployScripts).concat(this._noneDeployScripts).filter(x => x.fsUri.fsPath.startsWith(entry.fsUri.fsPath) && x.type === EntryType.File);
const toDeleteFiles: FileProjectEntry[] = this._files.concat(this._preDeployScripts).concat(this._postDeployScripts).concat(this._noneDeployScripts).concat(this._publishProfiles).filter(x => x.fsUri.fsPath.startsWith(entry.fsUri.fsPath) && x.type === EntryType.File);
const toDeleteFolders: FileProjectEntry[] = this._files.filter(x => x.fsUri.fsPath.startsWith(entry.fsUri.fsPath) && x.type === EntryType.Folder);
await Promise.all(toDeleteFiles.map(x => fs.unlink(x.fsUri.fsPath)));
@@ -1165,6 +1202,24 @@ export class Project implements ISqlProject {
return this.getCollectionProjectPropertyValue(constants.DatabaseSource);
}
/**
* Adds publish profile to the project
*
* @param relativeFilePath Relative path of the file
*/
public async addPublishProfileToProjFile(absolutePublishProfilePath: string): Promise<FileProjectEntry> {
const relativePublishProfilePath = (utils.trimUri(Uri.file(this.projectFilePath), Uri.file(absolutePublishProfilePath)));
// Update sqlproj XML
const fileEntry = this.createFileProjectEntry(relativePublishProfilePath, EntryType.File);
this._publishProfiles.push(fileEntry);
await this.addToProjFile(fileEntry, constants.None);
return fileEntry;
}
public createFileProjectEntry(relativePath: string, entryType: EntryType, sqlObjectType?: string, containsCreateTableStatement?: boolean): FileProjectEntry {
let platformSafeRelativePath = utils.getPlatformSafeFileEntryPath(relativePath);
return new FileProjectEntry(
@@ -1176,7 +1231,8 @@ export class Project implements ISqlProject {
}
private findOrCreateItemGroup(containedTag?: string, prePostScriptExist?: { scriptExist: boolean; }): Element {
let outputItemGroup = undefined;
let outputItemGroup: Element[] = []; // "None" can have more than one ItemGroup, for "None Include" (for pre/post deploy scripts and publish profiles), "None Remove"
let returnItemGroup;
// search for a particular item goup if a child type is provided
if (containedTag) {
@@ -1184,25 +1240,45 @@ export class Project implements ISqlProject {
for (let ig = 0; ig < this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ItemGroup).length; ig++) {
const currentItemGroup = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ItemGroup)[ig];
// if we find the tag, use the ItemGroup
if (currentItemGroup.getElementsByTagName(containedTag).length > 0) {
outputItemGroup = currentItemGroup;
break;
outputItemGroup.push(currentItemGroup);
}
}
}
// if none already exist, make a new ItemGroup for it
if (!outputItemGroup) {
outputItemGroup = this.projFileXmlDoc!.createElement(constants.ItemGroup);
this.projFileXmlDoc!.documentElement.appendChild(outputItemGroup);
if (outputItemGroup.length === 0) {
returnItemGroup = this.projFileXmlDoc!.createElement(constants.ItemGroup);
this.projFileXmlDoc!.documentElement.appendChild(returnItemGroup);
if (prePostScriptExist) {
prePostScriptExist.scriptExist = false;
}
} else { // if item group exists and containedTag = None, read the content to find None Include with publish profile
if (containedTag === constants.None) {
for (let ig = 0; ig < outputItemGroup.length; ig++) {
const itemGroup = outputItemGroup[ig];
// find all none include scripts specified in the sqlproj
const noneItems = itemGroup.getElementsByTagName(constants.None);
for (let n = 0; n < noneItems.length; n++) {
let noneIncludeItem = noneItems[n].getAttribute(constants.Include);
if (noneIncludeItem && utils.isPublishProfile(noneIncludeItem)) {
returnItemGroup = itemGroup;
break;
}
}
}
if (!returnItemGroup) {
returnItemGroup = this.projFileXmlDoc!.createElement(constants.ItemGroup);
this.projFileXmlDoc!.documentElement.appendChild(returnItemGroup);
}
} else {
returnItemGroup = outputItemGroup[0]; // Return the first item group that was found, to match prior implementation
}
}
return outputItemGroup;
return returnItemGroup;
}
private async addFileToProjFile(filePath: string, xmlTag: string, attributes?: Map<string, string>): Promise<void> {
@@ -1220,6 +1296,8 @@ export class Project implements ISqlProject {
void window.showInformationMessage(constants.deployScriptExists(xmlTag));
xmlTag = constants.None; // Add only one pre-deploy and post-deploy script. All additional ones get added in the same item group with None tag
}
} else if (xmlTag === constants.None) { // Add publish profiles with None tag
itemGroup = this.findOrCreateItemGroup(xmlTag);
}
else {
if (this.isSdkStyleProject) {
@@ -1260,7 +1338,7 @@ export class Project implements ISqlProject {
itemGroup.appendChild(newFileNode);
}
private async removeFileFromProjFile(path: string): Promise<void> {
private async removeFileFromProjFile(path: string): Promise<void> {//TODO: publish profile
const fileNodes = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.Build);
const preDeployNodes = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.PreDeploy);
const postDeployNodes = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.PostDeploy);

View File

@@ -65,7 +65,8 @@ export class ProjectRootTreeItem extends BaseProjectTreeItem {
let treeItemList = this.project.files
.concat(this.project.preDeployScripts)
.concat(this.project.postDeployScripts)
.concat(this.project.noneDeployScripts);
.concat(this.project.noneDeployScripts)
.concat(this.project.publishProfiles);
for (const entry of treeItemList) {
if (entry.type !== EntryType.File && entry.relativePath.startsWith(RelativeOuterPath)) {

View File

@@ -40,6 +40,7 @@ export let openSdkStyleSqlProjectWithFilesSpecifiedBaseline: string;
export let openSdkStyleSqlProjectWithGlobsSpecifiedBaseline: string;
export let openSdkStyleSqlProjectWithBuildRemoveBaseline: string;
export let openSdkStyleSqlProjectNoProjectGuidBaseline: string;
export let openSqlProjectWithAdditionalPublishProfileBaseline: string;
const baselineFolderPath = __dirname;
@@ -77,6 +78,7 @@ export async function loadBaselines() {
openSdkStyleSqlProjectWithGlobsSpecifiedBaseline = await loadBaseline(baselineFolderPath, 'openSdkStyleSqlProjectWithGlobsSpecifiedBaseline.xml');
openSdkStyleSqlProjectWithBuildRemoveBaseline = await loadBaseline(baselineFolderPath, 'openSdkStyleSqlProjectWithBuildRemoveBaseline.xml');
openSdkStyleSqlProjectNoProjectGuidBaseline = await loadBaseline(baselineFolderPath, 'openSdkStyleSqlProjectNoProjectGuidBaseline.xml');
openSqlProjectWithAdditionalPublishProfileBaseline = await loadBaseline(baselineFolderPath, 'openSqlProjectWithAdditionalPublishProfileBaseline.xml');
}
async function loadBaseline(baselineFolderPath: string, fileName: string): Promise<string> {

View File

@@ -105,6 +105,11 @@
<PostDeploy Include="Script.PostDeployment1.sql" />
<None Include="Tables\Script.PostDeployment1.sql" />
</ItemGroup>
<ItemGroup>
<None Include="TestProjectName_1.publish.xml" />
<None Include="TestProjectName_2.publish.xml" />
<None Include="TestProjectName_3.publish.xml" />
</ItemGroup>
<Target Name="BeforeBuild">
<Delete Files="$(BaseIntermediateOutputPath)\project.assets.json" />
</Target>

View File

@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<Name>TestProjectName</Name>
<SchemaVersion>2.0</SchemaVersion>
<ProjectVersion>4.1</ProjectVersion>
<ProjectGuid>{BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575}</ProjectGuid>
<DSP>Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider</DSP>
<OutputType>Database</OutputType>
<RootPath>
</RootPath>
<RootNamespace>TestProjectName</RootNamespace>
<AssemblyName>TestProjectName</AssemblyName>
<ModelCollation>1033, CI</ModelCollation>
<DefaultFileStructure>BySchemaAndSchemaType</DefaultFileStructure>
<DeployToDatabase>True</DeployToDatabase>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<TargetLanguage>CS</TargetLanguage>
<AppDesignerFolder>Properties</AppDesignerFolder>
<SqlServerVerification>False</SqlServerVerification>
<IncludeCompositeObjects>True</IncludeCompositeObjects>
<TargetDatabaseSet>True</TargetDatabaseSet>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<OutputPath>bin\Release\</OutputPath>
<BuildScriptName>$(MSBuildProjectName).sql</BuildScriptName>
<TreatWarningsAsErrors>False</TreatWarningsAsErrors>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<DefineDebug>false</DefineDebug>
<DefineTrace>true</DefineTrace>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<OutputPath>bin\Debug\</OutputPath>
<BuildScriptName>$(MSBuildProjectName).sql</BuildScriptName>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<DefineDebug>true</DefineDebug>
<DefineTrace>true</DefineTrace>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">11.0</VisualStudioVersion>
<!-- Default to the v11.0 targets path if the targets file for the current VS version is not found -->
<SSDTExists Condition="Exists('$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\SSDT\Microsoft.Data.Tools.Schema.SqlTasks.targets')">True</SSDTExists>
<VisualStudioVersion Condition="'$(SSDTExists)' == ''">11.0</VisualStudioVersion>
</PropertyGroup>
<Import Condition="'$(NetCoreBuild)' == 'true'" Project="$(NETCoreTargetsPath)\Microsoft.Data.Tools.Schema.SqlTasks.targets" />
<Import Condition="'$(NetCoreBuild)' != 'true' AND '$(SQLDBExtensionsRefPath)' != ''" Project="$(SQLDBExtensionsRefPath)\Microsoft.Data.Tools.Schema.SqlTasks.targets" />
<Import Condition="'$(NetCoreBuild)' != 'true' AND '$(SQLDBExtensionsRefPath)' == ''" Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\SSDT\Microsoft.Data.Tools.Schema.SqlTasks.targets" />
<ItemGroup>
<PackageReference Condition="'$(NetCoreBuild)' == 'true'" Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<Folder Include="Properties" />
<Folder Include="Tables" />
<Folder Include="Views" />
<Folder Include="Views\Maintenance" />
</ItemGroup>
<ItemGroup>
<Build Include="Tables\Users.sql" />
<Build Include="Tables\Action History.sql" />
<Build Include="Views\Maintenance\Database Performance.sql" />
<Build Include="..\Test\Test.sql" />
</ItemGroup>
<ItemGroup>
<Folder Include="Views\User" />
<Build Include="Views\User\Profile.sql" />
</ItemGroup>
<ItemGroup>
<Build Include="MyExternalStreamingJob.sql" Type="ExternalStreamingJob" />
</ItemGroup>
<ItemGroup>
<SqlCmdVariable Include="ProdDatabaseName">
<DefaultValue>MyProdDatabase</DefaultValue>
<Value>$(SqlCmdVar__1)</Value>
</SqlCmdVariable>
<SqlCmdVariable Include="BackupDatabaseName">
<DefaultValue>MyBackupDatabase</DefaultValue>
<Value>$(SqlCmdVar__2)</Value>
</SqlCmdVariable>
</ItemGroup>
<ItemGroup>
<ArtifactReference Condition="'$(NetCoreBuild)' == 'true'" Include="$(NETCoreTargetsPath)\SystemDacpacs\150\master.dacpac">
<SuppressMissingDependenciesErrors>False</SuppressMissingDependenciesErrors>
<DatabaseVariableLiteralValue>master</DatabaseVariableLiteralValue>
</ArtifactReference>
<ArtifactReference Condition="'$(NetCoreBuild)' != 'true'" Include="$(DacPacRootPath)\Extensions\Microsoft\SQLDB\Extensions\SqlServer\150\SqlSchemas\master.dacpac">
<SuppressMissingDependenciesErrors>False</SuppressMissingDependenciesErrors>
<DatabaseVariableLiteralValue>master</DatabaseVariableLiteralValue>
</ArtifactReference>
</ItemGroup>
<ItemGroup>
<PreDeploy Include="Script.PreDeployment1.sql" />
<None Include="Script.PreDeployment2.sql" />
</ItemGroup>
<ItemGroup>
<PostDeploy Include="Script.PostDeployment1.sql" />
<None Include="Tables\Script.PostDeployment1.sql" />
</ItemGroup>
<ItemGroup>
<None Include="TestProjectName_1.publish.xml" />
<None Include="TestProjectName_2.publish.xml" />
<None Include="TestProjectName_3.publish.xml" />
<None Include="TestProjectName_4.publish.xml" />
</ItemGroup>
<Target Name="BeforeBuild">
<Delete Files="$(BaseIntermediateOutputPath)\project.assets.json" />
</Target>
</Project>

View File

@@ -109,6 +109,11 @@
<PostDeploy Include="Script.PostDeployment1.sql" />
<None Include="Tables\Script.PostDeployment1.sql" />
</ItemGroup>
<ItemGroup>
<None Include="TestProjectName_1.publish.xml" />
<None Include="TestProjectName_2.publish.xml" />
<None Include="TestProjectName_3.publish.xml" />
</ItemGroup>
<Target Name="BeforeBuild">
<Delete Files="$(BaseIntermediateOutputPath)\project.assets.json" />
</Target>

View File

@@ -66,6 +66,12 @@ describe('Project: sqlproj content operations', function (): void {
should(project.postDeployScripts.find(f => f.type === EntryType.File && f.relativePath === 'Script.PostDeployment1.sql')).not.equal(undefined, 'File Script.PostDeployment1.sql not read');
should(project.noneDeployScripts.find(f => f.type === EntryType.File && f.relativePath === 'Script.PreDeployment2.sql')).not.equal(undefined, 'File Script.PostDeployment2.sql not read');
should(project.noneDeployScripts.find(f => f.type === EntryType.File && f.relativePath === 'Tables\\Script.PostDeployment1.sql')).not.equal(undefined, 'File Tables\\Script.PostDeployment1.sql not read');
// Publish profiles
should(project.publishProfiles.length).equal(3);
should(project.publishProfiles.find(f => f.type === EntryType.File && f.relativePath === 'TestProjectName_1.publish.xml')).not.equal(undefined, 'Profile TestProjectName_1.publish.xml not read');
should(project.publishProfiles.find(f => f.type === EntryType.File && f.relativePath === 'TestProjectName_2.publish.xml')).not.equal(undefined, 'Profile TestProjectName_2.publish.xml not read');
should(project.publishProfiles.find(f => f.type === EntryType.File && f.relativePath === 'TestProjectName_3.publish.xml')).not.equal(undefined, 'Profile TestProjectName_3.publish.xml not read');
});
it('Should read Project with Project reference from sqlproj', async function (): Promise<void> {
@@ -1592,6 +1598,30 @@ describe('Project: add SQLCMD Variables', function (): void {
});
});
describe('Project: add publish profiles', function (): void {
before(async function (): Promise<void> {
await baselines.loadBaselines();
});
after(async function (): Promise<void> {
await testUtils.deleteGeneratedTestFolder();
});
it('Should update .sqlproj with new publish profiles', async function (): Promise<void> {
projFilePath = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline);
const project = await Project.openProject(projFilePath);
should(Object.keys(project.publishProfiles).length).equal(3);
// add a new publish profile
await project.addPublishProfileToProjFile(path.join(projFilePath, 'TestProjectName_4.publish.xml'));
should(Object.keys(project.publishProfiles).length).equal(4);
const projFileText = (await fs.readFile(projFilePath)).toString();
should(projFileText).equal(baselines.openSqlProjectWithAdditionalPublishProfileBaseline.trim());
});
});
describe('Project: properties', function (): void {
before(async function (): Promise<void> {
await baselines.loadBaselines();