diff --git a/src/Peachpied.PhpUnit.TestAdapter/EnvironmentHelper.cs b/src/Peachpied.PhpUnit.TestAdapter/EnvironmentHelper.cs index 4608004..84cb314 100644 --- a/src/Peachpied.PhpUnit.TestAdapter/EnvironmentHelper.cs +++ b/src/Peachpied.PhpUnit.TestAdapter/EnvironmentHelper.cs @@ -34,5 +34,20 @@ namespace Peachpied.PhpUnit.TestAdapter return assemblyDir; } } + + /// + /// Generate a file name in the given directory with the given prefix which + /// does not exists in it yet. + /// + public static string GetNonexistingFilePath(string dir, string prefix = "") + { + string filePath; + do + { + filePath = Path.Combine(dir, prefix + Path.GetRandomFileName()); + } while (File.Exists(filePath)); + + return filePath; + } } } diff --git a/src/Peachpied.PhpUnit.TestAdapter/PhpUnitHelper.cs b/src/Peachpied.PhpUnit.TestAdapter/PhpUnitHelper.cs index c2f9eb1..4a219fd 100644 --- a/src/Peachpied.PhpUnit.TestAdapter/PhpUnitHelper.cs +++ b/src/Peachpied.PhpUnit.TestAdapter/PhpUnitHelper.cs @@ -4,6 +4,7 @@ using PHPUnit.Framework; using PHPUnit.TextUI; using System; using System.Collections.Generic; +using System.IO; using System.Reflection; using System.Text; @@ -14,6 +15,11 @@ namespace Peachpied.PhpUnit.TestAdapter /// internal static class PhpUnitHelper { + /// + /// Current PHPUnit version. + /// + public static Version Version { get; } = typeof(TestCase).Assembly.GetName().Version; + private const string PharName = "phpunit.phar"; static PhpUnitHelper() @@ -22,10 +28,30 @@ namespace Peachpied.PhpUnit.TestAdapter Context.AddScriptReference(typeof(TestCase).Assembly); } + /// + /// Find a PHPUnit configuration file in the given directory, return null if not present. + /// + public static string TryFindConfigFile(string dir) + { + string primaryConfig = Path.Combine(dir, "phpunit.xml"); + if (File.Exists(primaryConfig)) + { + return primaryConfig; + } + + string secondaryConfig = Path.Combine(dir, "phpunit.xml.dist"); + if (File.Exists(secondaryConfig)) + { + return secondaryConfig; + } + + return null; + } + /// /// Run PHPUnit on the given assembly and command line arguments. /// - public static void Launch(string cwd, string testedAssembly, string[] args, Action initCalblack = null, Action finishCallback = null) + public static void Launch(string cwd, string testedAssembly, string[] args, Action initCallback = null, Action finishCallback = null) { // Load assembly with tests (if not loaded yet) Context.AddScriptReference(Assembly.LoadFrom(testedAssembly)); @@ -39,7 +65,7 @@ namespace Peachpied.PhpUnit.TestAdapter ctx.Server[CommonPhpArrayKeys.SCRIPT_NAME] = "__DUMMY_INVALID_FILE"; // Perform any custom operations on the context - initCalblack?.Invoke(ctx); + initCallback?.Invoke(ctx); // Run the PHAR entry point so that all the classes are included var pharLoader = Context.TryGetDeclaredScript(PharName); diff --git a/src/Peachpied.PhpUnit.TestAdapter/PhpUnitTestExecutor.cs b/src/Peachpied.PhpUnit.TestAdapter/PhpUnitTestExecutor.cs index a506774..1e825f3 100644 --- a/src/Peachpied.PhpUnit.TestAdapter/PhpUnitTestExecutor.cs +++ b/src/Peachpied.PhpUnit.TestAdapter/PhpUnitTestExecutor.cs @@ -1,11 +1,15 @@ using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; +using Pchp.Core; using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; using System.Text.RegularExpressions; +using System.Xml; +using System.Xml.Linq; namespace Peachpied.PhpUnit.TestAdapter { @@ -44,8 +48,7 @@ namespace Peachpied.PhpUnit.TestAdapter { try { - // Inject our custom extension to report the test results - var args = new[] { "--extensions", TestReporterExtension.PhpName }; + var args = Array.Empty(); // Optionally filter the test cases by their names if (testCases != null) @@ -62,7 +65,8 @@ namespace Peachpied.PhpUnit.TestAdapter string projectDir = EnvironmentHelper.TryFindProjectDirectory(Path.GetDirectoryName(source)); - PhpUnitHelper.Launch(projectDir, source, args, + // Inject our custom extension to report the test results + RunPhpUnitWithExtension(projectDir, source, args, ctx => { // Enable Peachpie to create an instance of our custom extension @@ -79,6 +83,52 @@ namespace Peachpied.PhpUnit.TestAdapter } } + private void RunPhpUnitWithExtension(string projectDir, string source, string[] args, Action initCallback) + { + if (PhpUnitHelper.Version >= new Version(9, 1)) + { + // The --extensions CLI argument is available from PHPUnit 9.1 (although it's documented only from 9.3) + args = new[] { "--extensions", TestReporterExtension.PhpName }.Concat(args).ToArray(); + PhpUnitHelper.Launch(projectDir, source, args, initCallback); + } + else + { + // Older PHPUnit versions can receive extensions only in the configuration file, + // so we need to create a modified version of the existing one and pass it to PHPUnit. + // (or create one from scratch it if it doesn't exist at all) + + // Create PHPUnit configuration with the extension added + string origConfigFile = PhpUnitHelper.TryFindConfigFile(projectDir); + var configXml = (origConfigFile != null) ? XElement.Load(origConfigFile) : new XElement("phpunit"); + var extensionsEl = configXml.GetOrCreateElement("extensions"); + extensionsEl.Add(new XElement("extension", new XAttribute("class", TestReporterExtension.PhpName))); + + // Store the configuration in a temporary file to pass it to PHPUnit + string tempConfigFile = null; + try + { + // The configuration file must be in the same folder as the project in order to work (to load PHP classes properly etc.) + tempConfigFile = EnvironmentHelper.GetNonexistingFilePath(projectDir, "phpunit.xml."); + + // Save the config file without the BOM + using (var xmlWriter = new XmlTextWriter(tempConfigFile, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false))) + { + configXml.Save(xmlWriter); + } + + args = new[] { "--configuration", tempConfigFile }.Concat(args).ToArray(); + PhpUnitHelper.Launch(projectDir, source, args, initCallback); + } + finally + { + if (File.Exists(tempConfigFile)) + { + File.Delete(tempConfigFile); + } + } + } + } + /// /// Cancel test execution, currently ignored. /// diff --git a/src/Peachpied.PhpUnit.TestAdapter/XElementExtensions.cs b/src/Peachpied.PhpUnit.TestAdapter/XElementExtensions.cs new file mode 100644 index 0000000..735f9c9 --- /dev/null +++ b/src/Peachpied.PhpUnit.TestAdapter/XElementExtensions.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Xml.Linq; + +namespace Peachpied.PhpUnit.TestAdapter +{ + /// + /// LINQ to XML utility methods. + /// + internal static class XElementExtensions + { + /// + /// Retrieve the child element of the given name if it exists, or add an empty new one if it doesn't exist. + /// + public static XElement GetOrCreateElement(this XElement parent, XName elementName) + { + var element = parent.Element(elementName); + if (element == null) + { + element = new XElement(elementName); + parent.Add(element); + } + + return element; + } + } +}