Unverified Commit 22c80777 authored by stuartmorgan's avatar stuartmorgan Committed by GitHub

Automatically add plugin projects to Windows .sln (#51246)

Adds utility code for managing list of plugin projects within a solution file, updating them as the plugins change.

This is a prototype of an approach to solution-level portion of Windows plugin tooling; it may not be what the final plugin handling on Windows uses, but it makes things much better in the short term, and gives us a baseline to evaluate other possible solution management systems against.

Part of #32719
parent c71978f6
......@@ -20,6 +20,7 @@ import 'macos/cocoapods.dart';
import 'platform_plugins.dart';
import 'project.dart';
import 'windows/property_sheet.dart';
import 'windows/visual_studio_solution_utils.dart';
void _renderTemplateToFile(String template, dynamic context, String filePath) {
final String renderedTemplate =
......@@ -1038,6 +1039,7 @@ Future<void> injectPlugins(FlutterProject project, {bool checkProjects = false})
}
if (featureFlags.isWindowsEnabled && project.windows.existsSync()) {
await _writeWindowsPluginFiles(project, plugins);
await VisualStudioSolutionUtils(project: project.windows, fileSystem: globals.fs).updatePlugins(plugins);
}
for (final XcodeBasedProject subproject in <XcodeBasedProject>[project.ios, project.macos]) {
if (!project.isModule && (!checkProjects || subproject.existsSync())) {
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:meta/meta.dart';
import 'package:xml/xml.dart' as xml;
import '../base/file_system.dart';
/// A utility class for interacting with Visual Studio project files (e.g.,
/// .vcxproj).
class VisualStudioProject {
/// Creates a project object from the project file at [file].
VisualStudioProject(this.file, {
@required FileSystem fileSystem,
}): _fileSystem = fileSystem {
try {
content = xml.parse(file.readAsStringSync());
} on xml.XmlParserException {
// Silently continue; formatUnderstood will return false.
}
}
final FileSystem _fileSystem;
/// The file corresponding to this object.
final File file;
/// The content of the project file.
xml.XmlDocument content;
/// Whether or not the the project file was correctly parsed.
///
/// If false, this could indicate that the project file is damaged, or that
/// it's an unsupported project type.
bool get formatUnderstood => content != null;
String _guid;
/// Returns the ProjectGuid for the project, or null if it's not present.
String get guid {
return _guid ??= _findGuid();
}
String _findGuid() {
if (!formatUnderstood) {
return null;
}
try {
final String guidValue = content.findAllElements('ProjectGuid').single.text.trim();
// Remove the enclosing {} from the value.
return guidValue.substring(1, guidValue.length - 1);
} on StateError {
// If there is not exactly one ProjectGuid, return null.
return null;
}
}
String _name;
/// Returns the ProjectName for the project.
///
/// If not explicitly set in the project, uses the basename of the project
/// file.
String get name {
return _name ??= _findName();
}
String _findName() {
if (!formatUnderstood) {
return null;
}
try {
return content.findAllElements('ProjectName').first.text.trim();
} on StateError {
// If there is no name, fall back to filename.
return _fileSystem.path.basenameWithoutExtension(file.path);
}
}
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:meta/meta.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../convert.dart';
import '../plugins.dart';
import '../project.dart';
import 'visual_studio_project.dart';
// Constants corresponding to specific reference types in a solution file.
// These values are defined by the .sln format.
const String _kSolutionTypeGuidFolder = '2150E333-8FDC-42A3-9474-1A3956D46DE8';
const String _kSolutionTypeGuidVcxproj = '8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942';
// The GUID for the folder above, managed by this class. This is an arbitrary
// value that was randomly generated, but it should not be changed since that
// would cause issues for existing Flutter projects.
const String _kFlutterPluginSolutionFolderGuid = '5C2E738A-1DD3-445A-AAC8-EEB9648DD07C';
// The FlutterBuild project GUID. This is an arbitrary
// value that was randomly generated, but it should not be changed since that
// would cause issues for existing Flutter projects.
const String _kFlutterBuildProjectGuid = '6419BF13-6ECD-4CD2-9E85-E566A1F03F8F';
/// Extracts and stores the plugin name and vcxproj GUID for [plugin].
class _PluginProjectInfo {
_PluginProjectInfo(Plugin plugin, {
@required FileSystem fileSystem,
}) {
name = plugin.name;
final File projectFile = fileSystem.directory(plugin.path).childDirectory('windows').childFile('plugin.vcxproj');
guid = VisualStudioProject(projectFile, fileSystem: fileSystem).guid;
if (guid == null) {
throwToolExit('Unable to find a plugin.vcxproj for plugin "$name"');
}
}
// The name of the plugin, which is also the name of the symlink folder.
String name;
// The GUID of the plugin's project.
String guid;
}
// TODO(stuartmorgan): Consider replacing this class with a real parser. See
// https://github.com/flutter/flutter/issues/51430.
class VisualStudioSolutionUtils {
const VisualStudioSolutionUtils({
@required WindowsProject project,
@required FileSystem fileSystem,
}) : _project = project,
_fileSystem = fileSystem;
final WindowsProject _project;
final FileSystem _fileSystem;
/// Updates the solution file for [project] to have the project references and
/// dependencies to include [plugins], removing any previous plugins from the
/// solution.
Future<void> updatePlugins(List<Plugin> plugins) async {
final String solutionContent = await _project.solutionFile.readAsString();
// Map of GUID to name for the current plugin list.
final Map<String, String> currentPluginInfo = _getWindowsPluginNamesByGuid(plugins);
// Find any plugins referenced in the project that are no longer used, and
// any that are new.
//
// While the simplest approach to updating the solution would be to remove all
// entries associated with plugins, and then add all the current plugins in
// one block, Visual Studio has its own (unknown, likely data-structure-hash
// based) order that it will use each time it writes out the file due to any
// solution-level changes made in the UI. To avoid thrashing, and resulting
// confusion (e.g., in review diffs), this update attempts to instead preserve
// the ordering that is already there, so that once Visual Studio has
// reordered the plugins, that order will be stable.
final Set<String> existingPlugins = _findPreviousPluginGuids(solutionContent);
final Set<String> currentPlugins = currentPluginInfo.keys.toSet();
final Set<String> removedPlugins = existingPlugins.difference(currentPlugins);
final Set<String> addedPlugins = currentPlugins.difference(existingPlugins);
final RegExp projectStartPattern = RegExp(r'^Project\("{' + _kSolutionTypeGuidVcxproj + r'}"\)\s*=\s*".*",\s*"(.*)",\s*"{([A-Fa-f0-9\-]*)}"\s*$');
final RegExp pluginsFolderProjectStartPattern = RegExp(r'^Project\("{' + _kSolutionTypeGuidFolder + r'}"\)\s*=.*"{' + _kFlutterPluginSolutionFolderGuid + r'}"\s*$');
final RegExp projectEndPattern = RegExp(r'^EndProject\s*$');
final RegExp globalStartPattern = RegExp(r'^Global\s*$');
final RegExp globalEndPattern = RegExp(r'^EndGlobal\s*$');
final RegExp projectDependenciesStartPattern = RegExp(r'^\s*ProjectSection\(ProjectDependencies\)\s*=\s*postProject\s*$');
final RegExp globalSectionProjectConfigurationStartPattern = RegExp(r'^\s*GlobalSection\(ProjectConfigurationPlatforms\)\s*=\s*postSolution\s*$');
final RegExp globalSectionNestedProjectsStartPattern = RegExp(r'^\s*GlobalSection\(NestedProjects\)\s*=\s*preSolution\s*$');
final StringBuffer newSolutionContent = StringBuffer();
// readAsString drops the BOM; re-add it.
newSolutionContent.writeCharCode(unicodeBomCharacterRune);
final Iterator<String> lineIterator = solutionContent.split('\n').iterator;
bool foundFlutterPluginsFolder = false;
bool foundNestedProjectsSection = false;
bool foundRunnerProject = false;
while (lineIterator.moveNext()) {
final Match projectStartMatch = projectStartPattern.firstMatch(lineIterator.current);
if (projectStartMatch != null) {
final String guid = projectStartMatch.group(2);
if (currentPlugins.contains(guid)) {
// Write an up-to-date version at this location (in case, e.g., the name
// has changed).
_writePluginProjectEntry(guid, currentPluginInfo[guid], newSolutionContent);
// Drop the old copy.
_skipUntil(lineIterator, projectEndPattern);
continue;
} else if (removedPlugins.contains(guid)) {
// Drop the stale plugin project.
_skipUntil(lineIterator, projectEndPattern);
continue;
} else if (projectStartMatch.group(1) == _project.vcprojFile.basename) {
foundRunnerProject = true;
// Update the Runner project's dependencies on the plugins.
// Skip to the dependencies section, or if there isn't one the end of
// the project.
while (!projectDependenciesStartPattern.hasMatch(lineIterator.current) &&
!projectEndPattern.hasMatch(lineIterator.current)) {
newSolutionContent.writeln(lineIterator.current);
lineIterator.moveNext();
}
// Add/update the dependencies section.
if (projectDependenciesStartPattern.hasMatch(lineIterator.current)) {
newSolutionContent.writeln(lineIterator.current);
_processSectionPluginReferences(removedPlugins, addedPlugins, lineIterator, _writeProjectDependency, newSolutionContent);
} else {
_writeDependenciesSection(currentPlugins, newSolutionContent);
}
}
}
if (pluginsFolderProjectStartPattern.hasMatch(lineIterator.current)) {
foundFlutterPluginsFolder = true;
}
if (globalStartPattern.hasMatch(lineIterator.current)) {
// The Global section is the end of the project list. Add any new plugins
// here, since the location VS will use is unknown. They will likely be
// reordered the next time VS writes the file.
for (final String guid in addedPlugins) {
_writePluginProjectEntry(guid, currentPluginInfo[guid], newSolutionContent);
}
// Also add the plugins folder if there wasn't already one.
if (!foundFlutterPluginsFolder) {
_writePluginFolderProjectEntry(newSolutionContent);
}
}
// Update the ProjectConfiguration section once it is reached.
if (globalSectionProjectConfigurationStartPattern.hasMatch(lineIterator.current)) {
newSolutionContent.writeln(lineIterator.current);
_processSectionPluginReferences(removedPlugins, addedPlugins, lineIterator, _writePluginConfigurationEntries, newSolutionContent);
}
// Update the NestedProjects section once it is reached.
if (globalSectionNestedProjectsStartPattern.hasMatch(lineIterator.current)) {
newSolutionContent.writeln(lineIterator.current);
_processSectionPluginReferences(removedPlugins, addedPlugins, lineIterator, _writePluginNestingEntry, newSolutionContent);
foundNestedProjectsSection = true;
}
// If there wasn't a NestedProjects global section, add one at the end.
if (!foundNestedProjectsSection && globalEndPattern.hasMatch(lineIterator.current)) {
newSolutionContent.writeln('\tGlobalSection(NestedProjects) = preSolution\r');
for (final String guid in currentPlugins) {
_writePluginNestingEntry(guid, newSolutionContent);
}
newSolutionContent.writeln('\tEndGlobalSection\r');
}
// Re-output anything that hasn't been explicitly skipped above.
newSolutionContent.writeln(lineIterator.current);
}
if (!foundRunnerProject) {
throwToolExit(
'Could not add plugins to Windows project:\n'
'Unable to find a "${_project.vcprojFile.basename}" project in ${_project.solutionFile.path}');
}
await _project.solutionFile.writeAsString(newSolutionContent.toString().trimRight());
}
/// Advances [iterator] it reaches an element that matches [pattern].
///
/// Note that the current element at the time of calling is *not* checked.
void _skipUntil(Iterator<String> iterator, RegExp pattern) {
while (iterator.moveNext()) {
if (pattern.hasMatch(iterator.current)) {
return;
}
}
}
/// Writes the main project entry for the plugin with the given [guid] and
/// [name].
void _writePluginProjectEntry(String guid, String name, StringBuffer output) {
output.write('''
Project("{$_kSolutionTypeGuidVcxproj}") = "$name", "Flutter\\ephemeral\\.plugin_symlinks\\$name\\windows\\plugin.vcxproj", "{$guid}"\r
\tProjectSection(ProjectDependencies) = postProject\r
\t\t{$_kFlutterBuildProjectGuid} = {$_kFlutterBuildProjectGuid}\r
\tEndProjectSection\r
EndProject\r
''');
}
/// Writes the main project entry for the Flutter Plugins solution folder.
void _writePluginFolderProjectEntry(StringBuffer output) {
const String folderName = 'Flutter Plugins';
output.write('''
Project("{$_kSolutionTypeGuidFolder}") = "$folderName", "$folderName", "{$_kFlutterPluginSolutionFolderGuid}"\r
EndProject\r
''');
}
/// Writes a project dependencies section, depending on all the GUIDs in
/// [dependencies].
void _writeDependenciesSection(Iterable<String> dependencies, StringBuffer output) {
output.writeln('ProjectSection(ProjectDependencies) = postProject\r');
for (final String guid in dependencies) {
_writeProjectDependency(guid, output);
}
output.writeln('EndProjectSection\r');
}
/// Returns the GUIDs of all the Flutter plugin projects in the given solution.
Set<String> _findPreviousPluginGuids(String solutionContent) {
// Find the plugin folder's known GUID in ProjectDependencies lines.
// Each line in that section has the form:
// {project GUID} = {solution folder GUID}
final RegExp pluginFolderChildrenPattern = RegExp(
r'^\s*{([A-Fa-f0-9\-]*)}\s*=\s*{' + _kFlutterPluginSolutionFolderGuid + r'}\s*$',
multiLine: true,
);
return pluginFolderChildrenPattern
.allMatches(solutionContent)
.map((Match match) => match.group(1)).toSet();
}
/// Returns a mapping of plugin project GUID to name for all the Windows plugins
/// in [plugins].
Map<String, String> _getWindowsPluginNamesByGuid(List<Plugin> plugins) {
final Map<String, String> currentPluginInfo = <String, String>{};
for (final Plugin plugin in plugins) {
if (plugin.platforms.containsKey(_project.pluginConfigKey)) {
final _PluginProjectInfo info = _PluginProjectInfo(plugin, fileSystem: _fileSystem);
if (currentPluginInfo.containsKey(info.guid)) {
throwToolExit('The plugins "${currentPluginInfo[info.guid]}" and "${info.name}" '
'have the same ProjectGuid, which prevents them from being used together.\n\n'
'Please contact the plugin authors to resolve this, and/or remove one of the '
'plugins from your project.');
}
currentPluginInfo[info.guid] = info.name;
}
}
return currentPluginInfo;
}
/// Walks a GlobalSection or ProjectSection, removing entries related to removed
/// plugins and adding entries for new plugins at the end using
/// [newEntryWriter], which takes the guid of the plugin to write entries for.
///
/// The caller is responsible for printing the section start line, which should
/// be [lineIterator.current] when this is called, and the section end line,
/// which will be [lineIterator.current] on return.
void _processSectionPluginReferences(
Set<String> removedPlugins,
Set<String> addedPlugins,
Iterator<String> lineIterator,
Function(String, StringBuffer) newEntryWriter,
StringBuffer output,
) {
// Extracts the guid of the project that a line refers to. Currently all
// sections this function is used for start with "{project guid}", even though
// the rest of the line varies by section, so the pattern can currently be
// shared rather than parameterized.
final RegExp entryPattern = RegExp(r'^\s*{([A-Fa-f0-9\-]*)}');
final RegExp sectionEndPattern = RegExp(r'^\s*End\w*Section\s*$');
while (lineIterator.moveNext()) {
if (sectionEndPattern.hasMatch(lineIterator.current)) {
// The end of the section; add entries for new plugins, then exit.
for (final String guid in addedPlugins) {
newEntryWriter(guid, output);
}
return;
}
// Otherwise it's the sectino body. Drop any lines associated with old
// plugins, but pass everything else through as output.
final Match entryMatch = entryPattern.firstMatch(lineIterator.current);
if (entryMatch != null && removedPlugins.contains(entryMatch.group(1))) {
continue;
}
output.writeln(lineIterator.current);
}
}
/// Writes all the configuration entries for the plugin project with the given
/// [guid].
///
/// Should be called within the context of writing
/// GlobalSection(ProjectConfigurationPlatforms).
void _writePluginConfigurationEntries(String guid, StringBuffer output) {
final List<String> configurations = <String>['Debug', 'Profile', 'Release'];
final List<String> entryTypes = <String>['ActiveCfg', 'Build.0'];
for (final String configuration in configurations) {
for (final String entryType in entryTypes) {
output.writeln('\t\t{$guid}.$configuration|x64.$entryType = $configuration|x64\r');
}
}
}
/// Writes the entries to nest the plugin projects with the given [guid] under
/// the Flutter Plugins solution folder.
///
/// Should be called within the context of writing
/// GlobalSection(NestedProjects).
void _writePluginNestingEntry(String guid, StringBuffer output) {
output.writeln('\t\t{$guid} = {$_kFlutterPluginSolutionFolderGuid}\r');
}
/// Writes the entrie to make a project depend on another project with the
/// given [guid].
///
/// Should be called within the context of writing
/// ProjectSection(ProjectDependencies).
void _writeProjectDependency(String guid, StringBuffer output) {
output.writeln('\t\t{$guid} = {$guid}\r');
}
}
......@@ -76,6 +76,7 @@ void main() {
final Directory windowsManagedDirectory = flutterProject.directory.childDirectory('windows').childDirectory('flutter');
when(windowsProject.managedDirectory).thenReturn(windowsManagedDirectory);
when(windowsProject.vcprojFile).thenReturn(windowsManagedDirectory.parent.childFile('Runner.vcxproj'));
when(windowsProject.solutionFile).thenReturn(windowsManagedDirectory.parent.childFile('Runner.sln'));
when(windowsProject.pluginSymlinkDirectory).thenReturn(windowsManagedDirectory.childDirectory('ephemeral').childDirectory('.plugin_symlinks'));
when(windowsProject.generatedPluginPropertySheetFile).thenReturn(windowsManagedDirectory.childFile('GeneratedPlugins.props'));
when(windowsProject.existsSync()).thenReturn(false);
......@@ -309,6 +310,44 @@ dependencies:
project.podManifestLock.createSync(recursive: true);
}
// Creates a Windows solution file sufficient to allow plugin injection
// to run without failing.
void createDummyWindowsSolutionFile() {
windowsProject.solutionFile.createSync(recursive: true);
// This isn't a valid solution file, but it's just enough to work with the
// plugin injection.
windowsProject.solutionFile.writeAsStringSync('''
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Runner", "Runner.vcxproj", "{3842E94C-E348-463A-ADBE-625A2B69B628}"
ProjectSection(ProjectDependencies) = postProject
{6419BF13-6ECD-4CD2-9E85-E566A1F03F8F} = {6419BF13-6ECD-4CD2-9E85-E566A1F03F8F}
EndProjectSection
EndProject
Global
GlobalSection(ProjectConfigurationPlatforms) = postSolution
EndGlobalSection
EndGlobal''');
}
// Creates a Windows project file for dummyPackageDirectory sufficient to
// allow plugin injection to run without failing.
void createDummyPluginWindowsProjectFile() {
final File projectFile = dummyPackageDirectory
.parent
.childDirectory('windows')
.childFile('plugin.vcxproj');
projectFile.createSync(recursive: true);
// This isn't a valid project file, but it's just enough to work with the
// plugin injection.
projectFile.writeAsStringSync('''
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>{5919689F-A5D5-462C-AF50-D405CCEF89B8}</ProjectGuid>'}
<ProjectName>apackage</ProjectName>
</PropertyGroup>
</Project>''');
}
group('refreshPlugins', () {
testUsingContext('Refreshing the plugin list is a no-op when the plugins list stays empty', () {
refreshPluginsList(flutterProject);
......@@ -837,6 +876,8 @@ web_plugin_with_nested:${webPluginWithNestedFile.childDirectory('lib').uri.toStr
when(featureFlags.isWindowsEnabled).thenReturn(true);
when(flutterProject.isModule).thenReturn(false);
configureDummyPackageAsPlugin();
createDummyWindowsSolutionFile();
createDummyPluginWindowsProjectFile();
await injectPlugins(flutterProject, checkProjects: true);
......@@ -857,6 +898,8 @@ web_plugin_with_nested:${webPluginWithNestedFile.childDirectory('lib').uri.toStr
when(featureFlags.isWindowsEnabled).thenReturn(true);
when(flutterProject.isModule).thenReturn(false);
configureDummyPackageAsPlugin();
createDummyWindowsSolutionFile();
createDummyPluginWindowsProjectFile();
await injectPlugins(flutterProject, checkProjects: true);
......@@ -871,6 +914,23 @@ web_plugin_with_nested:${webPluginWithNestedFile.childDirectory('lib').uri.toStr
ProcessManager: () => FakeProcessManager.any(),
FeatureFlags: () => featureFlags,
});
testUsingContext('Injecting updates Windows solution file', () async {
when(windowsProject.existsSync()).thenReturn(true);
when(featureFlags.isWindowsEnabled).thenReturn(true);
when(flutterProject.isModule).thenReturn(false);
configureDummyPackageAsPlugin();
createDummyWindowsSolutionFile();
createDummyPluginWindowsProjectFile();
await injectPlugins(flutterProject, checkProjects: true);
expect(windowsProject.solutionFile.readAsStringSync(), contains(r'apackage\windows\plugin.vcxproj'));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
FeatureFlags: () => featureFlags,
});
});
group('createPluginSymlinks', () {
......
......@@ -225,6 +225,20 @@ void main() {
testUsingContext('injects plugins for Windows', () async {
final FlutterProject project = await someProject();
project.windows.managedDirectory.createSync(recursive: true);
project.windows.solutionFile.createSync(recursive: true);
// Just enough solution file to allow injection to pass.
// TODO(stuartmorgan): Consider allowing injecting a mock solution util
// class into the test environment instead.
project.windows.solutionFile.writeAsStringSync('''
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Runner", "Runner.vcxproj", "{3842E94C-E348-463A-ADBE-625A2B69B628}"
ProjectSection(ProjectDependencies) = postProject
{6419BF13-6ECD-4CD2-9E85-E566A1F03F8F} = {6419BF13-6ECD-4CD2-9E85-E566A1F03F8F}
EndProjectSection
EndProject
Global
GlobalSection(ProjectConfigurationPlatforms) = postSolution
EndGlobalSection
EndGlobal''');
await project.ensureReadyForPlatformSpecificTooling();
expectExists(project.windows.managedDirectory.childFile('generated_plugin_registrant.h'));
expectExists(project.windows.managedDirectory.childFile('generated_plugin_registrant.cc'));
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:file/memory.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/windows/visual_studio_project.dart';
import '../../src/common.dart';
void main() {
group('Visual Studio Project', () {
String generateProjectContents({String guid, String name}) {
// A bare-bones project.
return '''
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>15.0</VCProjectVersion>
${guid == null ? '' : '<ProjectGuid>{$guid}</ProjectGuid>'}
${name == null ? '' : '<ProjectName>$name</ProjectName>'}
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="\$(VCTargetsPath)\\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<PlatformToolset>v142</PlatformToolset>
</PropertyGroup>
<Import Project="\$(VCTargetsPath)\\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" />
<PropertyGroup Label="UserMacros" />
<PropertyGroup />
<ItemDefinitionGroup />
<ItemGroup>
</ItemGroup>
<Import Project="\$(VCTargetsPath)\\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>''';
}
test('Property extraction works on a simple vcxproj', () async {
final FileSystem fileSystem = MemoryFileSystem();
const String guid = '017C4BAC-FEBA-406D-8A2C-3099FFE9D811';
const String name = 'Test';
final File projectFile = fileSystem.file('aproject.vcxproj');
projectFile.writeAsStringSync(generateProjectContents(guid: guid, name: name));
final VisualStudioProject project = VisualStudioProject(projectFile, fileSystem: fileSystem);
expect(project.formatUnderstood, true);
expect(project.guid, guid);
expect(project.name, name);
});
test('Missing GUID returns null', () async {
final FileSystem fileSystem = MemoryFileSystem();
final File projectFile = fileSystem.file('aproject.vcxproj');
projectFile.writeAsStringSync(generateProjectContents());
final VisualStudioProject project = VisualStudioProject(projectFile, fileSystem: fileSystem);
expect(project.formatUnderstood, true);
expect(project.guid, null);
});
test('Missing project name uses filename', () async {
final FileSystem fileSystem = MemoryFileSystem();
final File projectFile = fileSystem.file('aproject.vcxproj');
projectFile.writeAsStringSync(generateProjectContents());
final VisualStudioProject project = VisualStudioProject(projectFile, fileSystem: fileSystem);
expect(project.formatUnderstood, true);
expect(project.name, 'aproject');
});
test('Unknown file contents creates an object, and return false for formatUnderstood', () async {
final FileSystem fileSystem = MemoryFileSystem();
final File projectFile = fileSystem.file('aproject.vcxproj');
projectFile.writeAsStringSync('This is not XML!');
final VisualStudioProject project = VisualStudioProject(projectFile, fileSystem: fileSystem);
expect(project.formatUnderstood, false);
});
test('Missing project file throws on creation', () async {
final FileSystem fileSystem = MemoryFileSystem();
final File projectFile = fileSystem.file('aproject.vcxproj');
expect(() => VisualStudioProject(projectFile, fileSystem: fileSystem), throwsFileSystemException());
});
});
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/plugins.dart';
import 'package:flutter_tools/src/platform_plugins.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/windows/visual_studio_solution_utils.dart';
import 'package:mockito/mockito.dart';
import '../../src/common.dart';
void main() {
group('Visual Studio Solution Utils', () {
// Magic values; see visual_studio_solution_utils.dart.
const String solutionTypeGuidFolder = '2150E333-8FDC-42A3-9474-1A3956D46DE8';
const String solutionTypeGuidVcxproj = '8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942';
const String flutterPluginSolutionFolderGuid = '5C2E738A-1DD3-445A-AAC8-EEB9648DD07C';
// Arbitrary random GUIDs to use for fake plugins.
const String pluginAGuid = '9F200BC4-A747-43D1-8B72-B778F2C4D048';
const String pluginBGuid = '39AC79B8-28A6-4526-B5FF-9C83F59B3AF0';
const String pluginCGuid = '8E010714-28FF-416A-BC6F-9CDE336A02A7';
const String pluginDGuid = '304F1860-7E8B-4C99-8E1D-F5E55259F5C3';
FileSystem fs;
MockWindowsProject project;
setUp(() async {
fs = MemoryFileSystem(style: FileSystemStyle.windows);
project = MockWindowsProject();
when(project.pluginConfigKey).thenReturn('windows');
final Directory windowsManagedDirectory = fs.directory('C:').childDirectory('windows').childDirectory('flutter');
when(project.solutionFile).thenReturn(windowsManagedDirectory.parent.childFile('Runner.sln'));
when(project.vcprojFile).thenReturn(windowsManagedDirectory.parent.childFile('Runner.vcxproj'));
when(project.pluginSymlinkDirectory).thenReturn(windowsManagedDirectory.childDirectory('ephemeral').childDirectory('.plugin_symlinks'));
});
// Returns a solution file contents for a solution without any plugins.
void writeSolutionWithoutPlugins() {
project.solutionFile.createSync(recursive: true);
project.solutionFile.writeAsStringSync('''
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29709.97
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Runner", "Runner.vcxproj", "{3842E94C-E348-463A-ADBE-625A2B69B628}"
ProjectSection(ProjectDependencies) = postProject
{6419BF13-6ECD-4CD2-9E85-E566A1F03F8F} = {6419BF13-6ECD-4CD2-9E85-E566A1F03F8F}
EndProjectSection
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Flutter Build", "FlutterBuild.vcxproj", "{6419BF13-6ECD-4CD2-9E85-E566A1F03F8F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Profile|x64 = Profile|x64
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{3842E94C-E348-463A-ADBE-625A2B69B628}.Debug|x64.ActiveCfg = Debug|x64
{3842E94C-E348-463A-ADBE-625A2B69B628}.Debug|x64.Build.0 = Debug|x64
{3842E94C-E348-463A-ADBE-625A2B69B628}.Profile|x64.ActiveCfg = Profile|x64
{3842E94C-E348-463A-ADBE-625A2B69B628}.Profile|x64.Build.0 = Profile|x64
{3842E94C-E348-463A-ADBE-625A2B69B628}.Release|x64.ActiveCfg = Release|x64
{3842E94C-E348-463A-ADBE-625A2B69B628}.Release|x64.Build.0 = Release|x64
{6419BF13-6ECD-4CD2-9E85-E566A1F03F8F}.Debug|x64.ActiveCfg = Debug|x64
{6419BF13-6ECD-4CD2-9E85-E566A1F03F8F}.Debug|x64.Build.0 = Debug|x64
{6419BF13-6ECD-4CD2-9E85-E566A1F03F8F}.Profile|x64.ActiveCfg = Profile|x64
{6419BF13-6ECD-4CD2-9E85-E566A1F03F8F}.Profile|x64.Build.0 = Profile|x64
{6419BF13-6ECD-4CD2-9E85-E566A1F03F8F}.Release|x64.ActiveCfg = Release|x64
{6419BF13-6ECD-4CD2-9E85-E566A1F03F8F}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B8A69CB0-A974-4774-9EBD-1E5EECACD186}
EndGlobalSection
EndGlobal''');
}
// Returns a solution file contents for a solution with plugins A, B, and C
// already added.
void writeSolutionWithPlugins() {
const String pluginSymlinkSubdirPath = r'Flutter\ephemeral\.plugin_symlinks';
const String pluginProjectSubpath = r'windows\plugin.vcxproj';
project.solutionFile.createSync(recursive: true);
project.solutionFile.writeAsStringSync('''
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29709.97
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Runner", "Runner.vcxproj", "{5A827760-CF8B-408A-99A3-B6C0AD2271E7}"
ProjectSection(ProjectDependencies) = postProject
{$pluginAGuid} = {$pluginAGuid}
{6419BF13-6ECD-4CD2-9E85-E566A1F03F8F} = {6419BF13-6ECD-4CD2-9E85-E566A1F03F8F}
{$pluginBGuid} = {$pluginBGuid}
{$pluginCGuid} = {$pluginCGuid}
EndProjectSection
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "plugin_a", "$pluginSymlinkSubdirPath\\plugin_a\\$pluginProjectSubpath", "{$pluginAGuid}"
ProjectSection(ProjectDependencies) = postProject
{6419BF13-6ECD-4CD2-9E85-E566A1F03F8F} = {6419BF13-6ECD-4CD2-9E85-E566A1F03F8F}
EndProjectSection
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Flutter Build", "FlutterBuild.vcxproj", "{6419BF13-6ECD-4CD2-9E85-E566A1F03F8F}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "plugin_b", "$pluginSymlinkSubdirPath\\plugin_b\\$pluginProjectSubpath", "{$pluginBGuid}"
ProjectSection(ProjectDependencies) = postProject
{6419BF13-6ECD-4CD2-9E85-E566A1F03F8F} = {6419BF13-6ECD-4CD2-9E85-E566A1F03F8F}
EndProjectSection
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "plugin_c", "$pluginSymlinkSubdirPath\\plugin_c\\$pluginProjectSubpath", "{$pluginCGuid}"
ProjectSection(ProjectDependencies) = postProject
{6419BF13-6ECD-4CD2-9E85-E566A1F03F8F} = {6419BF13-6ECD-4CD2-9E85-E566A1F03F8F}
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Flutter Plugins", "Flutter Plugins", "{5C2E738A-1DD3-445A-AAC8-EEB9648DD07C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Profile|x64 = Profile|x64
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{5A827760-CF8B-408A-99A3-B6C0AD2271E7}.Debug|x64.ActiveCfg = Debug|x64
{5A827760-CF8B-408A-99A3-B6C0AD2271E7}.Debug|x64.Build.0 = Debug|x64
{5A827760-CF8B-408A-99A3-B6C0AD2271E7}.Profile|x64.ActiveCfg = Profile|x64
{5A827760-CF8B-408A-99A3-B6C0AD2271E7}.Profile|x64.Build.0 = Profile|x64
{5A827760-CF8B-408A-99A3-B6C0AD2271E7}.Release|x64.ActiveCfg = Release|x64
{5A827760-CF8B-408A-99A3-B6C0AD2271E7}.Release|x64.Build.0 = Release|x64
{$pluginAGuid}.Debug|x64.ActiveCfg = Debug|x64
{$pluginAGuid}.Debug|x64.Build.0 = Debug|x64
{$pluginAGuid}.Profile|x64.ActiveCfg = Profile|x64
{$pluginAGuid}.Profile|x64.Build.0 = Profile|x64
{$pluginAGuid}.Release|x64.ActiveCfg = Release|x64
{$pluginAGuid}.Release|x64.Build.0 = Release|x64
{6419BF13-6ECD-4CD2-9E85-E566A1F03F8F}.Debug|x64.ActiveCfg = Debug|x64
{6419BF13-6ECD-4CD2-9E85-E566A1F03F8F}.Debug|x64.Build.0 = Debug|x64
{6419BF13-6ECD-4CD2-9E85-E566A1F03F8F}.Profile|x64.ActiveCfg = Profile|x64
{6419BF13-6ECD-4CD2-9E85-E566A1F03F8F}.Profile|x64.Build.0 = Profile|x64
{6419BF13-6ECD-4CD2-9E85-E566A1F03F8F}.Release|x64.ActiveCfg = Release|x64
{6419BF13-6ECD-4CD2-9E85-E566A1F03F8F}.Release|x64.Build.0 = Release|x64
{$pluginBGuid}.Debug|x64.ActiveCfg = Debug|x64
{$pluginBGuid}.Debug|x64.Build.0 = Debug|x64
{$pluginBGuid}.Profile|x64.ActiveCfg = Profile|x64
{$pluginBGuid}.Profile|x64.Build.0 = Profile|x64
{$pluginBGuid}.Release|x64.ActiveCfg = Release|x64
{$pluginBGuid}.Release|x64.Build.0 = Release|x64
{$pluginCGuid}.Debug|x64.ActiveCfg = Debug|x64
{$pluginCGuid}.Debug|x64.Build.0 = Debug|x64
{$pluginCGuid}.Profile|x64.ActiveCfg = Profile|x64
{$pluginCGuid}.Profile|x64.Build.0 = Profile|x64
{$pluginCGuid}.Release|x64.ActiveCfg = Release|x64
{$pluginCGuid}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{$pluginCGuid} = {$flutterPluginSolutionFolderGuid}
{$pluginBGuid} = {$flutterPluginSolutionFolderGuid}
{$pluginAGuid} = {$flutterPluginSolutionFolderGuid}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6C8A8041-10D8-4BEB-B73D-C02BCE62120B}
EndGlobalSection
EndGlobal''');
}
void writeDummyPluginProject(String pluginName, String guid) {
final File pluginProjectFile = project.pluginSymlinkDirectory
.childDirectory(pluginName)
.childDirectory('windows')
.childFile('plugin.vcxproj');
pluginProjectFile.createSync(recursive: true);
pluginProjectFile.writeAsStringSync('''
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>15.0</VCProjectVersion>
<ProjectGuid>{$guid}</ProjectGuid>
<ProjectName>$pluginName</ProjectName>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="\$(VCTargetsPath)\\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<PlatformToolset>v142</PlatformToolset>
</PropertyGroup>
<Import Project="\$(VCTargetsPath)\\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" />
<PropertyGroup Label="UserMacros" />
<PropertyGroup />
<ItemDefinitionGroup />
<ItemGroup>
</ItemGroup>
<Import Project="\$(VCTargetsPath)\\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>''');
}
// Configures and returns a mock plugin with the given name and GUID in the
// project's plugin symlink directory.
Plugin getMockPlugin(String name, String guid) {
final MockPlugin plugin = MockPlugin();
when(plugin.platforms).thenReturn(<String, PluginPlatform>{project.pluginConfigKey: null});
when(plugin.name).thenReturn(name);
when(plugin.path).thenReturn(project.pluginSymlinkDirectory.childDirectory(name).path);
writeDummyPluginProject(name, guid);
return plugin;
}
test('Adding the first plugin to a solution adds the expected references', () async {
writeSolutionWithoutPlugins();
final List<Plugin> plugins = <Plugin>[
getMockPlugin('plugin_a', pluginAGuid),
];
await VisualStudioSolutionUtils(project: project, fileSystem: fs).updatePlugins(plugins);
final String newSolutionContents = project.solutionFile.readAsStringSync();
// Check for:
// - Plugin project.
final String pluginSubpath = fs.path.join('Flutter', 'ephemeral', '.plugin_symlinks', 'plugin_a', 'windows', 'plugin.vcxproj');
expect(newSolutionContents, contains('Project("{$solutionTypeGuidVcxproj}") = "plugin_a", "$pluginSubpath", "{$pluginAGuid}"'));
// - A dependency on the plugin project (from the Runner).
expect(newSolutionContents, contains('{$pluginAGuid} = {$pluginAGuid}'));
// - Plugin project configurations.
expect(newSolutionContents, contains('{$pluginAGuid}.Debug|x64.ActiveCfg = Debug|x64'));
expect(newSolutionContents, contains('{$pluginAGuid}.Debug|x64.Build.0 = Debug|x64'));
expect(newSolutionContents, contains('{$pluginAGuid}.Profile|x64.ActiveCfg = Profile|x64'));
expect(newSolutionContents, contains('{$pluginAGuid}.Profile|x64.Build.0 = Profile|x64'));
expect(newSolutionContents, contains('{$pluginAGuid}.Release|x64.ActiveCfg = Release|x64'));
expect(newSolutionContents, contains('{$pluginAGuid}.Release|x64.Build.0 = Release|x64'));
// - A plugin folder, and a child reference for the plugin.
expect(newSolutionContents, contains('Project("{$solutionTypeGuidFolder}") = "Flutter Plugins", "Flutter Plugins", "{$flutterPluginSolutionFolderGuid}"'));
expect(newSolutionContents, contains('{$pluginAGuid} = {$flutterPluginSolutionFolderGuid}'));
});
test('Removing a plugin removes entries as expected', () async {
writeSolutionWithPlugins();
final List<Plugin> plugins = <Plugin>[
getMockPlugin('plugin_a', pluginAGuid),
getMockPlugin('plugin_c', pluginCGuid),
];
await VisualStudioSolutionUtils(project: project, fileSystem: fs).updatePlugins(plugins);
final String newSolutionContents = project.solutionFile.readAsStringSync();
// There should be no references to plugin B anywhere.
expect(newSolutionContents.contains(pluginBGuid), false);
// All the plugin A & C references should still be present in modified
// sections.
for (final String guid in <String>[pluginAGuid, pluginCGuid]) {
expect(newSolutionContents, contains('{$guid} = {$guid}'));
expect(newSolutionContents, contains('{$guid}.Debug|x64.ActiveCfg = Debug|x64'));
expect(newSolutionContents, contains('{$guid} = {$flutterPluginSolutionFolderGuid}'));
}
});
test('Removing all plugins works', () async {
writeSolutionWithPlugins();
final List<Plugin> plugins = <Plugin>[
];
await VisualStudioSolutionUtils(project: project, fileSystem: fs).updatePlugins( plugins);
final String newSolutionContents = project.solutionFile.readAsStringSync();
// There should be no references to any of the plugins.
expect(newSolutionContents.contains(pluginAGuid), false);
expect(newSolutionContents.contains(pluginBGuid), false);
expect(newSolutionContents.contains(pluginCGuid), false);
// Nor any plugins in the Flutter Plugins folder.
expect(newSolutionContents.contains('= {$flutterPluginSolutionFolderGuid}'), false);
});
test('Adjusting the plugin list by adding and removing adjusts entries as expected', () async {
writeSolutionWithPlugins();
final List<Plugin> plugins = <Plugin>[
getMockPlugin('plugin_b', pluginBGuid),
getMockPlugin('plugin_c', pluginCGuid),
getMockPlugin('plugin_d', pluginDGuid),
];
await VisualStudioSolutionUtils(project: project, fileSystem: fs).updatePlugins(plugins);
final String newSolutionContents = project.solutionFile.readAsStringSync();
// There should be no references to plugin A anywhere.
expect(newSolutionContents.contains(pluginAGuid), false);
// All the plugin B & C references should still be present in modified
// sections.
for (final String guid in <String>[pluginBGuid, pluginCGuid]) {
expect(newSolutionContents, contains('{$guid} = {$guid}'));
expect(newSolutionContents, contains('{$guid}.Debug|x64.ActiveCfg = Debug|x64'));
expect(newSolutionContents, contains('{$guid} = {$flutterPluginSolutionFolderGuid}'));
}
// All the plugin D values should be added:
// - Plugin project.
final String pluginSubpath = fs.path.join('Flutter', 'ephemeral', '.plugin_symlinks', 'plugin_d', 'windows', 'plugin.vcxproj');
expect(newSolutionContents, contains('Project("{$solutionTypeGuidVcxproj}") = "plugin_d", "$pluginSubpath", "{$pluginDGuid}"'));
// - A dependency on the plugin project (from the Runner).
expect(newSolutionContents, contains('{$pluginDGuid} = {$pluginDGuid}'));
// - Plugin project configurations.
expect(newSolutionContents, contains('{$pluginDGuid}.Debug|x64.ActiveCfg = Debug|x64'));
expect(newSolutionContents, contains('{$pluginDGuid}.Debug|x64.Build.0 = Debug|x64'));
expect(newSolutionContents, contains('{$pluginDGuid}.Profile|x64.ActiveCfg = Profile|x64'));
expect(newSolutionContents, contains('{$pluginDGuid}.Profile|x64.Build.0 = Profile|x64'));
expect(newSolutionContents, contains('{$pluginDGuid}.Release|x64.ActiveCfg = Release|x64'));
expect(newSolutionContents, contains('{$pluginDGuid}.Release|x64.Build.0 = Release|x64'));
// - A child reference for the plugin in the Flutter Plugins folder.
expect(newSolutionContents, contains('{$pluginDGuid} = {$flutterPluginSolutionFolderGuid}'));
});
test('Adding plugins doesn\'t create duplicate entries', () async {
writeSolutionWithPlugins();
final List<Plugin> plugins = <Plugin>[
getMockPlugin('plugin_a', pluginAGuid),
getMockPlugin('plugin_b', pluginBGuid),
getMockPlugin('plugin_c', pluginCGuid),
getMockPlugin('plugin_d', pluginDGuid),
];
await VisualStudioSolutionUtils(project: project, fileSystem: fs).updatePlugins(plugins);
final String newSolutionContents = project.solutionFile.readAsStringSync();
// There should only be:
// - one Flutter Plugins folder.
expect('Project("{$solutionTypeGuidFolder}")'.allMatches(newSolutionContents).length, 1);
// - one copy of plugin A's project.
expect('Project("{$solutionTypeGuidVcxproj}") = "plugin_a"'.allMatches(newSolutionContents).length, 1);
// - one copy of plugin A's configuration entries.
expect('{$pluginAGuid}.Debug|x64.ActiveCfg = Debug|x64'.allMatches(newSolutionContents).length, 1);
// - one dependency from the Runner to plugin A.
expect('{$pluginAGuid} = {$pluginAGuid}'.allMatches(newSolutionContents).length, 1);
// - one copy of plugin A in Flutter Plugins.
expect('{$pluginAGuid} = {$flutterPluginSolutionFolderGuid}'.allMatches(newSolutionContents).length, 1);
});
test('Adding plugins doesn\'t change ordering', () async {
writeSolutionWithPlugins();
final List<Plugin> plugins = <Plugin>[
getMockPlugin('plugin_a', pluginAGuid),
getMockPlugin('plugin_b', pluginBGuid),
getMockPlugin('plugin_c', pluginCGuid),
getMockPlugin('plugin_d', pluginDGuid),
];
await VisualStudioSolutionUtils(project: project, fileSystem: fs).updatePlugins(plugins);
final String newSolutionContents = project.solutionFile.readAsStringSync();
// Plugin A should still be before Flutter Build in the Runner dependencies.
expect(newSolutionContents.indexOf('{$pluginAGuid} = {$pluginAGuid}'),
lessThan(newSolutionContents.indexOf('{6419BF13-6ECD-4CD2-9E85-E566A1F03F8F} = {6419BF13-6ECD-4CD2-9E85-E566A1F03F8F}')));
// ... and in the build configuration list.
expect(newSolutionContents.indexOf('{$pluginAGuid}.Debug|x64.ActiveCfg = Debug|x64'),
lessThan(newSolutionContents.indexOf('{6419BF13-6ECD-4CD2-9E85-E566A1F03F8F}.Debug|x64.ActiveCfg = Debug|x64')));
// And plugin C should still be before plugin A in the Flutter Plugins nesting list.
expect(newSolutionContents.indexOf('{$pluginCGuid} = {$flutterPluginSolutionFolderGuid}'),
lessThan(newSolutionContents.indexOf('{$pluginAGuid} = {$flutterPluginSolutionFolderGuid}')));
});
test('Updating solution preserves BOM', () async {
writeSolutionWithPlugins();
final List<Plugin> plugins = <Plugin>[];
await VisualStudioSolutionUtils(project: project, fileSystem: fs).updatePlugins(plugins);
// Visual Studio expects sln files to start with a BOM.
final List<int> solutionStartingBytes = project.solutionFile.readAsBytesSync().take(3).toList();
final List<int> bomBytes = utf8.encode(String.fromCharCode(unicodeBomCharacterRune));
expect(solutionStartingBytes, bomBytes);
});
test('Updating solution dosen\'t introduce unexpected whitespace', () async {
writeSolutionWithPlugins();
final List<Plugin> plugins = <Plugin>[
getMockPlugin('plugin_a', pluginAGuid),
getMockPlugin('plugin_b', pluginBGuid),
];
await VisualStudioSolutionUtils(project: project, fileSystem: fs).updatePlugins(plugins);
final String newSolutionContents = project.solutionFile.readAsStringSync();
// Project, EndProject, Global, and EndGlobal should be at the start of
// lines.
expect(RegExp(r'^[ \t]+Project\(', multiLine: true).hasMatch(newSolutionContents), false);
expect(RegExp(r'^[ \t]+EndProject\s*$', multiLine: true).hasMatch(newSolutionContents), false);
expect(RegExp(r'^[ \t]+Global\s*$', multiLine: true).hasMatch(newSolutionContents), false);
expect(RegExp(r'^[ \t]+EndGlobal\s*$', multiLine: true).hasMatch(newSolutionContents), false);
// ProjectSection, GlobalSection, and their ends should be indented
// exactly one tab.
expect(RegExp(r'^([ \t]+\t|\t[ \t]+)ProjectSection\(', multiLine: true).hasMatch(newSolutionContents), false);
expect(RegExp(r'^([ \t]+\t|\t[ \t]+)EndProjectSection\s*$', multiLine: true).hasMatch(newSolutionContents), false);
expect(RegExp(r'^([ \t]+\t|\t[ \t]+)GlobalSection\(\s*$', multiLine: true).hasMatch(newSolutionContents), false);
expect(RegExp(r'^([ \t]+\t|\t[ \t]+)EndGlobalSection\s*$', multiLine: true).hasMatch(newSolutionContents), false);
});
});
}
class MockWindowsProject extends Mock implements WindowsProject {}
class MockPlugin extends Mock implements Plugin {}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment