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
diff --git a/packages/flutter_tools/test/general.shard/plugins_test.dart b/packages/flutter_tools/test/general.shard/plugins_test.dart
index 63f25b7..7fb5e87 100644
--- a/packages/flutter_tools/test/general.shard/plugins_test.dart
+++ b/packages/flutter_tools/test/general.shard/plugins_test.dart
@@ -76,6 +76,7 @@
       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 @@
       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 @@
         when(featureFlags.isWindowsEnabled).thenReturn(true);
         when(flutterProject.isModule).thenReturn(false);
         configureDummyPackageAsPlugin();
+        createDummyWindowsSolutionFile();
+        createDummyPluginWindowsProjectFile();
 
         await injectPlugins(flutterProject, checkProjects: true);
 
@@ -857,6 +898,8 @@
         when(featureFlags.isWindowsEnabled).thenReturn(true);
         when(flutterProject.isModule).thenReturn(false);
         configureDummyPackageAsPlugin();
+        createDummyWindowsSolutionFile();
+        createDummyPluginWindowsProjectFile();
 
         await injectPlugins(flutterProject, checkProjects: true);
 
@@ -871,6 +914,23 @@
         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', () {