From 04e24ca15acb9e0d84641640cd553cc908e1103e Mon Sep 17 00:00:00 2001
From: Koji Hasegawa <hasegawa@hubsys.co.jp>
Date: Mon, 6 May 2024 20:11:09 +0900
Subject: [PATCH 1/5] Add JUnitXmlWriter

---
 Editor/JUnitXmlWriter.cs                      | 41 +++++++++++++
 Editor/JUnitXmlWriter.cs.meta                 |  2 +
 Editor/TestRunnerCallbacksImpl.cs             | 41 +++++++++++++
 Editor/TestRunnerCallbacksImpl.cs.meta        |  3 +
 RuntimeInternals/CommandLineArgs.cs           | 19 ++++++
 Tests/Editor/JUnitXmlWriterTest.cs            | 46 ++++++++++++++
 Tests/Editor/JUnitXmlWriterTest.cs.meta       |  2 +
 Tests/Editor/TestDoubles.meta                 |  3 +
 .../TestDoubles/FakeTestResultAdaptor.cs      | 54 ++++++++++++++++
 .../TestDoubles/FakeTestResultAdaptor.cs.meta |  3 +
 Tests/Editor/TestResources.meta               |  3 +
 Tests/Editor/TestResources/junit.xml          | 10 +++
 Tests/Editor/TestResources/junit.xml.meta     |  7 +++
 Tests/Editor/TestResources/nunit3.xml         | 61 +++++++++++++++++++
 Tests/Editor/TestResources/nunit3.xml.meta    |  7 +++
 Tests/RuntimeInternals/CommandLineArgsTest.cs | 14 +++++
 16 files changed, 316 insertions(+)
 create mode 100644 Editor/JUnitXmlWriter.cs
 create mode 100644 Editor/JUnitXmlWriter.cs.meta
 create mode 100644 Editor/TestRunnerCallbacksImpl.cs
 create mode 100644 Editor/TestRunnerCallbacksImpl.cs.meta
 create mode 100644 Tests/Editor/JUnitXmlWriterTest.cs
 create mode 100644 Tests/Editor/JUnitXmlWriterTest.cs.meta
 create mode 100644 Tests/Editor/TestDoubles.meta
 create mode 100644 Tests/Editor/TestDoubles/FakeTestResultAdaptor.cs
 create mode 100644 Tests/Editor/TestDoubles/FakeTestResultAdaptor.cs.meta
 create mode 100644 Tests/Editor/TestResources.meta
 create mode 100644 Tests/Editor/TestResources/junit.xml
 create mode 100644 Tests/Editor/TestResources/junit.xml.meta
 create mode 100644 Tests/Editor/TestResources/nunit3.xml
 create mode 100644 Tests/Editor/TestResources/nunit3.xml.meta

diff --git a/Editor/JUnitXmlWriter.cs b/Editor/JUnitXmlWriter.cs
new file mode 100644
index 0000000..605fa18
--- /dev/null
+++ b/Editor/JUnitXmlWriter.cs
@@ -0,0 +1,41 @@
+// Copyright (c) 2023-2024 Koji Hasegawa.
+// This software is released under the MIT License.
+
+using System.IO;
+using System.Xml;
+using System.Xml.XPath;
+using System.Xml.Xsl;
+using UnityEditor.TestTools.TestRunner.Api;
+
+namespace TestHelper.Editor
+{
+    public static class JUnitXmlWriter
+    {
+        private const string XsltPath = "Packages/com.nowsprinting.test-helper/Editor/nunit3-junit/nunit3-junit.xslt";
+        // Note: This XSLT file is copied from https://github.com/nunit/nunit-transforms/tree/master/nunit3-junit
+
+        public static void WriteTo(ITestResultAdaptor result, string path)
+        {
+            // Input
+            var nunit3XmlStream = new MemoryStream();
+            var nunit3Writer = XmlWriter.Create(nunit3XmlStream);
+            result.ToXml().WriteTo(nunit3Writer);
+            var nunit3Xml = new XPathDocument(nunit3XmlStream);
+
+            // Create output directory if it does not exist.
+            var directory = Path.GetDirectoryName(path);
+            if (directory != null && !Directory.Exists(directory))
+            {
+                Directory.CreateDirectory(directory);
+            }
+
+            // Output (JUnit XML)
+            var writer = XmlWriter.Create(path);
+
+            // Execute the transformation.
+            var transformer = new XslCompiledTransform();
+            transformer.Load(XsltPath);
+            transformer.Transform(nunit3Xml, writer);
+        }
+    }
+}
diff --git a/Editor/JUnitXmlWriter.cs.meta b/Editor/JUnitXmlWriter.cs.meta
new file mode 100644
index 0000000..f6aa9e3
--- /dev/null
+++ b/Editor/JUnitXmlWriter.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 4dc20ae42ef14abd914e0200e5b9e36b
\ No newline at end of file
diff --git a/Editor/TestRunnerCallbacksImpl.cs b/Editor/TestRunnerCallbacksImpl.cs
new file mode 100644
index 0000000..a258882
--- /dev/null
+++ b/Editor/TestRunnerCallbacksImpl.cs
@@ -0,0 +1,41 @@
+// Copyright (c) 2023-2024 Koji Hasegawa.
+// This software is released under the MIT License.
+
+using TestHelper.RuntimeInternals;
+using UnityEditor;
+using UnityEditor.TestTools.TestRunner.Api;
+using UnityEngine;
+
+namespace TestHelper.Editor
+{
+    public class TestRunnerCallbacksImpl : ICallbacks
+    {
+        [InitializeOnLoadMethod]
+        private static void SetupCallbacks()
+        {
+            var api = ScriptableObject.CreateInstance<TestRunnerApi>();
+            api.RegisterCallbacks(new TestRunnerCallbacksImpl());
+        }
+
+        public void RunStarted(ITestAdaptor testsToRun)
+        {
+        }
+
+        public void RunFinished(ITestResultAdaptor result)
+        {
+            var path = CommandLineArgs.GetJUnitResultsPath();
+            if (path != null)
+            {
+                JUnitXmlWriter.WriteTo(result, path);
+            }
+        }
+
+        public void TestStarted(ITestAdaptor test)
+        {
+        }
+
+        public void TestFinished(ITestResultAdaptor result)
+        {
+        }
+    }
+}
diff --git a/Editor/TestRunnerCallbacksImpl.cs.meta b/Editor/TestRunnerCallbacksImpl.cs.meta
new file mode 100644
index 0000000..ca6e86e
--- /dev/null
+++ b/Editor/TestRunnerCallbacksImpl.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: f474f20a34e94893ae5d168787df181b
+timeCreated: 1714972666
\ No newline at end of file
diff --git a/RuntimeInternals/CommandLineArgs.cs b/RuntimeInternals/CommandLineArgs.cs
index 5714e33..a4a225d 100644
--- a/RuntimeInternals/CommandLineArgs.cs
+++ b/RuntimeInternals/CommandLineArgs.cs
@@ -54,5 +54,24 @@ public static string GetScreenshotDirectory(string[] args = null)
                 return Path.Combine(Application.persistentDataPath, "TestHelper", "Screenshots");
             }
         }
+
+        /// <summary>
+        /// JUnit XML report save path.
+        /// </summary>
+        /// <returns></returns>
+        public static string GetJUnitResultsPath(string[] args = null)
+        {
+            const string JUnitResultsKey = "-testHelperJUnitResults";
+
+            try
+            {
+                args = args ?? Environment.GetCommandLineArgs();
+                return DictionaryFromCommandLineArgs(args)[JUnitResultsKey];
+            }
+            catch (KeyNotFoundException)
+            {
+                return null;
+            }
+        }
     }
 }
diff --git a/Tests/Editor/JUnitXmlWriterTest.cs b/Tests/Editor/JUnitXmlWriterTest.cs
new file mode 100644
index 0000000..b66b957
--- /dev/null
+++ b/Tests/Editor/JUnitXmlWriterTest.cs
@@ -0,0 +1,46 @@
+// Copyright (c) 2023-2024 Koji Hasegawa.
+// This software is released under the MIT License.
+
+using System.IO;
+using NUnit.Framework;
+using TestHelper.Editor.TestDoubles;
+
+namespace TestHelper.Editor
+{
+    [TestFixture]
+    public class JUnitXmlWriterTest
+    {
+        private const string TestResourcesPath = "Packages/com.nowsprinting.test-helper/Tests/Editor/TestResources";
+        private const string TestOutputDirectoryPath = "Logs/JUnitXmlWriterTest";
+        // Note: relative path from the project root directory.
+
+        [Test, Order(0)]
+        public void WriteTo_DirectoryDoesNotExist_CreateDirectoryAndWriteToFile()
+        {
+            if (Directory.Exists(TestOutputDirectoryPath))
+            {
+                Directory.Delete(TestOutputDirectoryPath, true);
+            }
+
+            var nunitXmlPath = Path.Combine(TestResourcesPath, "nunit3.xml");
+            var result = new FakeTestResultAdaptor(nunitXmlPath);
+            var path = Path.Combine(TestOutputDirectoryPath, TestContext.CurrentContext.Test.Name + ".xml");
+            JUnitXmlWriter.WriteTo(result, path);
+
+            Assert.That(path, Does.Exist);
+        }
+
+        [Test]
+        public void WriteTo_WriteToFile()
+        {
+            var nunitXmlPath = Path.Combine(TestResourcesPath, "nunit3.xml");
+            var result = new FakeTestResultAdaptor(nunitXmlPath);
+            var path = Path.Combine(TestOutputDirectoryPath, TestContext.CurrentContext.Test.Name + ".xml");
+            JUnitXmlWriter.WriteTo(result, path);
+
+            var actual = File.ReadAllText(path);
+            var expected = File.ReadAllText(Path.Combine(TestResourcesPath, "junit.xml"));
+            Assert.That(actual, Is.EqualTo(expected));
+        }
+    }
+}
diff --git a/Tests/Editor/JUnitXmlWriterTest.cs.meta b/Tests/Editor/JUnitXmlWriterTest.cs.meta
new file mode 100644
index 0000000..95c1c25
--- /dev/null
+++ b/Tests/Editor/JUnitXmlWriterTest.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: abb979e3bb6b4ddd8e55f15691105f22
\ No newline at end of file
diff --git a/Tests/Editor/TestDoubles.meta b/Tests/Editor/TestDoubles.meta
new file mode 100644
index 0000000..8fa1fc2
--- /dev/null
+++ b/Tests/Editor/TestDoubles.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 1b70d94c46ae4c5ead3ad474b63d0c94
+timeCreated: 1714994775
\ No newline at end of file
diff --git a/Tests/Editor/TestDoubles/FakeTestResultAdaptor.cs b/Tests/Editor/TestDoubles/FakeTestResultAdaptor.cs
new file mode 100644
index 0000000..2f59f78
--- /dev/null
+++ b/Tests/Editor/TestDoubles/FakeTestResultAdaptor.cs
@@ -0,0 +1,54 @@
+// Copyright (c) 2023-2024 Koji Hasegawa.
+// This software is released under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using NUnit.Framework.Interfaces;
+using UnityEditor.TestTools.TestRunner.Api;
+using TestStatus = UnityEditor.TestTools.TestRunner.Api.TestStatus;
+
+namespace TestHelper.Editor.TestDoubles
+{
+    /// <summary>
+    /// Implemented only <c>ToXml</c> method.
+    /// </summary>
+    public class FakeTestResultAdaptor : ITestResultAdaptor
+    {
+        private readonly string _path;
+
+        /// <summary>
+        /// Constructor.
+        /// </summary>
+        /// <param name="path">NUnit3 XML file path used in <c>ToXml</c> method.</param>
+        public FakeTestResultAdaptor(string path)
+        {
+            _path = path;
+        }
+
+        public TNode ToXml()
+        {
+            var xmlText = File.ReadAllText(_path);
+            return TNode.FromXml(xmlText);
+        }
+
+        public ITestAdaptor Test { get { throw new NotImplementedException(); } }
+        public string Name { get { throw new NotImplementedException(); } }
+        public string FullName { get { throw new NotImplementedException(); } }
+        public string ResultState { get { throw new NotImplementedException(); } }
+        public TestStatus TestStatus { get { throw new NotImplementedException(); } }
+        public double Duration { get { throw new NotImplementedException(); } }
+        public DateTime StartTime { get { throw new NotImplementedException(); } }
+        public DateTime EndTime { get { throw new NotImplementedException(); } }
+        public string Message { get { throw new NotImplementedException(); } }
+        public string StackTrace { get { throw new NotImplementedException(); } }
+        public int AssertCount { get { throw new NotImplementedException(); } }
+        public int FailCount { get { throw new NotImplementedException(); } }
+        public int PassCount { get { throw new NotImplementedException(); } }
+        public int SkipCount { get { throw new NotImplementedException(); } }
+        public int InconclusiveCount { get { throw new NotImplementedException(); } }
+        public bool HasChildren { get { throw new NotImplementedException(); } }
+        public IEnumerable<ITestResultAdaptor> Children { get { throw new NotImplementedException(); } }
+        public string Output { get { throw new NotImplementedException(); } }
+    }
+}
diff --git a/Tests/Editor/TestDoubles/FakeTestResultAdaptor.cs.meta b/Tests/Editor/TestDoubles/FakeTestResultAdaptor.cs.meta
new file mode 100644
index 0000000..f5c4ee1
--- /dev/null
+++ b/Tests/Editor/TestDoubles/FakeTestResultAdaptor.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 91128e261ace43cfb04e5ea70971677a
+timeCreated: 1714994808
\ No newline at end of file
diff --git a/Tests/Editor/TestResources.meta b/Tests/Editor/TestResources.meta
new file mode 100644
index 0000000..5f3d813
--- /dev/null
+++ b/Tests/Editor/TestResources.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: d63ee51a7f8948909631726b0a5a269d
+timeCreated: 1714990355
\ No newline at end of file
diff --git a/Tests/Editor/TestResources/junit.xml b/Tests/Editor/TestResources/junit.xml
new file mode 100644
index 0000000..5749df1
--- /dev/null
+++ b/Tests/Editor/TestResources/junit.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?><testsuites tests="3" failures="0" disabled="0" time="0.2538825"><testsuite tests="1" time="0.092622" errors="0" failures="0" skipped="0" timestamp="2024-05-03 10:09:28Z" name="UnityProject~.TestHelper.Editor."><testcase name="Attach_CreateNewSceneWithCameraAndLight" assertions="0" time="0.036802" status="Passed" classname="TestHelper.Editor.CreateSceneAttributeTest"><system-out>[Code Coverage] Code Coverage results for visited sequence points were saved in /github/workspace/CodeCoverage/UnityProject~-opencov/EditMode/TestCoverageResults_0001.xml
+</system-out></testcase></testsuite><testsuite tests="1" time="0.089184" errors="0" failures="0" skipped="0" timestamp="2024-05-03 10:09:28Z" name="UnityProject~.TestHelper.Editor."><testcase name="Attach_LoadedSceneNotInBuild" assertions="0" time="0.053176" status="Passed" classname="TestHelper.Editor.LoadSceneAttributeTest"><system-out>[Code Coverage] Code Coverage results for visited sequence points were saved in /github/workspace/CodeCoverage/UnityProject~-opencov/EditMode/TestCoverageResults_0002.xml
+</system-out></testcase></testsuite><testsuite tests="1" time="0.055223" errors="0" failures="0" skipped="0" timestamp="2024-05-03 10:09:28Z" name="UnityProject~.TestHelper.Editor."><testcase name="GetScenesUsingInTest_AttachedToMethod_ReturnScenesSpecifiedByAttribute" assertions="0" time="0.014506" status="Passed" classname="TestHelper.Editor.TemporaryBuildScenesUsingInTestTest"><system-out>[Code Coverage] Code Coverage results for visited sequence points were saved in /github/workspace/CodeCoverage/UnityProject~-opencov/EditMode/TestCoverageResults_0003.xml
+[Code Coverage] Code Coverage Report was generated in /github/workspace/CodeCoverage/Report
+Included Assemblies: TestHelper,TestHelper.RuntimeInternals,TestHelper.Editor,
+Excluded Assemblies: *.Tests
+Included Paths: &lt;Not specified&gt;
+Excluded Paths: &lt;Not specified&gt;
+Saving results to: /github/workspace/artifacts/editmode-results.xml
+</system-out></testcase></testsuite></testsuites>
\ No newline at end of file
diff --git a/Tests/Editor/TestResources/junit.xml.meta b/Tests/Editor/TestResources/junit.xml.meta
new file mode 100644
index 0000000..b047d49
--- /dev/null
+++ b/Tests/Editor/TestResources/junit.xml.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 85243ab70dec645acb9374c9fbf196b1
+TextScriptImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Tests/Editor/TestResources/nunit3.xml b/Tests/Editor/TestResources/nunit3.xml
new file mode 100644
index 0000000..fd467eb
--- /dev/null
+++ b/Tests/Editor/TestResources/nunit3.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<test-run id="2" testcasecount="3" result="Passed" total="3" passed="3" failed="0" inconclusive="0" skipped="0" asserts="0" engine-version="3.5.0.0" clr-version="4.0.30319.42000" start-time="2024-05-03 10:09:28Z" end-time="2024-05-03 10:09:28Z" duration="0.2538825">
+  <test-suite type="TestSuite" id="1000" name="UnityProject~" fullname="UnityProject~" runstate="Runnable" testcasecount="3" result="Passed" start-time="2024-05-03 10:09:28Z" end-time="2024-05-03 10:09:28Z" duration="0.253883" total="3" passed="3" failed="0" inconclusive="0" skipped="0" asserts="0">
+    <properties>
+      <property name="platform" value="EditMode" />
+    </properties>
+    <test-suite type="Assembly" id="1009" name="TestHelper.Editor.Tests.dll" fullname="/github/workspace/UnityProject~/Library/ScriptAssemblies/TestHelper.Editor.Tests.dll" runstate="Runnable" testcasecount="3" result="Passed" start-time="2024-05-03 10:09:28Z" end-time="2024-05-03 10:09:28Z" duration="0.246003" total="3" passed="3" failed="0" inconclusive="0" skipped="0" asserts="0">
+      <properties>
+        <property name="_PID" value="3301" />
+        <property name="_APPDOMAIN" value="Unity Child Domain" />
+        <property name="platform" value="EditMode" />
+        <property name="EditorOnly" value="True" />
+      </properties>
+      <test-suite type="TestSuite" id="1010" name="TestHelper" fullname="TestHelper" runstate="Runnable" testcasecount="3" result="Passed" start-time="2024-05-03 10:09:28Z" end-time="2024-05-03 10:09:28Z" duration="0.245391" total="3" passed="3" failed="0" inconclusive="0" skipped="0" asserts="0">
+        <properties />
+        <test-suite type="TestSuite" id="1011" name="Editor" fullname="TestHelper.Editor" runstate="Runnable" testcasecount="3" result="Passed" start-time="2024-05-03 10:09:28Z" end-time="2024-05-03 10:09:28Z" duration="0.241916" total="3" passed="3" failed="0" inconclusive="0" skipped="0" asserts="0">
+          <properties />
+          <test-suite type="TestFixture" id="1003" name="CreateSceneAttributeTest" fullname="TestHelper.Editor.CreateSceneAttributeTest" classname="TestHelper.Editor.CreateSceneAttributeTest" runstate="Runnable" testcasecount="1" result="Passed" start-time="2024-05-03 10:09:28Z" end-time="2024-05-03 10:09:28Z" duration="0.092622" total="1" passed="1" failed="0" inconclusive="0" skipped="0" asserts="0">
+            <properties />
+            <test-case id="1004" name="Attach_CreateNewSceneWithCameraAndLight" fullname="TestHelper.Editor.CreateSceneAttributeTest.Attach_CreateNewSceneWithCameraAndLight" methodname="Attach_CreateNewSceneWithCameraAndLight" classname="TestHelper.Editor.CreateSceneAttributeTest" runstate="Runnable" seed="1087926775" result="Passed" start-time="2024-05-03 10:09:28Z" end-time="2024-05-03 10:09:28Z" duration="0.036802" asserts="0">
+              <properties>
+                <property name="retryIteration" value="0" />
+                <property name="repeatIteration" value="0" />
+              </properties>
+              <output><![CDATA[[Code Coverage] Code Coverage results for visited sequence points were saved in /github/workspace/CodeCoverage/UnityProject~-opencov/EditMode/TestCoverageResults_0001.xml
+]]></output>
+            </test-case>
+          </test-suite>
+          <test-suite type="TestFixture" id="1005" name="LoadSceneAttributeTest" fullname="TestHelper.Editor.LoadSceneAttributeTest" classname="TestHelper.Editor.LoadSceneAttributeTest" runstate="Runnable" testcasecount="1" result="Passed" start-time="2024-05-03 10:09:28Z" end-time="2024-05-03 10:09:28Z" duration="0.089184" total="1" passed="1" failed="0" inconclusive="0" skipped="0" asserts="0">
+            <properties />
+            <test-case id="1006" name="Attach_LoadedSceneNotInBuild" fullname="TestHelper.Editor.LoadSceneAttributeTest.Attach_LoadedSceneNotInBuild" methodname="Attach_LoadedSceneNotInBuild" classname="TestHelper.Editor.LoadSceneAttributeTest" runstate="Runnable" seed="818183097" result="Passed" start-time="2024-05-03 10:09:28Z" end-time="2024-05-03 10:09:28Z" duration="0.053176" asserts="0">
+              <properties>
+                <property name="retryIteration" value="0" />
+                <property name="repeatIteration" value="0" />
+              </properties>
+              <output><![CDATA[[Code Coverage] Code Coverage results for visited sequence points were saved in /github/workspace/CodeCoverage/UnityProject~-opencov/EditMode/TestCoverageResults_0002.xml
+]]></output>
+            </test-case>
+          </test-suite>
+          <test-suite type="TestFixture" id="1007" name="TemporaryBuildScenesUsingInTestTest" fullname="TestHelper.Editor.TemporaryBuildScenesUsingInTestTest" classname="TestHelper.Editor.TemporaryBuildScenesUsingInTestTest" runstate="Runnable" testcasecount="1" result="Passed" start-time="2024-05-03 10:09:28Z" end-time="2024-05-03 10:09:28Z" duration="0.055223" total="1" passed="1" failed="0" inconclusive="0" skipped="0" asserts="0">
+            <properties />
+            <test-case id="1008" name="GetScenesUsingInTest_AttachedToMethod_ReturnScenesSpecifiedByAttribute" fullname="TestHelper.Editor.TemporaryBuildScenesUsingInTestTest.GetScenesUsingInTest_AttachedToMethod_ReturnScenesSpecifiedByAttribute" methodname="GetScenesUsingInTest_AttachedToMethod_ReturnScenesSpecifiedByAttribute" classname="TestHelper.Editor.TemporaryBuildScenesUsingInTestTest" runstate="Runnable" seed="1243330322" result="Passed" start-time="2024-05-03 10:09:28Z" end-time="2024-05-03 10:09:28Z" duration="0.014506" asserts="0">
+              <properties>
+                <property name="retryIteration" value="0" />
+                <property name="repeatIteration" value="0" />
+              </properties>
+              <output><![CDATA[[Code Coverage] Code Coverage results for visited sequence points were saved in /github/workspace/CodeCoverage/UnityProject~-opencov/EditMode/TestCoverageResults_0003.xml
+[Code Coverage] Code Coverage Report was generated in /github/workspace/CodeCoverage/Report
+Included Assemblies: TestHelper,TestHelper.RuntimeInternals,TestHelper.Editor,
+Excluded Assemblies: *.Tests
+Included Paths: <Not specified>
+Excluded Paths: <Not specified>
+Saving results to: /github/workspace/artifacts/editmode-results.xml
+]]></output>
+            </test-case>
+          </test-suite>
+        </test-suite>
+      </test-suite>
+    </test-suite>
+  </test-suite>
+</test-run>
\ No newline at end of file
diff --git a/Tests/Editor/TestResources/nunit3.xml.meta b/Tests/Editor/TestResources/nunit3.xml.meta
new file mode 100644
index 0000000..acf9a28
--- /dev/null
+++ b/Tests/Editor/TestResources/nunit3.xml.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 0eaf0c82a5e0b4b4b95f815481936e47
+TextScriptImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Tests/RuntimeInternals/CommandLineArgsTest.cs b/Tests/RuntimeInternals/CommandLineArgsTest.cs
index 49e3351..f390855 100644
--- a/Tests/RuntimeInternals/CommandLineArgsTest.cs
+++ b/Tests/RuntimeInternals/CommandLineArgsTest.cs
@@ -39,5 +39,19 @@ public void GetScreenshotDirectory_WithoutArgument_GotDefaultDirectory()
             var actual = CommandLineArgs.GetScreenshotDirectory(Array.Empty<string>());
             Assert.That(actual, Is.EqualTo(Path.Combine(Application.persistentDataPath, "TestHelper", "Screenshots")));
         }
+
+        [Test]
+        public void GetJUnitResultsPath_WithArgument_GotSpecifiedPath()
+        {
+            var actual = CommandLineArgs.GetJUnitResultsPath(new[] { "-testHelperJUnitResults", "Test" });
+            Assert.That(actual, Is.EqualTo("Test"));
+        }
+
+        [Test]
+        public void GetJUnitResultsPath_WithoutArgument_ReturnsNull()
+        {
+            var actual = CommandLineArgs.GetJUnitResultsPath(Array.Empty<string>());
+            Assert.That(actual, Is.Null);
+        }
     }
 }

From 82bb8534970051b8e21f3c1e58c67afdde319a6b Mon Sep 17 00:00:00 2001
From: Koji Hasegawa <hasegawa@hubsys.co.jp>
Date: Sun, 27 Oct 2024 19:57:10 +0900
Subject: [PATCH 2/5] Fix fake input NUnit3 XML file format

---
 Editor/JUnitXmlWriter.cs                      |  1 +
 Tests/Editor/JUnitXmlWriterTest.cs            | 19 ++++++-------------
 .../TestDoubles/FakeTestResultAdaptor.cs      |  8 +++++++-
 Tests/Editor/TestResources/nunit3.xml         |  1 -
 4 files changed, 14 insertions(+), 15 deletions(-)

diff --git a/Editor/JUnitXmlWriter.cs b/Editor/JUnitXmlWriter.cs
index 605fa18..ab45363 100644
--- a/Editor/JUnitXmlWriter.cs
+++ b/Editor/JUnitXmlWriter.cs
@@ -20,6 +20,7 @@ public static void WriteTo(ITestResultAdaptor result, string path)
             var nunit3XmlStream = new MemoryStream();
             var nunit3Writer = XmlWriter.Create(nunit3XmlStream);
             result.ToXml().WriteTo(nunit3Writer);
+            nunit3XmlStream.Position = 0;
             var nunit3Xml = new XPathDocument(nunit3XmlStream);
 
             // Create output directory if it does not exist.
diff --git a/Tests/Editor/JUnitXmlWriterTest.cs b/Tests/Editor/JUnitXmlWriterTest.cs
index b66b957..5fdd6fa 100644
--- a/Tests/Editor/JUnitXmlWriterTest.cs
+++ b/Tests/Editor/JUnitXmlWriterTest.cs
@@ -11,11 +11,11 @@ namespace TestHelper.Editor
     public class JUnitXmlWriterTest
     {
         private const string TestResourcesPath = "Packages/com.nowsprinting.test-helper/Tests/Editor/TestResources";
-        private const string TestOutputDirectoryPath = "Logs/JUnitXmlWriterTest";
+        private const string TestOutputDirectoryPath = "Logs/TestHelper/JUnitXmlWriterTest";
         // Note: relative path from the project root directory.
 
         [Test, Order(0)]
-        public void WriteTo_DirectoryDoesNotExist_CreateDirectoryAndWriteToFile()
+        public void WriteTo_CreatedJUnitXmlFormatFile()
         {
             if (Directory.Exists(TestOutputDirectoryPath))
             {
@@ -27,20 +27,13 @@ public void WriteTo_DirectoryDoesNotExist_CreateDirectoryAndWriteToFile()
             var path = Path.Combine(TestOutputDirectoryPath, TestContext.CurrentContext.Test.Name + ".xml");
             JUnitXmlWriter.WriteTo(result, path);
 
-            Assert.That(path, Does.Exist);
-        }
-
-        [Test]
-        public void WriteTo_WriteToFile()
-        {
-            var nunitXmlPath = Path.Combine(TestResourcesPath, "nunit3.xml");
-            var result = new FakeTestResultAdaptor(nunitXmlPath);
-            var path = Path.Combine(TestOutputDirectoryPath, TestContext.CurrentContext.Test.Name + ".xml");
-            JUnitXmlWriter.WriteTo(result, path);
+            Assume.That(path, Does.Exist);
 
             var actual = File.ReadAllText(path);
-            var expected = File.ReadAllText(Path.Combine(TestResourcesPath, "junit.xml"));
+            var expected = File.ReadAllText(Path.Combine(TestResourcesPath, "junit.xml"));  // TODO: use XmlComparer
             Assert.That(actual, Is.EqualTo(expected));
         }
+
+        // TODO: overwrite test
     }
 }
diff --git a/Tests/Editor/TestDoubles/FakeTestResultAdaptor.cs b/Tests/Editor/TestDoubles/FakeTestResultAdaptor.cs
index 2f59f78..6285282 100644
--- a/Tests/Editor/TestDoubles/FakeTestResultAdaptor.cs
+++ b/Tests/Editor/TestDoubles/FakeTestResultAdaptor.cs
@@ -4,6 +4,8 @@
 using System;
 using System.Collections.Generic;
 using System.IO;
+using System.Xml.Linq;
+using NUnit.Framework;
 using NUnit.Framework.Interfaces;
 using UnityEditor.TestTools.TestRunner.Api;
 using TestStatus = UnityEditor.TestTools.TestRunner.Api.TestStatus;
@@ -20,7 +22,7 @@ public class FakeTestResultAdaptor : ITestResultAdaptor
         /// <summary>
         /// Constructor.
         /// </summary>
-        /// <param name="path">NUnit3 XML file path used in <c>ToXml</c> method.</param>
+        /// <param name="path">Fake input NUnit3 XML file. This file should not contain XML declaration; <c>TNode</c> does not expect it.</param>
         public FakeTestResultAdaptor(string path)
         {
             _path = path;
@@ -29,6 +31,10 @@ public FakeTestResultAdaptor(string path)
         public TNode ToXml()
         {
             var xmlText = File.ReadAllText(_path);
+            var xDocument = XDocument.Parse(xmlText);
+            Assume.That(xDocument.Declaration, Is.Null,
+                "The test input file should not contain XML declaration; TNode does not expect this.");
+
             return TNode.FromXml(xmlText);
         }
 
diff --git a/Tests/Editor/TestResources/nunit3.xml b/Tests/Editor/TestResources/nunit3.xml
index fd467eb..0e48b3d 100644
--- a/Tests/Editor/TestResources/nunit3.xml
+++ b/Tests/Editor/TestResources/nunit3.xml
@@ -1,4 +1,3 @@
-<?xml version="1.0" encoding="utf-8"?>
 <test-run id="2" testcasecount="3" result="Passed" total="3" passed="3" failed="0" inconclusive="0" skipped="0" asserts="0" engine-version="3.5.0.0" clr-version="4.0.30319.42000" start-time="2024-05-03 10:09:28Z" end-time="2024-05-03 10:09:28Z" duration="0.2538825">
   <test-suite type="TestSuite" id="1000" name="UnityProject~" fullname="UnityProject~" runstate="Runnable" testcasecount="3" result="Passed" start-time="2024-05-03 10:09:28Z" end-time="2024-05-03 10:09:28Z" duration="0.253883" total="3" passed="3" failed="0" inconclusive="0" skipped="0" asserts="0">
     <properties>

From ac602c976170f969b505bd8d18469a85579ba256 Mon Sep 17 00:00:00 2001
From: Koji Hasegawa <hasegawa@hubsys.co.jp>
Date: Mon, 28 Oct 2024 01:51:02 +0900
Subject: [PATCH 3/5] Fix JUnitXmlWriter

---
 Editor/JUnitXml.meta                          |   3 +
 .../JUnitXml/AbstractJUnitElementConverter.cs | 159 ++++++++++++++++++
 .../AbstractJUnitElementConverter.cs.meta     |   3 +
 .../JUnitXml/JUnitTestCaseElementConverter.cs |  65 +++++++
 .../JUnitTestCaseElementConverter.cs.meta     |   3 +
 .../JUnitTestSuiteElementConverter.cs         |  55 ++++++
 .../JUnitTestSuiteElementConverter.cs.meta    |   3 +
 .../JUnitTestSuitesElementConverter.cs        |  33 ++++
 .../JUnitTestSuitesElementConverter.cs.meta   |   3 +
 Editor/JUnitXml/JUnitXmlWriter.cs             |  78 +++++++++
 Editor/{ => JUnitXml}/JUnitXmlWriter.cs.meta  |   0
 Editor/JUnitXmlWriter.cs                      |  42 -----
 Editor/TestRunnerCallbacksImpl.cs             |   1 +
 Tests/Editor/JUnitXml.meta                    |   3 +
 .../{ => JUnitXml}/JUnitXmlWriterTest.cs      |   6 +-
 .../{ => JUnitXml}/JUnitXmlWriterTest.cs.meta |   0
 16 files changed, 412 insertions(+), 45 deletions(-)
 create mode 100644 Editor/JUnitXml.meta
 create mode 100644 Editor/JUnitXml/AbstractJUnitElementConverter.cs
 create mode 100644 Editor/JUnitXml/AbstractJUnitElementConverter.cs.meta
 create mode 100644 Editor/JUnitXml/JUnitTestCaseElementConverter.cs
 create mode 100644 Editor/JUnitXml/JUnitTestCaseElementConverter.cs.meta
 create mode 100644 Editor/JUnitXml/JUnitTestSuiteElementConverter.cs
 create mode 100644 Editor/JUnitXml/JUnitTestSuiteElementConverter.cs.meta
 create mode 100644 Editor/JUnitXml/JUnitTestSuitesElementConverter.cs
 create mode 100644 Editor/JUnitXml/JUnitTestSuitesElementConverter.cs.meta
 create mode 100644 Editor/JUnitXml/JUnitXmlWriter.cs
 rename Editor/{ => JUnitXml}/JUnitXmlWriter.cs.meta (100%)
 delete mode 100644 Editor/JUnitXmlWriter.cs
 create mode 100644 Tests/Editor/JUnitXml.meta
 rename Tests/Editor/{ => JUnitXml}/JUnitXmlWriterTest.cs (89%)
 rename Tests/Editor/{ => JUnitXml}/JUnitXmlWriterTest.cs.meta (100%)

diff --git a/Editor/JUnitXml.meta b/Editor/JUnitXml.meta
new file mode 100644
index 0000000..d16ad23
--- /dev/null
+++ b/Editor/JUnitXml.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 323defc91e624fd7937fe4473c0f009a
+timeCreated: 1730050270
\ No newline at end of file
diff --git a/Editor/JUnitXml/AbstractJUnitElementConverter.cs b/Editor/JUnitXml/AbstractJUnitElementConverter.cs
new file mode 100644
index 0000000..b18b3e8
--- /dev/null
+++ b/Editor/JUnitXml/AbstractJUnitElementConverter.cs
@@ -0,0 +1,159 @@
+// Copyright (c) 2023-2024 Koji Hasegawa.
+// This software is released under the MIT License.
+
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Xml.Linq;
+using NUnit.Framework.Interfaces;
+
+namespace TestHelper.Editor.JUnitXml
+{
+    /// <summary>
+    /// Abstract class of converting element of NUnit3 test result to JUnit XML format (legacy) element.
+    /// </summary>
+    [SuppressMessage("ReSharper", "ConvertToAutoProperty")]
+    [SuppressMessage("ReSharper", "ConvertToAutoPropertyWhenPossible")]
+    internal abstract class AbstractJUnitElementConverter
+    {
+        // JUnit XML format (legacy) element/attribute names.
+        protected const string JUnitElementTestsuites = "testsuites";
+        protected const string JUnitElementTestsuite = "testsuite";
+        protected const string JUnitElementProperties = "properties";
+        protected const string JUnitElementProperty = "property";
+        protected const string JUnitElementSystemOut = "system-out";
+        protected const string JUnitElementTestcase = "testcase";
+        protected const string JUnitElementFailure = "failure";
+        protected const string JUnitAttributeName = "name";
+        protected const string JUnitAttributeDisabled = "disabled";
+        protected const string JUnitAttributeErrors = "errors";
+        protected const string JUnitAttributeFailures = "failures";
+        protected const string JUnitAttributeTests = "tests";
+        protected const string JUnitAttributeTime = "time";
+        protected const string JUnitAttributeID = "id";
+        protected const string JUnitAttributeSkipped = "skipped";
+        protected const string JUnitAttributeTimestamp = "timestamp";
+        protected const string JUnitAttributeValue = "value";
+        protected const string JUnitAttributeClassname = "classname";
+        protected const string JUnitAttributeAssertions = "assertions";
+        protected const string JUnitAttributeStatus = "status";
+        protected const string JUnitAttributeMessage = "message";
+        protected const string JUnitAttributeType = "type";
+
+        // JUnit XML format (legacy) element attributes and elements.
+        protected string ID => _id;
+        protected string Name => _name;
+        protected string ClassName => _classname;
+        protected int Disabled => _skipped;
+        protected int Skipped => _skipped;
+        protected static int Errors => 0;
+        protected int Failures => _failed + _inconclusive;
+        protected int Tests => _passed + _failed + _inconclusive + _skipped;
+        protected int Assertions => _asserts;
+        protected double Time => _duration; // seconds
+        protected string Timestamp => _starttime;
+        protected string Status => _result;
+        protected bool IsTestCaseSkipped => _result == "Skipped"; // Note: test-case has not skipped attribute
+        protected List<( string, string)> Properties => _properties;
+        protected string Reason => _reason;
+        protected (string, string) Failure => _failure; // message, stack-trace
+        protected string SystemOut => _output;
+
+        // NUnit3 test result element names.
+        internal const string NUnitTestRun = "test-run";
+        internal const string NUnitTestSuite = "test-suite";
+        internal const string NUnitTestCase = "test-case";
+
+        // NUnit3 test result element attributes and elements.
+        private readonly string _id;
+        private readonly string _name;
+        private readonly string _classname;
+        private readonly string _result;
+        private readonly int _passed;
+        private readonly int _failed;
+        private readonly int _inconclusive;
+        private readonly int _skipped;
+        private readonly int _asserts;
+        private readonly string _starttime;
+        private readonly double _duration; // seconds
+        private readonly List<( string, string)> _properties = new List<(string, string)>(); // name, value
+        private string _reason;
+        private (string, string) _failure; // message, stack-trace
+        private string _output;
+
+        /// <summary>
+        /// Constructor.
+        /// Parse NUnit3 test result elements and store them.
+        /// </summary>
+        /// <param name="node">NUnit3 test result elements to be converted.</param>
+        protected AbstractJUnitElementConverter(TNode node)
+        {
+            this._id = node.Attributes["id"];
+            this._result = node.Attributes["result"];
+            this._asserts = System.Convert.ToInt32(node.Attributes["asserts"]);
+            this._starttime = node.Attributes["start-time"];
+            this._duration = System.Convert.ToDouble(node.Attributes["duration"]);
+
+            switch (node.Name)
+            {
+                case NUnitTestRun:
+                case NUnitTestSuite:
+                    this._passed = System.Convert.ToInt32(node.Attributes["passed"]);
+                    this._failed = System.Convert.ToInt32(node.Attributes["failed"]);
+                    this._inconclusive = System.Convert.ToInt32(node.Attributes["inconclusive"]);
+                    this._skipped = System.Convert.ToInt32(node.Attributes["skipped"]);
+                    break;
+                case NUnitTestCase:
+                    this._passed = 0;
+                    this._failed = 0;
+                    this._inconclusive = 0;
+                    this._skipped = 0;
+                    break;
+            }
+
+            switch (node.Name)
+            {
+                case NUnitTestSuite:
+                case NUnitTestCase:
+                    this._name = node.Attributes["name"];
+                    this._classname = node.Attributes["classname"];
+
+                    node.ChildNodes.ForEach(child =>
+                    {
+                        if (child.Name == "properties")
+                        {
+                            child.ChildNodes.ForEach(property =>
+                            {
+                                this._properties.Add((property.Attributes["name"], property.Attributes["value"]));
+                            });
+                        }
+
+                        if (child.Name == "reason")
+                        {
+                            this._reason = (child.Attributes["message"]);
+                        }
+
+                        if (child.Name == "failure")
+                        {
+                            this._failure = (child.Attributes["message"], child.Attributes["stack-trace"]);
+                        }
+
+                        if (child.Name == "output")
+                        {
+                            this._output = child.Value;
+                        }
+                    });
+                    break;
+                case NUnitTestRun:
+                    this._name = string.Empty;
+                    this._classname = string.Empty;
+                    break;
+            }
+        }
+
+        /// <summary>
+        /// Convert to JUnit XML format (legacy) element.
+        /// </summary>
+        /// <returns></returns>
+        public abstract XElement ToJUnitElement();
+    }
+}
diff --git a/Editor/JUnitXml/AbstractJUnitElementConverter.cs.meta b/Editor/JUnitXml/AbstractJUnitElementConverter.cs.meta
new file mode 100644
index 0000000..0c91607
--- /dev/null
+++ b/Editor/JUnitXml/AbstractJUnitElementConverter.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 068db053ac1c4d97ba1c1afbbe22c634
+timeCreated: 1730050312
\ No newline at end of file
diff --git a/Editor/JUnitXml/JUnitTestCaseElementConverter.cs b/Editor/JUnitXml/JUnitTestCaseElementConverter.cs
new file mode 100644
index 0000000..a49579e
--- /dev/null
+++ b/Editor/JUnitXml/JUnitTestCaseElementConverter.cs
@@ -0,0 +1,65 @@
+// Copyright (c) 2023-2024 Koji Hasegawa.
+// This software is released under the MIT License.
+
+using System.Xml.Linq;
+using NUnit.Framework.Interfaces;
+
+namespace TestHelper.Editor.JUnitXml
+{
+    /// <summary>
+    /// Converter class of NUnit3 "test-case" element to JUnit "testcase" element.
+    /// </summary>
+    internal class JUnitTestCaseElementConverter : AbstractJUnitElementConverter
+    {
+        /// <inheritdoc/>
+        public JUnitTestCaseElementConverter(TNode node) : base(node)
+        {
+        }
+
+        /// <inheritdoc/>
+        public override XElement ToJUnitElement()
+        {
+            var element = new XElement(JUnitElementTestcase);
+            element.Add(new XAttribute(JUnitAttributeName, Name));
+            element.Add(new XAttribute(JUnitAttributeClassname, ClassName));
+            element.Add(new XAttribute(JUnitAttributeAssertions, Assertions));
+            element.Add(new XAttribute(JUnitAttributeTime, Time));
+            element.Add(new XAttribute(JUnitAttributeStatus, Status));
+
+            if (Properties.Count > 0)
+            {
+                var propertiesElement = new XElement(JUnitElementProperties);
+                foreach (var property in Properties)
+                {
+                    var propertyElement = new XElement(JUnitElementProperty);
+                    propertyElement.Add(new XAttribute(JUnitAttributeName, property.Item1));
+                    propertyElement.Add(new XAttribute(JUnitAttributeValue, property.Item2));
+                    propertiesElement.Add(propertyElement);
+                }
+
+                element.Add(propertiesElement);
+            }
+
+            if (IsTestCaseSkipped)
+            {
+                var skippedNode = new XElement(JUnitAttributeSkipped, Reason);
+                element.Add(skippedNode);
+            }
+
+            if (Failures > 0)
+            {
+                var failure = new XElement(JUnitElementFailure);
+                failure.Add(new XAttribute(JUnitAttributeMessage, Failure.Item1));
+                failure.Add(new XAttribute(JUnitAttributeType, string.Empty));
+                element.Add(failure);
+            }
+
+            if (string.IsNullOrEmpty(SystemOut))
+            {
+                element.Add(new XElement(JUnitElementSystemOut, SystemOut));
+            }
+
+            return element;
+        }
+    }
+}
diff --git a/Editor/JUnitXml/JUnitTestCaseElementConverter.cs.meta b/Editor/JUnitXml/JUnitTestCaseElementConverter.cs.meta
new file mode 100644
index 0000000..568374e
--- /dev/null
+++ b/Editor/JUnitXml/JUnitTestCaseElementConverter.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: e3a78e94958143029d6556403c6ec7a0
+timeCreated: 1730050666
\ No newline at end of file
diff --git a/Editor/JUnitXml/JUnitTestSuiteElementConverter.cs b/Editor/JUnitXml/JUnitTestSuiteElementConverter.cs
new file mode 100644
index 0000000..7f19238
--- /dev/null
+++ b/Editor/JUnitXml/JUnitTestSuiteElementConverter.cs
@@ -0,0 +1,55 @@
+// Copyright (c) 2023-2024 Koji Hasegawa.
+// This software is released under the MIT License.
+
+using System.Xml.Linq;
+using NUnit.Framework.Interfaces;
+
+namespace TestHelper.Editor.JUnitXml
+{
+    /// <summary>
+    /// Converter class of NUnit3 "test-suite" element to JUnit "testsuite" element.
+    /// </summary>
+    internal class JUnitTestSuiteElementConverter : AbstractJUnitElementConverter
+    {
+        /// <inheritdoc/>
+        public JUnitTestSuiteElementConverter(TNode node) : base(node)
+        {
+        }
+
+        /// <inheritdoc/>
+        public override XElement ToJUnitElement()
+        {
+            var element = new XElement(JUnitElementTestsuite);
+            element.Add(new XAttribute(JUnitAttributeName, Name));
+            element.Add(new XAttribute(JUnitAttributeTests, Tests));
+            element.Add(new XAttribute(JUnitAttributeID, ID));
+            element.Add(new XAttribute(JUnitAttributeDisabled, Disabled));
+            element.Add(new XAttribute(JUnitAttributeErrors, Errors));
+            element.Add(new XAttribute(JUnitAttributeFailures, Failures));
+            element.Add(new XAttribute(JUnitAttributeSkipped, Skipped));
+            element.Add(new XAttribute(JUnitAttributeTime, Time));
+            element.Add(new XAttribute(JUnitAttributeTimestamp, Timestamp));
+
+            if (Properties.Count > 0)
+            {
+                var propertiesElement = new XElement(JUnitElementProperties);
+                foreach (var property in Properties)
+                {
+                    var propertyElement = new XElement(JUnitElementProperty);
+                    propertyElement.Add(new XAttribute(JUnitAttributeName, property.Item1));
+                    propertyElement.Add(new XAttribute(JUnitAttributeValue, property.Item2));
+                    propertiesElement.Add(propertyElement);
+                }
+
+                element.Add(propertiesElement);
+            }
+
+            if (string.IsNullOrEmpty(SystemOut))
+            {
+                element.Add(new XElement(JUnitElementSystemOut, SystemOut));
+            }
+
+            return element;
+        }
+    }
+}
diff --git a/Editor/JUnitXml/JUnitTestSuiteElementConverter.cs.meta b/Editor/JUnitXml/JUnitTestSuiteElementConverter.cs.meta
new file mode 100644
index 0000000..68b5bbb
--- /dev/null
+++ b/Editor/JUnitXml/JUnitTestSuiteElementConverter.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: c08508b34cab4243b39056af0a6324bd
+timeCreated: 1730050583
\ No newline at end of file
diff --git a/Editor/JUnitXml/JUnitTestSuitesElementConverter.cs b/Editor/JUnitXml/JUnitTestSuitesElementConverter.cs
new file mode 100644
index 0000000..e17c2c8
--- /dev/null
+++ b/Editor/JUnitXml/JUnitTestSuitesElementConverter.cs
@@ -0,0 +1,33 @@
+// Copyright (c) 2023-2024 Koji Hasegawa.
+// This software is released under the MIT License.
+
+using System.Xml.Linq;
+using NUnit.Framework.Interfaces;
+
+namespace TestHelper.Editor.JUnitXml
+{
+    /// <summary>
+    /// Converter class of NUnit3 "test-run" element to JUnit "testsuites" element.
+    /// </summary>
+    internal class JUnitTestSuitesElementConverter : AbstractJUnitElementConverter
+    {
+        /// <inheritdoc/>
+        public JUnitTestSuitesElementConverter(TNode node) : base(node)
+        {
+        }
+
+        /// <inheritdoc/>
+        public override XElement ToJUnitElement()
+        {
+            var element = new XElement(JUnitElementTestsuites);
+            element.Add(new XAttribute(JUnitAttributeName, Name));
+            element.Add(new XAttribute(JUnitAttributeDisabled, Disabled));
+            element.Add(new XAttribute(JUnitAttributeErrors, Errors));
+            element.Add(new XAttribute(JUnitAttributeFailures, Failures));
+            element.Add(new XAttribute(JUnitAttributeTests, Tests));
+            element.Add(new XAttribute(JUnitAttributeTime, Time));
+
+            return element;
+        }
+    }
+}
diff --git a/Editor/JUnitXml/JUnitTestSuitesElementConverter.cs.meta b/Editor/JUnitXml/JUnitTestSuitesElementConverter.cs.meta
new file mode 100644
index 0000000..ffb573b
--- /dev/null
+++ b/Editor/JUnitXml/JUnitTestSuitesElementConverter.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: cde60a2b46934cf2a5bf718b6c452ea5
+timeCreated: 1730050521
\ No newline at end of file
diff --git a/Editor/JUnitXml/JUnitXmlWriter.cs b/Editor/JUnitXml/JUnitXmlWriter.cs
new file mode 100644
index 0000000..0ea9e5f
--- /dev/null
+++ b/Editor/JUnitXml/JUnitXmlWriter.cs
@@ -0,0 +1,78 @@
+// Copyright (c) 2023-2024 Koji Hasegawa.
+// This software is released under the MIT License.
+
+using System;
+using System.IO;
+using System.Xml;
+using System.Xml.Linq;
+using NUnit.Framework.Interfaces;
+using UnityEditor.TestTools.TestRunner.Api;
+
+namespace TestHelper.Editor.JUnitXml
+{
+    /// <summary>
+    /// Convert NUnit3 test result to JUnit XML format (legacy) and write it to a file.
+    /// </summary>
+    /// <remarks>
+    /// Not supported "Open Test Reporting format" introduced in JUnit 5.
+    /// </remarks>
+    /// <seealso href="https://docs.nunit.org/articles/nunit/technical-notes/usage/Test-Result-XML-Format.html"/>
+    /// <seealso href="https://github.com/jenkinsci/benchmark-plugin/blob/master/doc/JUnit%20format/JUnit.txt"/>
+    public static class JUnitXmlWriter
+    {
+        public static void WriteTo(ITestResultAdaptor result, string path)
+        {
+            // Convert NUnit3 XML to JUnit XML.
+            var junitDocument = new XDocument { Declaration = new XDeclaration("1.0", "utf-8", null) };
+            var junitRoot = Convert(result.ToXml());
+            junitDocument.Add(junitRoot);
+
+            // Create output directory if it does not exist.
+            var directory = Path.GetDirectoryName(path);
+            if (directory != null && !Directory.Exists(directory))
+            {
+                Directory.CreateDirectory(directory);
+            }
+
+            // Output JUnit XML to the file.
+            using (var writer = XmlWriter.Create(path))
+            {
+                junitDocument.WriteTo(writer);
+                writer.Flush();
+            }
+        }
+
+        private static XElement Convert(TNode node)
+        {
+            AbstractJUnitElementConverter converter;
+            switch (node.Name)
+            {
+                case AbstractJUnitElementConverter.NUnitTestRun:
+                    converter = new JUnitTestSuitesElementConverter(node);
+                    break;
+                case AbstractJUnitElementConverter.NUnitTestSuite:
+                    converter = new JUnitTestSuiteElementConverter(node);
+                    break;
+                case AbstractJUnitElementConverter.NUnitTestCase:
+                    converter = new JUnitTestCaseElementConverter(node);
+                    break;
+                default:
+                    throw new ArgumentException($"Unsupported node name: {node.Name}");
+            }
+
+            var junitElement = converter.ToJUnitElement();
+
+            // Recursively convert child nodes.
+            node.ChildNodes.ForEach(child =>
+            {
+                if (child.Name == AbstractJUnitElementConverter.NUnitTestSuite ||
+                    child.Name == AbstractJUnitElementConverter.NUnitTestCase)
+                {
+                    junitElement.Add(Convert(child));
+                }
+            });
+
+            return junitElement;
+        }
+    }
+}
diff --git a/Editor/JUnitXmlWriter.cs.meta b/Editor/JUnitXml/JUnitXmlWriter.cs.meta
similarity index 100%
rename from Editor/JUnitXmlWriter.cs.meta
rename to Editor/JUnitXml/JUnitXmlWriter.cs.meta
diff --git a/Editor/JUnitXmlWriter.cs b/Editor/JUnitXmlWriter.cs
deleted file mode 100644
index ab45363..0000000
--- a/Editor/JUnitXmlWriter.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (c) 2023-2024 Koji Hasegawa.
-// This software is released under the MIT License.
-
-using System.IO;
-using System.Xml;
-using System.Xml.XPath;
-using System.Xml.Xsl;
-using UnityEditor.TestTools.TestRunner.Api;
-
-namespace TestHelper.Editor
-{
-    public static class JUnitXmlWriter
-    {
-        private const string XsltPath = "Packages/com.nowsprinting.test-helper/Editor/nunit3-junit/nunit3-junit.xslt";
-        // Note: This XSLT file is copied from https://github.com/nunit/nunit-transforms/tree/master/nunit3-junit
-
-        public static void WriteTo(ITestResultAdaptor result, string path)
-        {
-            // Input
-            var nunit3XmlStream = new MemoryStream();
-            var nunit3Writer = XmlWriter.Create(nunit3XmlStream);
-            result.ToXml().WriteTo(nunit3Writer);
-            nunit3XmlStream.Position = 0;
-            var nunit3Xml = new XPathDocument(nunit3XmlStream);
-
-            // Create output directory if it does not exist.
-            var directory = Path.GetDirectoryName(path);
-            if (directory != null && !Directory.Exists(directory))
-            {
-                Directory.CreateDirectory(directory);
-            }
-
-            // Output (JUnit XML)
-            var writer = XmlWriter.Create(path);
-
-            // Execute the transformation.
-            var transformer = new XslCompiledTransform();
-            transformer.Load(XsltPath);
-            transformer.Transform(nunit3Xml, writer);
-        }
-    }
-}
diff --git a/Editor/TestRunnerCallbacksImpl.cs b/Editor/TestRunnerCallbacksImpl.cs
index a258882..5009c4e 100644
--- a/Editor/TestRunnerCallbacksImpl.cs
+++ b/Editor/TestRunnerCallbacksImpl.cs
@@ -1,6 +1,7 @@
 // Copyright (c) 2023-2024 Koji Hasegawa.
 // This software is released under the MIT License.
 
+using TestHelper.Editor.JUnitXml;
 using TestHelper.RuntimeInternals;
 using UnityEditor;
 using UnityEditor.TestTools.TestRunner.Api;
diff --git a/Tests/Editor/JUnitXml.meta b/Tests/Editor/JUnitXml.meta
new file mode 100644
index 0000000..4ae04d2
--- /dev/null
+++ b/Tests/Editor/JUnitXml.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 9aedd21ecb7d4a37a4ea7117ed07831a
+timeCreated: 1730058413
\ No newline at end of file
diff --git a/Tests/Editor/JUnitXmlWriterTest.cs b/Tests/Editor/JUnitXml/JUnitXmlWriterTest.cs
similarity index 89%
rename from Tests/Editor/JUnitXmlWriterTest.cs
rename to Tests/Editor/JUnitXml/JUnitXmlWriterTest.cs
index 5fdd6fa..b1db9fb 100644
--- a/Tests/Editor/JUnitXmlWriterTest.cs
+++ b/Tests/Editor/JUnitXml/JUnitXmlWriterTest.cs
@@ -5,7 +5,7 @@
 using NUnit.Framework;
 using TestHelper.Editor.TestDoubles;
 
-namespace TestHelper.Editor
+namespace TestHelper.Editor.JUnitXml
 {
     [TestFixture]
     public class JUnitXmlWriterTest
@@ -25,12 +25,12 @@ public void WriteTo_CreatedJUnitXmlFormatFile()
             var nunitXmlPath = Path.Combine(TestResourcesPath, "nunit3.xml");
             var result = new FakeTestResultAdaptor(nunitXmlPath);
             var path = Path.Combine(TestOutputDirectoryPath, TestContext.CurrentContext.Test.Name + ".xml");
-            JUnitXmlWriter.WriteTo(result, path);
+            JUnitXml.JUnitXmlWriter.WriteTo(result, path);
 
             Assume.That(path, Does.Exist);
 
             var actual = File.ReadAllText(path);
-            var expected = File.ReadAllText(Path.Combine(TestResourcesPath, "junit.xml"));  // TODO: use XmlComparer
+            var expected = File.ReadAllText(Path.Combine(TestResourcesPath, "junit.xml")); // TODO: use XmlComparer
             Assert.That(actual, Is.EqualTo(expected));
         }
 
diff --git a/Tests/Editor/JUnitXmlWriterTest.cs.meta b/Tests/Editor/JUnitXml/JUnitXmlWriterTest.cs.meta
similarity index 100%
rename from Tests/Editor/JUnitXmlWriterTest.cs.meta
rename to Tests/Editor/JUnitXml/JUnitXmlWriterTest.cs.meta

From 47857b187e07392a0b1df9abd04315ef08528b4a Mon Sep 17 00:00:00 2001
From: Koji Hasegawa <hasegawa@hubsys.co.jp>
Date: Mon, 28 Oct 2024 05:57:03 +0900
Subject: [PATCH 4/5] Fix tests

---
 .../JUnitXml/AbstractJUnitElementConverter.cs |  27 +-
 .../JUnitXml/JUnitTestCaseElementConverter.cs |  16 +-
 .../JUnitTestSuiteElementConverter.cs         |   6 +-
 .../JUnitTestSuitesElementConverter.cs        |   2 +-
 Tests/Editor/JUnitXml/JUnitXmlWriterTest.cs   |  24 +-
 Tests/Editor/TestResources/junit.xml          | 154 +++++++-
 Tests/Editor/TestResources/nunit3.xml         | 339 +++++++++++++++---
 7 files changed, 487 insertions(+), 81 deletions(-)

diff --git a/Editor/JUnitXml/AbstractJUnitElementConverter.cs b/Editor/JUnitXml/AbstractJUnitElementConverter.cs
index b18b3e8..d22748d 100644
--- a/Editor/JUnitXml/AbstractJUnitElementConverter.cs
+++ b/Editor/JUnitXml/AbstractJUnitElementConverter.cs
@@ -52,7 +52,9 @@ internal abstract class AbstractJUnitElementConverter
         protected double Time => _duration; // seconds
         protected string Timestamp => _starttime;
         protected string Status => _result;
-        protected bool IsTestCaseSkipped => _result == "Skipped"; // Note: test-case has not skipped attribute
+        protected bool IsTestCaseSkipped => _result == "Skipped"; // test-case has not skipped attribute
+        protected bool IsTestCaseFailed => _result == "Failed"; // test-case has not failures attribute
+        protected bool IsTestCaseInconclusive => _result == "Inconclusive"; // test-case has not inconclusive attribute
         protected List<( string, string)> Properties => _properties;
         protected string Reason => _reason;
         protected (string, string) Failure => _failure; // message, stack-trace
@@ -129,17 +131,34 @@ protected AbstractJUnitElementConverter(TNode node)
 
                         if (child.Name == "reason")
                         {
-                            this._reason = (child.Attributes["message"]);
+                            child.ChildNodes.ForEach(grandchild =>
+                            {
+                                if (grandchild.Name == "message")
+                                {
+                                    this._reason = grandchild.Value.Trim();
+                                }
+                            });
                         }
 
                         if (child.Name == "failure")
                         {
-                            this._failure = (child.Attributes["message"], child.Attributes["stack-trace"]);
+                            child.ChildNodes.ForEach(grandchild =>
+                            {
+                                switch (grandchild.Name)
+                                {
+                                    case "message":
+                                        this._failure.Item1 = grandchild.Value.Trim();
+                                        break;
+                                    case "stack-trace":
+                                        this._failure.Item2 = grandchild.Value.Trim();
+                                        break;
+                                }
+                            });
                         }
 
                         if (child.Name == "output")
                         {
-                            this._output = child.Value;
+                            this._output = child.Value.Trim();
                         }
                     });
                     break;
diff --git a/Editor/JUnitXml/JUnitTestCaseElementConverter.cs b/Editor/JUnitXml/JUnitTestCaseElementConverter.cs
index a49579e..23186c6 100644
--- a/Editor/JUnitXml/JUnitTestCaseElementConverter.cs
+++ b/Editor/JUnitXml/JUnitTestCaseElementConverter.cs
@@ -22,7 +22,7 @@ public override XElement ToJUnitElement()
             var element = new XElement(JUnitElementTestcase);
             element.Add(new XAttribute(JUnitAttributeName, Name));
             element.Add(new XAttribute(JUnitAttributeClassname, ClassName));
-            element.Add(new XAttribute(JUnitAttributeAssertions, Assertions));
+            element.Add(new XAttribute(JUnitAttributeAssertions, Assertions)); // always 0
             element.Add(new XAttribute(JUnitAttributeTime, Time));
             element.Add(new XAttribute(JUnitAttributeStatus, Status));
 
@@ -46,7 +46,7 @@ public override XElement ToJUnitElement()
                 element.Add(skippedNode);
             }
 
-            if (Failures > 0)
+            if (IsTestCaseFailed)
             {
                 var failure = new XElement(JUnitElementFailure);
                 failure.Add(new XAttribute(JUnitAttributeMessage, Failure.Item1));
@@ -54,9 +54,17 @@ public override XElement ToJUnitElement()
                 element.Add(failure);
             }
 
-            if (string.IsNullOrEmpty(SystemOut))
+            if (IsTestCaseInconclusive)
             {
-                element.Add(new XElement(JUnitElementSystemOut, SystemOut));
+                var failure = new XElement(JUnitElementFailure);
+                failure.Add(new XAttribute(JUnitAttributeMessage, Reason));
+                failure.Add(new XAttribute(JUnitAttributeType, string.Empty));
+                element.Add(failure);
+            }
+
+            if (!string.IsNullOrEmpty(SystemOut))
+            {
+                element.Add(new XElement(JUnitElementSystemOut, new XCData(SystemOut)));
             }
 
             return element;
diff --git a/Editor/JUnitXml/JUnitTestSuiteElementConverter.cs b/Editor/JUnitXml/JUnitTestSuiteElementConverter.cs
index 7f19238..de3a9d5 100644
--- a/Editor/JUnitXml/JUnitTestSuiteElementConverter.cs
+++ b/Editor/JUnitXml/JUnitTestSuiteElementConverter.cs
@@ -24,7 +24,7 @@ public override XElement ToJUnitElement()
             element.Add(new XAttribute(JUnitAttributeTests, Tests));
             element.Add(new XAttribute(JUnitAttributeID, ID));
             element.Add(new XAttribute(JUnitAttributeDisabled, Disabled));
-            element.Add(new XAttribute(JUnitAttributeErrors, Errors));
+            element.Add(new XAttribute(JUnitAttributeErrors, Errors));  // always 0
             element.Add(new XAttribute(JUnitAttributeFailures, Failures));
             element.Add(new XAttribute(JUnitAttributeSkipped, Skipped));
             element.Add(new XAttribute(JUnitAttributeTime, Time));
@@ -44,9 +44,9 @@ public override XElement ToJUnitElement()
                 element.Add(propertiesElement);
             }
 
-            if (string.IsNullOrEmpty(SystemOut))
+            if (!string.IsNullOrEmpty(SystemOut))
             {
-                element.Add(new XElement(JUnitElementSystemOut, SystemOut));
+                element.Add(new XElement(JUnitElementSystemOut, new XCData(SystemOut)));
             }
 
             return element;
diff --git a/Editor/JUnitXml/JUnitTestSuitesElementConverter.cs b/Editor/JUnitXml/JUnitTestSuitesElementConverter.cs
index e17c2c8..c8e42b2 100644
--- a/Editor/JUnitXml/JUnitTestSuitesElementConverter.cs
+++ b/Editor/JUnitXml/JUnitTestSuitesElementConverter.cs
@@ -22,7 +22,7 @@ public override XElement ToJUnitElement()
             var element = new XElement(JUnitElementTestsuites);
             element.Add(new XAttribute(JUnitAttributeName, Name));
             element.Add(new XAttribute(JUnitAttributeDisabled, Disabled));
-            element.Add(new XAttribute(JUnitAttributeErrors, Errors));
+            element.Add(new XAttribute(JUnitAttributeErrors, Errors)); // always 0
             element.Add(new XAttribute(JUnitAttributeFailures, Failures));
             element.Add(new XAttribute(JUnitAttributeTests, Tests));
             element.Add(new XAttribute(JUnitAttributeTime, Time));
diff --git a/Tests/Editor/JUnitXml/JUnitXmlWriterTest.cs b/Tests/Editor/JUnitXml/JUnitXmlWriterTest.cs
index b1db9fb..d614de4 100644
--- a/Tests/Editor/JUnitXml/JUnitXmlWriterTest.cs
+++ b/Tests/Editor/JUnitXml/JUnitXmlWriterTest.cs
@@ -3,6 +3,7 @@
 
 using System.IO;
 using NUnit.Framework;
+using TestHelper.Comparers;
 using TestHelper.Editor.TestDoubles;
 
 namespace TestHelper.Editor.JUnitXml
@@ -25,15 +26,30 @@ public void WriteTo_CreatedJUnitXmlFormatFile()
             var nunitXmlPath = Path.Combine(TestResourcesPath, "nunit3.xml");
             var result = new FakeTestResultAdaptor(nunitXmlPath);
             var path = Path.Combine(TestOutputDirectoryPath, TestContext.CurrentContext.Test.Name + ".xml");
-            JUnitXml.JUnitXmlWriter.WriteTo(result, path);
+            JUnitXmlWriter.WriteTo(result, path);
 
             Assume.That(path, Does.Exist);
 
             var actual = File.ReadAllText(path);
-            var expected = File.ReadAllText(Path.Combine(TestResourcesPath, "junit.xml")); // TODO: use XmlComparer
-            Assert.That(actual, Is.EqualTo(expected));
+            var expected = File.ReadAllText(Path.Combine(TestResourcesPath, "junit.xml"));
+            Assert.That(actual, Is.EqualTo(expected).Using(new XmlComparer()));
         }
 
-        // TODO: overwrite test
+        [Test]
+        public void WriteTo_ExistFile_OverwriteFile()
+        {
+            var nunitXmlPath = Path.Combine(TestResourcesPath, "nunit3.xml");
+            var result = new FakeTestResultAdaptor(nunitXmlPath);
+            var path = Path.Combine(TestOutputDirectoryPath, TestContext.CurrentContext.Test.Name + ".xml");
+
+            // Destroy the output destination file.
+            File.Copy(nunitXmlPath, path, true);
+
+            JUnitXmlWriter.WriteTo(result, path);
+
+            var actual = File.ReadAllText(path);
+            var expected = File.ReadAllText(Path.Combine(TestResourcesPath, "junit.xml"));
+            Assert.That(actual, Is.EqualTo(expected).Using(new XmlComparer()));
+        }
     }
 }
diff --git a/Tests/Editor/TestResources/junit.xml b/Tests/Editor/TestResources/junit.xml
index 5749df1..099508d 100644
--- a/Tests/Editor/TestResources/junit.xml
+++ b/Tests/Editor/TestResources/junit.xml
@@ -1,10 +1,144 @@
-<?xml version="1.0" encoding="utf-8"?><testsuites tests="3" failures="0" disabled="0" time="0.2538825"><testsuite tests="1" time="0.092622" errors="0" failures="0" skipped="0" timestamp="2024-05-03 10:09:28Z" name="UnityProject~.TestHelper.Editor."><testcase name="Attach_CreateNewSceneWithCameraAndLight" assertions="0" time="0.036802" status="Passed" classname="TestHelper.Editor.CreateSceneAttributeTest"><system-out>[Code Coverage] Code Coverage results for visited sequence points were saved in /github/workspace/CodeCoverage/UnityProject~-opencov/EditMode/TestCoverageResults_0001.xml
-</system-out></testcase></testsuite><testsuite tests="1" time="0.089184" errors="0" failures="0" skipped="0" timestamp="2024-05-03 10:09:28Z" name="UnityProject~.TestHelper.Editor."><testcase name="Attach_LoadedSceneNotInBuild" assertions="0" time="0.053176" status="Passed" classname="TestHelper.Editor.LoadSceneAttributeTest"><system-out>[Code Coverage] Code Coverage results for visited sequence points were saved in /github/workspace/CodeCoverage/UnityProject~-opencov/EditMode/TestCoverageResults_0002.xml
-</system-out></testcase></testsuite><testsuite tests="1" time="0.055223" errors="0" failures="0" skipped="0" timestamp="2024-05-03 10:09:28Z" name="UnityProject~.TestHelper.Editor."><testcase name="GetScenesUsingInTest_AttachedToMethod_ReturnScenesSpecifiedByAttribute" assertions="0" time="0.014506" status="Passed" classname="TestHelper.Editor.TemporaryBuildScenesUsingInTestTest"><system-out>[Code Coverage] Code Coverage results for visited sequence points were saved in /github/workspace/CodeCoverage/UnityProject~-opencov/EditMode/TestCoverageResults_0003.xml
-[Code Coverage] Code Coverage Report was generated in /github/workspace/CodeCoverage/Report
-Included Assemblies: TestHelper,TestHelper.RuntimeInternals,TestHelper.Editor,
-Excluded Assemblies: *.Tests
-Included Paths: &lt;Not specified&gt;
-Excluded Paths: &lt;Not specified&gt;
-Saving results to: /github/workspace/artifacts/editmode-results.xml
-</system-out></testcase></testsuite></testsuites>
\ No newline at end of file
+<?xml version="1.0" encoding="utf-8"?>
+<testsuites name="" disabled="1" errors="0" failures="7" tests="11" time="0.0876691">
+    <testsuite name="NUnitXml" tests="11" id="1031" disabled="1" errors="0" failures="7" skipped="1" time="0.087669"
+               timestamp="2024-10-27 20:25:09Z">
+        <properties>
+            <property name="platform" value="EditMode"/>
+        </properties>
+        <testsuite name="MyFeature1.Editor.Tests.dll" tests="10" id="1018" disabled="1" errors="0" failures="6"
+                   skipped="1" time="0.072287" timestamp="2024-10-27 20:25:09Z">
+            <properties>
+                <property name="_PID" value="19711"/>
+                <property name="_APPDOMAIN" value="Unity Child Domain"/>
+                <property name="platform" value="EditMode"/>
+                <property name="EditorOnly" value="True"/>
+            </properties>
+            <testsuite name="MyFeature1" tests="10" id="1019" disabled="1" errors="0" failures="6" skipped="1"
+                       time="0.071332" timestamp="2024-10-27 20:25:09Z">
+                <testsuite name="Editor" tests="10" id="1020" disabled="1" errors="0" failures="6" skipped="1"
+                           time="0.07093" timestamp="2024-10-27 20:25:09Z">
+                    <testsuite name="MyFeature1Test" tests="7" id="1005" disabled="1" errors="0" failures="5"
+                               skipped="1" time="0.046854" timestamp="2024-10-27 20:25:09Z">
+                        <testcase name="TestFailedByAssertion" classname="MyFeature1.Editor.MyFeature1Test"
+                                  assertions="0" time="0.010554" status="Failed">
+                            <properties>
+                                <property name="retryIteration" value="0"/>
+                                <property name="repeatIteration" value="0"/>
+                            </properties>
+                            <failure message="failed" type=""/>
+                        </testcase>
+                        <testcase name="TestFailedByException" classname="MyFeature1.Editor.MyFeature1Test"
+                                  assertions="0" time="0.001907" status="Failed">
+                            <properties>
+                                <property name="retryIteration" value="0"/>
+                                <property name="repeatIteration" value="0"/>
+                            </properties>
+                            <failure message="System.ApplicationException : Fail test by exception" type=""/>
+                        </testcase>
+                        <testcase name="TestFailedByLogError" classname="MyFeature1.Editor.MyFeature1Test"
+                                  assertions="0" time="0.004971" status="Failed">
+                            <properties>
+                                <property name="retryIteration" value="0"/>
+                                <property name="repeatIteration" value="0"/>
+                            </properties>
+                            <failure
+                                    message="Unhandled log message: '[Error] Fail test by log error'. Use UnityEngine.TestTools.LogAssert.Expect"
+                                    type=""/>
+                            <system-out><![CDATA[Fail test by log error]]></system-out>
+                        </testcase>
+                        <testcase name="TestFailedByUnityEngineAssertion" classname="MyFeature1.Editor.MyFeature1Test"
+                                  assertions="0" time="0.000755" status="Failed">
+                            <properties>
+                                <property name="retryIteration" value="0"/>
+                                <property name="repeatIteration" value="0"/>
+                            </properties>
+                            <failure
+                                    message="UnityEngine.Assertions.AssertionException : Fail test by UnityEngine.Assertions.Assert&#xA;Assertion failure. Value was False&#xA;Expected: True"
+                                    type=""/>
+                        </testcase>
+                        <testcase name="TestInconclusive" classname="MyFeature1.Editor.MyFeature1Test" assertions="0"
+                                  time="0.006531" status="Inconclusive">
+                            <properties>
+                                <property name="retryIteration" value="0"/>
+                                <property name="repeatIteration" value="0"/>
+                            </properties>
+                            <failure message="inconclusive&#xA;  Expected: False&#xA;  But was:  True" type=""/>
+                        </testcase>
+                        <testcase name="TestPassed" classname="MyFeature1.Editor.MyFeature1Test" assertions="0"
+                                  time="0.002809" status="Passed">
+                            <properties>
+                                <property name="retryIteration" value="0"/>
+                                <property name="repeatIteration" value="0"/>
+                            </properties>
+                            <system-out><![CDATA[passed]]></system-out>
+                        </testcase>
+                        <testcase name="TestSkipped" classname="MyFeature1.Editor.MyFeature1Test" assertions="0"
+                                  time="0.000242" status="Skipped">
+                            <properties>
+                                <property name="_SKIPREASON" value="Skipped test"/>
+                            </properties>
+                            <skipped>Skipped test</skipped>
+                        </testcase>
+                    </testsuite>
+                    <testsuite name="MyFeature1TestParameterized" tests="3" id="1013" disabled="0" errors="0"
+                               failures="1" skipped="0" time="0.02234" timestamp="2024-10-27 20:25:09Z">
+                        <testsuite name="Parameterized" tests="3" id="1017" disabled="0" errors="0" failures="1"
+                                   skipped="0" time="0.009447" timestamp="2024-10-27 20:25:09Z">
+                            <testcase name="Parameterized(1,2,3)"
+                                      classname="MyFeature1.Editor.MyFeature1TestParameterized" assertions="0"
+                                      time="0.00569" status="Passed">
+                                <properties>
+                                    <property name="retryIteration" value="0"/>
+                                    <property name="repeatIteration" value="0"/>
+                                </properties>
+                            </testcase>
+                            <testcase name="Parameterized(2,3,5)"
+                                      classname="MyFeature1.Editor.MyFeature1TestParameterized" assertions="0"
+                                      time="9.8E-05" status="Passed">
+                                <properties>
+                                    <property name="retryIteration" value="0"/>
+                                    <property name="repeatIteration" value="0"/>
+                                </properties>
+                            </testcase>
+                            <testcase name="Parameterized(3,4,9)"
+                                      classname="MyFeature1.Editor.MyFeature1TestParameterized" assertions="0"
+                                      time="0.001034" status="Failed">
+                                <properties>
+                                    <property name="retryIteration" value="0"/>
+                                    <property name="repeatIteration" value="0"/>
+                                </properties>
+                                <failure message="Expected: 9&#xA;  But was:  7" type=""/>
+                            </testcase>
+                        </testsuite>
+                    </testsuite>
+                </testsuite>
+            </testsuite>
+        </testsuite>
+        <testsuite name="MyFeature2.Editor.Tests.dll" tests="1" id="1023" disabled="0" errors="0" failures="1"
+                   skipped="0" time="0.005488" timestamp="2024-10-27 20:25:09Z">
+            <properties>
+                <property name="_PID" value="19711"/>
+                <property name="_APPDOMAIN" value="Unity Child Domain"/>
+                <property name="platform" value="EditMode"/>
+                <property name="EditorOnly" value="True"/>
+            </properties>
+            <testsuite name="MyFeature2" tests="1" id="1024" disabled="0" errors="0" failures="1" skipped="0"
+                       time="0.002666" timestamp="2024-10-27 20:25:09Z">
+                <testsuite name="Editor" tests="1" id="1025" disabled="0" errors="0" failures="1" skipped="0"
+                           time="0.002034" timestamp="2024-10-27 20:25:09Z">
+                    <testsuite name="MyFeature2Test" tests="1" id="1021" disabled="0" errors="0" failures="1"
+                               skipped="0" time="0.00151" timestamp="2024-10-27 20:25:09Z">
+                        <testcase name="TestInconclusive" classname="MyFeature2.Editor.MyFeature2Test" assertions="0"
+                                  time="0.000408" status="Inconclusive">
+                            <properties>
+                                <property name="retryIteration" value="0"/>
+                                <property name="repeatIteration" value="0"/>
+                            </properties>
+                            <failure message="inconclusive&#xA;  Expected: False&#xA;  But was:  True" type=""/>
+                        </testcase>
+                    </testsuite>
+                </testsuite>
+            </testsuite>
+        </testsuite>
+    </testsuite>
+</testsuites>
\ No newline at end of file
diff --git a/Tests/Editor/TestResources/nunit3.xml b/Tests/Editor/TestResources/nunit3.xml
index 0e48b3d..c64b0b7 100644
--- a/Tests/Editor/TestResources/nunit3.xml
+++ b/Tests/Editor/TestResources/nunit3.xml
@@ -1,60 +1,289 @@
-<test-run id="2" testcasecount="3" result="Passed" total="3" passed="3" failed="0" inconclusive="0" skipped="0" asserts="0" engine-version="3.5.0.0" clr-version="4.0.30319.42000" start-time="2024-05-03 10:09:28Z" end-time="2024-05-03 10:09:28Z" duration="0.2538825">
-  <test-suite type="TestSuite" id="1000" name="UnityProject~" fullname="UnityProject~" runstate="Runnable" testcasecount="3" result="Passed" start-time="2024-05-03 10:09:28Z" end-time="2024-05-03 10:09:28Z" duration="0.253883" total="3" passed="3" failed="0" inconclusive="0" skipped="0" asserts="0">
-    <properties>
-      <property name="platform" value="EditMode" />
-    </properties>
-    <test-suite type="Assembly" id="1009" name="TestHelper.Editor.Tests.dll" fullname="/github/workspace/UnityProject~/Library/ScriptAssemblies/TestHelper.Editor.Tests.dll" runstate="Runnable" testcasecount="3" result="Passed" start-time="2024-05-03 10:09:28Z" end-time="2024-05-03 10:09:28Z" duration="0.246003" total="3" passed="3" failed="0" inconclusive="0" skipped="0" asserts="0">
-      <properties>
-        <property name="_PID" value="3301" />
-        <property name="_APPDOMAIN" value="Unity Child Domain" />
-        <property name="platform" value="EditMode" />
-        <property name="EditorOnly" value="True" />
-      </properties>
-      <test-suite type="TestSuite" id="1010" name="TestHelper" fullname="TestHelper" runstate="Runnable" testcasecount="3" result="Passed" start-time="2024-05-03 10:09:28Z" end-time="2024-05-03 10:09:28Z" duration="0.245391" total="3" passed="3" failed="0" inconclusive="0" skipped="0" asserts="0">
-        <properties />
-        <test-suite type="TestSuite" id="1011" name="Editor" fullname="TestHelper.Editor" runstate="Runnable" testcasecount="3" result="Passed" start-time="2024-05-03 10:09:28Z" end-time="2024-05-03 10:09:28Z" duration="0.241916" total="3" passed="3" failed="0" inconclusive="0" skipped="0" asserts="0">
-          <properties />
-          <test-suite type="TestFixture" id="1003" name="CreateSceneAttributeTest" fullname="TestHelper.Editor.CreateSceneAttributeTest" classname="TestHelper.Editor.CreateSceneAttributeTest" runstate="Runnable" testcasecount="1" result="Passed" start-time="2024-05-03 10:09:28Z" end-time="2024-05-03 10:09:28Z" duration="0.092622" total="1" passed="1" failed="0" inconclusive="0" skipped="0" asserts="0">
-            <properties />
-            <test-case id="1004" name="Attach_CreateNewSceneWithCameraAndLight" fullname="TestHelper.Editor.CreateSceneAttributeTest.Attach_CreateNewSceneWithCameraAndLight" methodname="Attach_CreateNewSceneWithCameraAndLight" classname="TestHelper.Editor.CreateSceneAttributeTest" runstate="Runnable" seed="1087926775" result="Passed" start-time="2024-05-03 10:09:28Z" end-time="2024-05-03 10:09:28Z" duration="0.036802" asserts="0">
-              <properties>
-                <property name="retryIteration" value="0" />
-                <property name="repeatIteration" value="0" />
-              </properties>
-              <output><![CDATA[[Code Coverage] Code Coverage results for visited sequence points were saved in /github/workspace/CodeCoverage/UnityProject~-opencov/EditMode/TestCoverageResults_0001.xml
+<test-run id="2" testcasecount="11" result="Failed(Child)" total="11" passed="3" failed="5" inconclusive="2" skipped="1"
+          asserts="0" engine-version="3.5.0.0" clr-version="4.0.30319.42000" start-time="2024-10-27 20:25:09Z"
+          end-time="2024-10-27 20:25:09Z" duration="0.0876691">
+    <test-suite type="TestSuite" id="1031" name="NUnitXml" fullname="NUnitXml" runstate="Runnable" testcasecount="11"
+                result="Failed" site="Child" start-time="2024-10-27 20:25:09Z" end-time="2024-10-27 20:25:09Z"
+                duration="0.087669" total="11" passed="3" failed="5" inconclusive="2" skipped="1" asserts="0">
+        <properties>
+            <property name="platform" value="EditMode"/>
+        </properties>
+        <failure>
+            <message><![CDATA[One or more child tests had errors]]></message>
+        </failure>
+        <test-suite type="Assembly" id="1018" name="MyFeature1.Editor.Tests.dll"
+                    fullname="/Users/ko2hase/Documents/NUnitXml/Library/ScriptAssemblies/MyFeature1.Editor.Tests.dll"
+                    runstate="Runnable" testcasecount="10" result="Failed" site="Child"
+                    start-time="2024-10-27 20:25:09Z" end-time="2024-10-27 20:25:09Z" duration="0.072287" total="10"
+                    passed="3" failed="5" inconclusive="1" skipped="1" asserts="0">
+            <properties>
+                <property name="_PID" value="19711"/>
+                <property name="_APPDOMAIN" value="Unity Child Domain"/>
+                <property name="platform" value="EditMode"/>
+                <property name="EditorOnly" value="True"/>
+            </properties>
+            <failure>
+                <message><![CDATA[One or more child tests had errors]]></message>
+            </failure>
+            <test-suite type="TestSuite" id="1019" name="MyFeature1" fullname="MyFeature1" runstate="Runnable"
+                        testcasecount="10" result="Failed" site="Child" start-time="2024-10-27 20:25:09Z"
+                        end-time="2024-10-27 20:25:09Z" duration="0.071332" total="10" passed="3" failed="5"
+                        inconclusive="1" skipped="1" asserts="0">
+                <properties/>
+                <failure>
+                    <message><![CDATA[One or more child tests had errors]]></message>
+                </failure>
+                <test-suite type="TestSuite" id="1020" name="Editor" fullname="MyFeature1.Editor" runstate="Runnable"
+                            testcasecount="10" result="Failed" site="Child" start-time="2024-10-27 20:25:09Z"
+                            end-time="2024-10-27 20:25:09Z" duration="0.070930" total="10" passed="3" failed="5"
+                            inconclusive="1" skipped="1" asserts="0">
+                    <properties/>
+                    <failure>
+                        <message><![CDATA[One or more child tests had errors]]></message>
+                    </failure>
+                    <test-suite type="TestFixture" id="1005" name="MyFeature1Test"
+                                fullname="MyFeature1.Editor.MyFeature1Test" classname="MyFeature1.Editor.MyFeature1Test"
+                                runstate="Runnable" testcasecount="7" result="Failed" site="Child"
+                                start-time="2024-10-27 20:25:09Z" end-time="2024-10-27 20:25:09Z" duration="0.046854"
+                                total="7" passed="1" failed="4" inconclusive="1" skipped="1" asserts="0">
+                        <properties/>
+                        <failure>
+                            <message><![CDATA[One or more child tests had errors]]></message>
+                        </failure>
+                        <test-case id="1007" name="TestFailedByAssertion"
+                                   fullname="MyFeature1.Editor.MyFeature1Test.TestFailedByAssertion"
+                                   methodname="TestFailedByAssertion" classname="MyFeature1.Editor.MyFeature1Test"
+                                   runstate="Runnable" seed="1040389132" result="Failed"
+                                   start-time="2024-10-27 20:25:09Z" end-time="2024-10-27 20:25:09Z" duration="0.010554"
+                                   asserts="0">
+                            <properties>
+                                <property name="retryIteration" value="0"/>
+                                <property name="repeatIteration" value="0"/>
+                            </properties>
+                            <failure>
+                                <message><![CDATA[failed]]></message>
+                                <stack-trace><![CDATA[at MyFeature1.Editor.MyFeature1Test.TestFailedByAssertion () [0x00001] in /Users/ko2hase/Documents/NUnitXml/Assets/MyFeature1/Tests/Editor/MyFeature1Test.cs:25
+]]></stack-trace>
+                            </failure>
+                        </test-case>
+                        <test-case id="1008" name="TestFailedByException"
+                                   fullname="MyFeature1.Editor.MyFeature1Test.TestFailedByException"
+                                   methodname="TestFailedByException" classname="MyFeature1.Editor.MyFeature1Test"
+                                   runstate="Runnable" seed="2056623004" result="Failed" label="Error"
+                                   start-time="2024-10-27 20:25:09Z" end-time="2024-10-27 20:25:09Z" duration="0.001907"
+                                   asserts="0">
+                            <properties>
+                                <property name="retryIteration" value="0"/>
+                                <property name="repeatIteration" value="0"/>
+                            </properties>
+                            <failure>
+                                <message><![CDATA[System.ApplicationException : Fail test by exception]]></message>
+                                <stack-trace><![CDATA[  at MyFeature1.Editor.MyFeature1Test.TestFailedByException () [0x00001] in /Users/ko2hase/Documents/NUnitXml/Assets/MyFeature1/Tests/Editor/MyFeature1Test.cs:31 
+  at (wrapper managed-to-native) System.Reflection.RuntimeMethodInfo.InternalInvoke(System.Reflection.RuntimeMethodInfo,object,object[],System.Exception&)
+  at System.Reflection.RuntimeMethodInfo.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x0006a] in <b904252b6b4e4277834bcca7e51f318d>:0 ]]></stack-trace>
+                            </failure>
+                        </test-case>
+                        <test-case id="1009" name="TestFailedByLogError"
+                                   fullname="MyFeature1.Editor.MyFeature1Test.TestFailedByLogError"
+                                   methodname="TestFailedByLogError" classname="MyFeature1.Editor.MyFeature1Test"
+                                   runstate="Runnable" seed="810015734" result="Failed"
+                                   start-time="2024-10-27 20:25:09Z" end-time="2024-10-27 20:25:09Z" duration="0.004971"
+                                   asserts="0">
+                            <properties>
+                                <property name="retryIteration" value="0"/>
+                                <property name="repeatIteration" value="0"/>
+                            </properties>
+                            <failure>
+                                <message>
+                                    <![CDATA[Unhandled log message: '[Error] Fail test by log error'. Use UnityEngine.TestTools.LogAssert.Expect]]></message>
+                                <stack-trace><![CDATA[MyFeature1.Editor.MyFeature1Test:TestFailedByLogError () (at Assets/MyFeature1/Tests/Editor/MyFeature1Test.cs:37)
+System.Reflection.MethodBase:Invoke (object,object[])
+NUnit.Framework.Internal.Reflect:InvokeMethod (System.Reflection.MethodInfo,object,object[])
+NUnit.Framework.Internal.MethodWrapper:Invoke (object,object[])
+NUnit.Framework.Internal.Commands.TestMethodCommand:RunNonAsyncTestMethod (NUnit.Framework.Internal.ITestExecutionContext)
+NUnit.Framework.Internal.Commands.TestMethodCommand:RunTestMethod (NUnit.Framework.Internal.ITestExecutionContext)
+NUnit.Framework.Internal.Commands.TestMethodCommand:Execute (NUnit.Framework.Internal.ITestExecutionContext)
+UnityEditor.EditorApplication:Internal_CallUpdateFunctions () (at /Users/bokken/build/output/unity/unity/Editor/Mono/EditorApplication.cs:381)
+
+]]></stack-trace>
+                            </failure>
+                            <output><![CDATA[Fail test by log error
 ]]></output>
-            </test-case>
-          </test-suite>
-          <test-suite type="TestFixture" id="1005" name="LoadSceneAttributeTest" fullname="TestHelper.Editor.LoadSceneAttributeTest" classname="TestHelper.Editor.LoadSceneAttributeTest" runstate="Runnable" testcasecount="1" result="Passed" start-time="2024-05-03 10:09:28Z" end-time="2024-05-03 10:09:28Z" duration="0.089184" total="1" passed="1" failed="0" inconclusive="0" skipped="0" asserts="0">
-            <properties />
-            <test-case id="1006" name="Attach_LoadedSceneNotInBuild" fullname="TestHelper.Editor.LoadSceneAttributeTest.Attach_LoadedSceneNotInBuild" methodname="Attach_LoadedSceneNotInBuild" classname="TestHelper.Editor.LoadSceneAttributeTest" runstate="Runnable" seed="818183097" result="Passed" start-time="2024-05-03 10:09:28Z" end-time="2024-05-03 10:09:28Z" duration="0.053176" asserts="0">
-              <properties>
-                <property name="retryIteration" value="0" />
-                <property name="repeatIteration" value="0" />
-              </properties>
-              <output><![CDATA[[Code Coverage] Code Coverage results for visited sequence points were saved in /github/workspace/CodeCoverage/UnityProject~-opencov/EditMode/TestCoverageResults_0002.xml
+                        </test-case>
+                        <test-case id="1010" name="TestFailedByUnityEngineAssertion"
+                                   fullname="MyFeature1.Editor.MyFeature1Test.TestFailedByUnityEngineAssertion"
+                                   methodname="TestFailedByUnityEngineAssertion"
+                                   classname="MyFeature1.Editor.MyFeature1Test" runstate="Runnable" seed="2040230563"
+                                   result="Failed" label="Error" start-time="2024-10-27 20:25:09Z"
+                                   end-time="2024-10-27 20:25:09Z" duration="0.000755" asserts="0">
+                            <properties>
+                                <property name="retryIteration" value="0"/>
+                                <property name="repeatIteration" value="0"/>
+                            </properties>
+                            <failure>
+                                <message><![CDATA[UnityEngine.Assertions.AssertionException : Fail test by UnityEngine.Assertions.Assert
+Assertion failure. Value was False
+Expected: True]]></message>
+                                <stack-trace><![CDATA[  at UnityEngine.Assertions.Assert.Fail (System.String message, System.String userMessage) [0x0003c] in /Users/bokken/build/output/unity/unity/Runtime/Export/Assertions/Assert/AssertBase.cs:29 
+  at UnityEngine.Assertions.Assert.IsTrue (System.Boolean condition, System.String message) [0x00009] in /Users/bokken/build/output/unity/unity/Runtime/Export/Assertions/Assert/AssertBool.cs:20 
+  at MyFeature1.Editor.MyFeature1Test.TestFailedByUnityEngineAssertion () [0x00001] in /Users/ko2hase/Documents/NUnitXml/Assets/MyFeature1/Tests/Editor/MyFeature1Test.cs:43 
+  at (wrapper managed-to-native) System.Reflection.RuntimeMethodInfo.InternalInvoke(System.Reflection.RuntimeMethodInfo,object,object[],System.Exception&)
+  at System.Reflection.RuntimeMethodInfo.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x0006a] in <b904252b6b4e4277834bcca7e51f318d>:0 ]]></stack-trace>
+                            </failure>
+                        </test-case>
+                        <test-case id="1011" name="TestInconclusive"
+                                   fullname="MyFeature1.Editor.MyFeature1Test.TestInconclusive"
+                                   methodname="TestInconclusive" classname="MyFeature1.Editor.MyFeature1Test"
+                                   runstate="Runnable" seed="1689148236" result="Inconclusive"
+                                   start-time="2024-10-27 20:25:09Z" end-time="2024-10-27 20:25:09Z" duration="0.006531"
+                                   asserts="0">
+                            <properties>
+                                <property name="retryIteration" value="0"/>
+                                <property name="repeatIteration" value="0"/>
+                            </properties>
+                            <reason>
+                                <message><![CDATA[  inconclusive
+  Expected: False
+  But was:  True
+]]></message>
+                            </reason>
+                        </test-case>
+                        <test-case id="1006" name="TestPassed" fullname="MyFeature1.Editor.MyFeature1Test.TestPassed"
+                                   methodname="TestPassed" classname="MyFeature1.Editor.MyFeature1Test"
+                                   runstate="Runnable" seed="694536572" result="Passed"
+                                   start-time="2024-10-27 20:25:09Z" end-time="2024-10-27 20:25:09Z" duration="0.002809"
+                                   asserts="0">
+                            <properties>
+                                <property name="retryIteration" value="0"/>
+                                <property name="repeatIteration" value="0"/>
+                            </properties>
+                            <output><![CDATA[passed
 ]]></output>
-            </test-case>
-          </test-suite>
-          <test-suite type="TestFixture" id="1007" name="TemporaryBuildScenesUsingInTestTest" fullname="TestHelper.Editor.TemporaryBuildScenesUsingInTestTest" classname="TestHelper.Editor.TemporaryBuildScenesUsingInTestTest" runstate="Runnable" testcasecount="1" result="Passed" start-time="2024-05-03 10:09:28Z" end-time="2024-05-03 10:09:28Z" duration="0.055223" total="1" passed="1" failed="0" inconclusive="0" skipped="0" asserts="0">
-            <properties />
-            <test-case id="1008" name="GetScenesUsingInTest_AttachedToMethod_ReturnScenesSpecifiedByAttribute" fullname="TestHelper.Editor.TemporaryBuildScenesUsingInTestTest.GetScenesUsingInTest_AttachedToMethod_ReturnScenesSpecifiedByAttribute" methodname="GetScenesUsingInTest_AttachedToMethod_ReturnScenesSpecifiedByAttribute" classname="TestHelper.Editor.TemporaryBuildScenesUsingInTestTest" runstate="Runnable" seed="1243330322" result="Passed" start-time="2024-05-03 10:09:28Z" end-time="2024-05-03 10:09:28Z" duration="0.014506" asserts="0">
-              <properties>
-                <property name="retryIteration" value="0" />
-                <property name="repeatIteration" value="0" />
-              </properties>
-              <output><![CDATA[[Code Coverage] Code Coverage results for visited sequence points were saved in /github/workspace/CodeCoverage/UnityProject~-opencov/EditMode/TestCoverageResults_0003.xml
-[Code Coverage] Code Coverage Report was generated in /github/workspace/CodeCoverage/Report
-Included Assemblies: TestHelper,TestHelper.RuntimeInternals,TestHelper.Editor,
-Excluded Assemblies: *.Tests
-Included Paths: <Not specified>
-Excluded Paths: <Not specified>
-Saving results to: /github/workspace/artifacts/editmode-results.xml
-]]></output>
-            </test-case>
-          </test-suite>
+                        </test-case>
+                        <test-case id="1012" name="TestSkipped" fullname="MyFeature1.Editor.MyFeature1Test.TestSkipped"
+                                   methodname="TestSkipped" classname="MyFeature1.Editor.MyFeature1Test"
+                                   runstate="Ignored" seed="2113885182" result="Skipped" label="Ignored"
+                                   start-time="2024-10-27 20:25:09Z" end-time="2024-10-27 20:25:09Z" duration="0.000242"
+                                   asserts="0">
+                            <properties>
+                                <property name="_SKIPREASON" value="Skipped test"/>
+                            </properties>
+                            <reason>
+                                <message><![CDATA[Skipped test]]></message>
+                            </reason>
+                        </test-case>
+                    </test-suite>
+                    <test-suite type="TestFixture" id="1013" name="MyFeature1TestParameterized"
+                                fullname="MyFeature1.Editor.MyFeature1TestParameterized"
+                                classname="MyFeature1.Editor.MyFeature1TestParameterized" runstate="Runnable"
+                                testcasecount="3" result="Failed" site="Child" start-time="2024-10-27 20:25:09Z"
+                                end-time="2024-10-27 20:25:09Z" duration="0.022340" total="3" passed="2" failed="1"
+                                inconclusive="0" skipped="0" asserts="0">
+                        <properties/>
+                        <failure>
+                            <message><![CDATA[One or more child tests had errors]]></message>
+                        </failure>
+                        <test-suite type="ParameterizedMethod" id="1017" name="Parameterized"
+                                    fullname="MyFeature1.Editor.MyFeature1TestParameterized.Parameterized"
+                                    classname="MyFeature1.Editor.MyFeature1TestParameterized" runstate="Runnable"
+                                    testcasecount="3" result="Failed" site="Child" start-time="2024-10-27 20:25:09Z"
+                                    end-time="2024-10-27 20:25:09Z" duration="0.009447" total="3" passed="2" failed="1"
+                                    inconclusive="0" skipped="0" asserts="0">
+                            <properties/>
+                            <failure>
+                                <message><![CDATA[One or more child tests had errors]]></message>
+                            </failure>
+                            <test-case id="1014" name="Parameterized(1,2,3)"
+                                       fullname="MyFeature1.Editor.MyFeature1TestParameterized.Parameterized(1,2,3)"
+                                       methodname="Parameterized"
+                                       classname="MyFeature1.Editor.MyFeature1TestParameterized" runstate="Runnable"
+                                       seed="470631257" result="Passed" start-time="2024-10-27 20:25:09Z"
+                                       end-time="2024-10-27 20:25:09Z" duration="0.005690" asserts="0">
+                                <properties>
+                                    <property name="retryIteration" value="0"/>
+                                    <property name="repeatIteration" value="0"/>
+                                </properties>
+                            </test-case>
+                            <test-case id="1015" name="Parameterized(2,3,5)"
+                                       fullname="MyFeature1.Editor.MyFeature1TestParameterized.Parameterized(2,3,5)"
+                                       methodname="Parameterized"
+                                       classname="MyFeature1.Editor.MyFeature1TestParameterized" runstate="Runnable"
+                                       seed="2138031648" result="Passed" start-time="2024-10-27 20:25:09Z"
+                                       end-time="2024-10-27 20:25:09Z" duration="0.000098" asserts="0">
+                                <properties>
+                                    <property name="retryIteration" value="0"/>
+                                    <property name="repeatIteration" value="0"/>
+                                </properties>
+                            </test-case>
+                            <test-case id="1016" name="Parameterized(3,4,9)"
+                                       fullname="MyFeature1.Editor.MyFeature1TestParameterized.Parameterized(3,4,9)"
+                                       methodname="Parameterized"
+                                       classname="MyFeature1.Editor.MyFeature1TestParameterized" runstate="Runnable"
+                                       seed="1211309338" result="Failed" start-time="2024-10-27 20:25:09Z"
+                                       end-time="2024-10-27 20:25:09Z" duration="0.001034" asserts="0">
+                                <properties>
+                                    <property name="retryIteration" value="0"/>
+                                    <property name="repeatIteration" value="0"/>
+                                </properties>
+                                <failure>
+                                    <message><![CDATA[  Expected: 9
+  But was:  7
+]]></message>
+                                    <stack-trace><![CDATA[at MyFeature1.Editor.MyFeature1TestParameterized.Parameterized (System.Int32 i, System.Int32 j, System.Int32 expected) [0x00001] in /Users/ko2hase/Documents/NUnitXml/Assets/MyFeature1/Tests/Editor/MyFeature1TestParameterized.cs:16
+]]></stack-trace>
+                                </failure>
+                            </test-case>
+                        </test-suite>
+                    </test-suite>
+                </test-suite>
+            </test-suite>
+        </test-suite>
+        <test-suite type="Assembly" id="1023" name="MyFeature2.Editor.Tests.dll"
+                    fullname="/Users/ko2hase/Documents/NUnitXml/Library/ScriptAssemblies/MyFeature2.Editor.Tests.dll"
+                    runstate="Runnable" testcasecount="1" result="Passed" start-time="2024-10-27 20:25:09Z"
+                    end-time="2024-10-27 20:25:09Z" duration="0.005488" total="1" passed="0" failed="0" inconclusive="1"
+                    skipped="0" asserts="0">
+            <properties>
+                <property name="_PID" value="19711"/>
+                <property name="_APPDOMAIN" value="Unity Child Domain"/>
+                <property name="platform" value="EditMode"/>
+                <property name="EditorOnly" value="True"/>
+            </properties>
+            <test-suite type="TestSuite" id="1024" name="MyFeature2" fullname="MyFeature2" runstate="Runnable"
+                        testcasecount="1" result="Passed" start-time="2024-10-27 20:25:09Z"
+                        end-time="2024-10-27 20:25:09Z" duration="0.002666" total="1" passed="0" failed="0"
+                        inconclusive="1" skipped="0" asserts="0">
+                <properties/>
+                <test-suite type="TestSuite" id="1025" name="Editor" fullname="MyFeature2.Editor" runstate="Runnable"
+                            testcasecount="1" result="Passed" start-time="2024-10-27 20:25:09Z"
+                            end-time="2024-10-27 20:25:09Z" duration="0.002034" total="1" passed="0" failed="0"
+                            inconclusive="1" skipped="0" asserts="0">
+                    <properties/>
+                    <test-suite type="TestFixture" id="1021" name="MyFeature2Test"
+                                fullname="MyFeature2.Editor.MyFeature2Test" classname="MyFeature2.Editor.MyFeature2Test"
+                                runstate="Runnable" testcasecount="1" result="Passed" start-time="2024-10-27 20:25:09Z"
+                                end-time="2024-10-27 20:25:09Z" duration="0.001510" total="1" passed="0" failed="0"
+                                inconclusive="1" skipped="0" asserts="0">
+                        <properties/>
+                        <test-case id="1022" name="TestInconclusive"
+                                   fullname="MyFeature2.Editor.MyFeature2Test.TestInconclusive"
+                                   methodname="TestInconclusive" classname="MyFeature2.Editor.MyFeature2Test"
+                                   runstate="Runnable" seed="350965709" result="Inconclusive"
+                                   start-time="2024-10-27 20:25:09Z" end-time="2024-10-27 20:25:09Z" duration="0.000408"
+                                   asserts="0">
+                            <properties>
+                                <property name="retryIteration" value="0"/>
+                                <property name="repeatIteration" value="0"/>
+                            </properties>
+                            <reason>
+                                <message><![CDATA[  inconclusive
+  Expected: False
+  But was:  True
+]]></message>
+                            </reason>
+                        </test-case>
+                    </test-suite>
+                </test-suite>
+            </test-suite>
         </test-suite>
-      </test-suite>
     </test-suite>
-  </test-suite>
 </test-run>
\ No newline at end of file

From af2f20b2399d19114a299d92d0b90be66af14bfa Mon Sep 17 00:00:00 2001
From: Koji Hasegawa <hasegawa@hubsys.co.jp>
Date: Mon, 28 Oct 2024 08:04:49 +0900
Subject: [PATCH 5/5] Mod README.md

---
 README.md | 18 +++++++++++++-----
 1 file changed, 13 insertions(+), 5 deletions(-)

diff --git a/README.md b/README.md
index e4e0edd..d2a44b0 100644
--- a/README.md
+++ b/README.md
@@ -334,7 +334,7 @@ public class MyTestClass
 }
 ```
 
-> [!NOTE]
+> [!NOTE]  
 > - Scene file path is starts with `Assets/` or `Packages/` or `.`. And package name using `name` instead of `displayName`, when scenes in the package. (e.g., `Packages/com.nowsprinting.test-helper/Tests/Scenes/Scene.unity`)
 
 
@@ -529,11 +529,11 @@ public class MyTestClass
 > When used with operators, use it in method style. e.g., `Is.Not.Destroyed()`
 
 
-### RuntimeInternals
+### Runtime APIs
 
 `TestHelper.RuntimeInternals` assembly can be used from the runtime code because it does not depend on test-framework.
 
-> [!NOTE]
+> [!NOTE]  
 > The "Define Constraints" is set to `UNITY_INCLUDE_TESTS || COM_NOWSPRINTING_TEST_HELPER_ENABLE` in this assembly definition files, so it is generally excluded from release builds.
 > To use the feature in release builds, add `COM_NOWSPRINTING_TEST_HELPER_ENABLE` to the "Define Symbols" at build time.
 
@@ -615,7 +615,7 @@ public class MyTestClass
 }
 ```
 
-> [!NOTE]
+> [!NOTE]  
 > - Scene file path is starts with `Assets/` or `Packages/` or `.`. And package name using `name` instead of `displayName`, when scenes in the package. (e.g., `Packages/com.nowsprinting.test-helper/Tests/Scenes/Scene.unity`)
 > - When loading the scene that is not in "Scenes in Build", use [BuildSceneAttribute](#BuildScene).
 
@@ -624,7 +624,15 @@ public class MyTestClass
 
 #### Open Persistent Data Directory
 
-Select **Window** > **Open Persistent Data Directory**, which opens the directory pointed to by [Application.persistentDataPath](https://docs.unity3d.com/ScriptReference/Application-persistentDataPath.html) in the Finder/ File Explorer.
+Select **Window > Open Persistent Data Directory**, which opens the directory pointed to by [Application.persistentDataPath](https://docs.unity3d.com/ScriptReference/Application-persistentDataPath.html) in the Finder/ File Explorer.
+
+
+### JUnit XML format report
+
+If you specify path with `-testHelperJUnitResults` command line option, the test result will be written in JUnit XML format when the tests are finished.
+
+> [!NOTE]  
+> The JUnit XML format is the so-called "Legacy." It does not support the "Open Test Reporting format" introduced in JUnit 5.