public static IReadOnlyCollection <PatchResult> PatchAssemblies(
            [NotNull] List <string> testAssemblyPaths)
        {
            var testAssemblyPath   = testAssemblyPaths[0];
            var testAssemblyFolder = Path.GetDirectoryName(testAssemblyPath);

            if (testAssemblyFolder.IsNullOrEmpty())
            {
                throw new Exception("Unable to find folder for test assembly");
            }
            testAssemblyFolder = Path.GetFullPath(testAssemblyFolder);

            // scope
            {
                var thisAssemblyFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
                if (thisAssemblyFolder.IsNullOrEmpty())
                {
                    throw new Exception("Can only patch assemblies on disk");
                }
                thisAssemblyFolder = Path.GetFullPath(thisAssemblyFolder);

                // keep things really simple, at least for now
                if (string.Compare(testAssemblyFolder, thisAssemblyFolder, StringComparison.OrdinalIgnoreCase) != 0)
                {
                    throw new Exception("All assemblies must be in the same folder");
                }
            }

            var nsubElevatedPath = Path.Combine(testAssemblyFolder, "NSubstitute.Elevated.dll");
            var mockInjector     = new MockInjector();

            foreach (var assemblyPath in testAssemblyPaths)
            {
                var assemblyDefinition = AssemblyDefinition.ReadAssembly(assemblyPath);
                mockInjector.Patch(assemblyDefinition);
                // atomic write of file with backup
                var tmpPath = assemblyPath.Split(new[] { ".dll" }, StringSplitOptions.None)[0] + ".tmp";
                File.Delete(tmpPath);
                assemblyDefinition.Write(tmpPath);//$$$$, new WriterParameters { WriteSymbols = true });  // getting exception, haven't looked into it yet
                assemblyDefinition.Dispose();

                /*var originalPath = GetPatchBackupPathFor(assemblyToPatchPath);
                 * File.Replace(tmpPath, assemblyToPatchPath, originalPath);*/
                // $$$ TODO: move pdb file too
            }

            return(null);
        }
        public static IReadOnlyCollection <PatchResult> PatchAllDependentAssemblies(NPath testAssemblyPath, PatchOptions patchOptions)
        {
            // TODO: ensure we do not have any assemblies that we want to patch already loaded
            // (this will require the separate in-memory patching ability)

            // this dll has types we're going to be injecting, so ensure it is in the same folder
            //var targetWeaverDll

            var toProcess = new List <NPath> {
                testAssemblyPath.FileMustExist()
            };
            var patchResults = new Dictionary <string, PatchResult>(StringComparer.OrdinalIgnoreCase);
            var mockInjector = new MockInjector();

            EnsureMockTypesAssemblyInFolder(testAssemblyPath.Parent);

            for (var toProcessIndex = 0; toProcessIndex < toProcess.Count; ++toProcessIndex)
            {
                var assemblyToPatchPath = toProcess[toProcessIndex];

                // as we accumulate dependencies recursively, we will probably get some duplicates we can early-out on
                if (patchResults.ContainsKey(assemblyToPatchPath))
                {
                    continue;
                }

                using (var assemblyToPatch = AssemblyDefinition.ReadAssembly(assemblyToPatchPath))
                {
                    GatherReferences(assemblyToPatchPath, assemblyToPatch);

                    var patchResult = TryPatch(assemblyToPatchPath, assemblyToPatch);
                    patchResults.Add(assemblyToPatchPath, patchResult);
                }
            }

            void GatherReferences(NPath assemblyToPatchPath, AssemblyDefinition assemblyToPatch)
            {
                foreach (var referencedAssembly in assemblyToPatch.Modules.SelectMany(m => m.AssemblyReferences))
                {
                    // only patch dll's we "own", that are in the same folder as the test assembly
                    var referencedAssemblyPath = assemblyToPatchPath.Parent.Combine(referencedAssembly.Name + ".dll");

                    if (referencedAssemblyPath.FileExists())
                    {
                        toProcess.Add(referencedAssemblyPath);
                    }
                    else if (!patchResults.ContainsKey(referencedAssembly.Name))
                    {
                        patchResults.Add(referencedAssembly.Name, new PatchResult(referencedAssembly.Name, null, PatchState.IgnoredForeignAssembly));
                    }
                }
            }

            PatchResult TryPatch(NPath assemblyToPatchPath, AssemblyDefinition assemblyToPatch)
            {
                var alreadyPatched = MockInjector.IsPatched(assemblyToPatch);
                var cannotPatch    = assemblyToPatch.Name.HasPublicKey;

                if (assemblyToPatchPath == testAssemblyPath && (patchOptions & PatchOptions.PatchTestAssembly) == 0)
                {
                    if (alreadyPatched)
                    {
                        throw new Exception("Unexpected already-patched test assembly, yet we want PatchTestAssembly.No");
                    }
                    if (cannotPatch)
                    {
                        throw new Exception("Cannot patch an assembly with a strong name");
                    }
                    return(new PatchResult(assemblyToPatchPath, null, PatchState.IgnoredTestAssembly));
                }

                if (alreadyPatched)
                {
                    return(new PatchResult(assemblyToPatchPath, null, PatchState.AlreadyPatched));
                }
                if (cannotPatch)
                {
                    return(new PatchResult(assemblyToPatchPath, null, PatchState.IgnoredForeignAssembly));
                }

                return(Patch(assemblyToPatchPath, assemblyToPatch));
            }

            PatchResult Patch(NPath assemblyToPatchPath, AssemblyDefinition assemblyToPatch)
            {
                mockInjector.Patch(assemblyToPatch);

                // atomic write of file with backup
                // TODO: skip backup if existing file already patched. want the .orig to only be the unpatched file.

                // write to tmp and release the lock
                var tmpPath = assemblyToPatchPath.ChangeExtension(".tmp");

                tmpPath.DeleteIfExists();
                assemblyToPatch.Write(tmpPath); // $$$ , new WriterParameters { WriteSymbols = true }); see https://github.com/jbevain/cecil/issues/421
                assemblyToPatch.Dispose();

                if ((patchOptions & PatchOptions.SkipPeVerify) == 0)
                {
                    PeVerify.Verify(tmpPath);
                }

                // move the actual file to backup, and move the tmp to actual
                var backupPath = GetPatchBackupPathFor(assemblyToPatchPath);

                File.Replace(tmpPath, assemblyToPatchPath, backupPath);

                // TODO: move pdb file too

                return(new PatchResult(assemblyToPatchPath, backupPath, PatchState.Patched));
            }

            return(patchResults.Values);
        }
        public static IReadOnlyCollection <PatchResult> PatchAllDependentAssemblies(
            [NotNull] string testAssemblyPath,
            PatchTestAssembly patchTestAssembly = PatchTestAssembly.No) // typically we don't want to patch the test assembly itself, only the systems under test
        {
            var testAssemblyFolder = Path.GetDirectoryName(testAssemblyPath);

            if (testAssemblyFolder.IsNullOrEmpty())
            {
                throw new Exception("Unable to find folder for test assembly");
            }
            testAssemblyFolder = Path.GetFullPath(testAssemblyFolder);

            // scope
            {
                var thisAssemblyFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
                if (thisAssemblyFolder.IsNullOrEmpty())
                {
                    throw new Exception("Can only patch assemblies on disk");
                }
                thisAssemblyFolder = Path.GetFullPath(thisAssemblyFolder);

                // keep things really simple, at least for now
                if (string.Compare(testAssemblyFolder, thisAssemblyFolder, StringComparison.OrdinalIgnoreCase) != 0)
                {
                    throw new Exception("All assemblies must be in the same folder");
                }
            }

            var nsubElevatedPath = Path.Combine(testAssemblyFolder, "NSubstitute.Elevated.dll");

            using (var nsubElevatedAssembly = AssemblyDefinition.ReadAssembly(nsubElevatedPath))
            {
                var mockInjector = new MockInjector(nsubElevatedAssembly);
                var toProcess    = new List <string> {
                    Path.GetFullPath(testAssemblyPath)
                };
                var patchResults = new Dictionary <string, PatchResult>(StringComparer.OrdinalIgnoreCase);

                for (var toProcessIndex = 0; toProcessIndex < toProcess.Count; ++toProcessIndex)
                {
                    var assemblyToPatchPath = toProcess[toProcessIndex];
                    if (patchResults.ContainsKey(assemblyToPatchPath))
                    {
                        continue;
                    }

                    if (!Path.IsPathRooted(assemblyToPatchPath))
                    {
                        throw new Exception($"Unexpected non-rooted assembly path '{assemblyToPatchPath}'");
                    }

                    using (var assemblyToPatch = AssemblyDefinition.ReadAssembly(assemblyToPatchPath))
                    {
                        foreach (var referencedAssembly in assemblyToPatch.Modules.SelectMany(m => m.AssemblyReferences))
                        {
                            // only patch dll's we "own", that are in the same folder as the test assembly
                            var foundPath = Path.Combine(testAssemblyFolder, referencedAssembly.Name + ".dll");

                            if (File.Exists(foundPath))
                            {
                                toProcess.Add(foundPath);
                            }
                            else if (!patchResults.ContainsKey(referencedAssembly.Name))
                            {
                                patchResults.Add(referencedAssembly.Name, new PatchResult(referencedAssembly.Name, null, PatchState.IgnoredOutsideAllowedPaths));
                            }
                        }

                        PatchResult patchResult;

                        if (toProcessIndex == 0 && patchTestAssembly == PatchTestAssembly.No)
                        {
                            patchResult = new PatchResult(assemblyToPatchPath, null, PatchState.IgnoredTestAssembly);
                        }
                        else if (MockInjector.IsPatched(assemblyToPatch))
                        {
                            patchResult = new PatchResult(assemblyToPatchPath, null, PatchState.AlreadyPatched);
                        }
                        else
                        {
                            mockInjector.Patch(assemblyToPatch);

                            // atomic write of file with backup
                            var tmpPath = assemblyToPatchPath + ".tmp";
                            File.Delete(tmpPath);
                            assemblyToPatch.Write(tmpPath);//$$$$, new WriterParameters { WriteSymbols = true });  // getting exception, haven't looked into it yet
                            assemblyToPatch.Dispose();
                            var originalPath = GetPatchBackupPathFor(assemblyToPatchPath);
                            File.Replace(tmpPath, assemblyToPatchPath, originalPath);
                            // $$$ TODO: move pdb file too

                            patchResult = new PatchResult(assemblyToPatchPath, originalPath, PatchState.Patched);
                        }

                        patchResults.Add(assemblyToPatchPath, patchResult);
                    }
                }

                return(patchResults.Values);
            }
        }