This is an automated email from the ASF dual-hosted git repository.

veithen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ws-axiom.git


The following commit(s) were added to refs/heads/master by this push:
     new 75ea3245b Add MatrixTestContainer + custom @Test annotation for 
multi-method matrix tests
75ea3245b is described below

commit 75ea3245b4b4df49cebb37384901c7380f762ec9
Author: Copilot <[email protected]>
AuthorDate: Fri May 22 21:19:19 2026 +0100

    Add MatrixTestContainer + custom @Test annotation for multi-method matrix 
tests
    
    Co-authored-by: Andreas Veithen-Knowles <[email protected]>
---
 .github/agents/consolidate-matrix-tests.md         | 141 +++++++++++++++++++++
 .../java/org/apache/axiom/ts/dom/DOMTestSuite.java |   6 +-
 .../documentfragment/DocumentFragmentTests.java    |  99 +++++++++++++++
 .../ts/dom/documentfragment/TestCloneNodeDeep.java |  53 --------
 .../dom/documentfragment/TestCloneNodeShallow.java |  46 -------
 .../documentfragment/TestLookupNamespaceURI.java   |  50 --------
 .../ts/dom/documentfragment/TestLookupPrefix.java  |  50 --------
 testing/matrix-testsuite/README.md                 |  71 +++++++++--
 .../axiom/testutils/suite/MatrixTestContainer.java |  91 +++++++++++++
 .../org/apache/axiom/testutils/suite/Test.java     |  33 +++++
 10 files changed, 425 insertions(+), 215 deletions(-)

diff --git a/.github/agents/consolidate-matrix-tests.md 
b/.github/agents/consolidate-matrix-tests.md
new file mode 100644
index 000000000..9c8111632
--- /dev/null
+++ b/.github/agents/consolidate-matrix-tests.md
@@ -0,0 +1,141 @@
+# Consolidate Matrix Tests
+
+## Purpose
+
+This skill describes how to consolidate multiple single-method matrix test 
classes (each
+implementing `Executable`) into a single multi-method class annotated with
+`@org.apache.axiom.testutils.suite.Test` methods. Use this when several 
closely-related test
+classes in the same package share the same injected dependencies and logically 
belong together
+(e.g., they all test the same DOM node type or API surface).
+
+## When to consolidate
+
+Consolidation requires that:
+
+- Multiple `Executable` test classes live in the same package.
+- All the classes being consolidated are wrapped in `MatrixTest` nodes under 
the same parent
+  node. This (in general, but not always) means they inject the same set of 
fields.
+
+## How MatrixTestContainer handles multi-method classes
+
+`MatrixTestContainer` is a dedicated leaf node for multi-method test classes. 
It:
+
+- Scans the class for methods annotated with 
`@org.apache.axiom.testutils.suite.Test`.
+- Sorts them alphabetically for reproducibility.
+- Evaluates exclusion filters **per method**: a label `"test"` set to the 
method name is added
+  to the inherited label map before testing. Methods that match the exclusion 
predicate are
+  omitted; if all are excluded the node produces nothing.
+- Produces a `DynamicContainer` named after the class, with one `DynamicTest` 
per remaining method.
+- Creates a fresh Guice-injected instance for each method invocation.
+
+**Important:** use `@org.apache.axiom.testutils.suite.Test`, **not** JUnit 5's
+`@org.junit.jupiter.api.Test`. The custom annotation has no JUnit 
meta-annotations, so
+Surefire and the Jupiter engine will not discover and run the class directly 
as a standalone
+JUnit test.
+
+Individual methods can be excluded using the `"test"` label, for example:
+
+```java
+filters.add(SomeBehaviorTests.class, "(test=methodToSkip)")
+```
+
+## Step-by-step consolidation process
+
+### 1. Identify the target group
+
+Find all `Executable` test classes in a package that share the same parent 
node in the test
+tree. Example: a `documentfragment` package containing `TestCloneNodeDeep`,
+`TestCloneNodeShallow`, `TestLookupNamespaceURI`, `TestLookupPrefix`.
+
+### 2. Migrate any existing filters
+
+Search across the entire codebase for `MatrixTestFilters` usages that 
reference any of the
+target classes. If any consumer excludes one of the old classes individually, 
migrate the filter
+to use the new consolidated class plus the `"test"` label for the 
corresponding method:
+
+```java
+// Before (old class-level exclusion)
+filters.add(TestCloneNodeDeep.class)
+
+// After (new per-method exclusion on the consolidated class)
+filters.add(DocumentFragmentTests.class, "(test=cloneNodeDeep)")
+```
+
+### 3. Create the consolidated test class
+
+Create a new class in the same package. Follow this naming convention:
+- Use the shared noun/area as the class name prefix, e.g., 
`DocumentFragmentTests`.
+- The class name should end with `Tests` (plural) to distinguish it from 
single-method
+  `Executable` test classes which typically end with no suffix or a singular 
noun.
+
+Structure of the new class:
+
+```java
+import org.apache.axiom.testutils.suite.Test;
+
+/** Tests for {@link SomeType}. */
+public class SomeTypeTests {
+    // Declare only the fields that were @Inject-ed in the old classes
+    @Inject
+    private SomeDependency dep;
+
+    /** One-line description of what this method tests. */
+    @Test
+    public void descriptiveMethodName() throws Throwable {
+        // body from the old execute() method
+    }
+
+    // ... one @Test method per old class
+}
+```
+
+Method naming conventions:
+- Drop the `Test` prefix from the old class name and lowercase the first 
letter.
+  - `TestCloneNodeDeep` → `cloneNodeDeep()`
+  - `TestLookupNamespaceURI` → `lookupNamespaceURI()`
+- Methods should be `public void` and may declare `throws Throwable`.
+- Keep the original Javadoc comment from the old class on the corresponding 
method.
+
+### 4. Update the test suite registration
+
+In the suite factory class (e.g., `DOMTestSuite`), replace the N individual 
`MatrixTest`
+entries with a single `MatrixTestContainer` entry:
+
+```java
+// Before
+new MatrixTest(org.example.documentfragment.TestCloneNodeDeep.class),
+new MatrixTest(org.example.documentfragment.TestCloneNodeShallow.class),
+new MatrixTest(org.example.documentfragment.TestLookupNamespaceURI.class),
+new MatrixTest(org.example.documentfragment.TestLookupPrefix.class),
+
+// After
+new 
MatrixTestContainer(org.example.documentfragment.DocumentFragmentTests.class),
+```
+
+### 5. Delete the old files
+
+Remove all the individual `Executable` test files that were consolidated.
+
+### 6. Build and test
+
+Run the affected module(s) to confirm no regressions:
+
+```bash
+./mvnw -pl <affected-modules> -am test
+```
+
+For changes to `testing/dom-testsuite` or `testing/matrix-testsuite`, run:
+
+```bash
+./mvnw -pl testing/matrix-testsuite,testing/dom-testsuite -am test
+```
+
+## Formatting
+
+The project uses `spotless-maven-plugin` with `palantirJavaFormat`. After 
editing, run:
+
+```bash
+./mvnw -pl <module> spotless:apply
+```
+
+or let the build fail on the first attempt and re-run after applying the 
formatter.
diff --git 
a/testing/dom-testsuite/src/main/java/org/apache/axiom/ts/dom/DOMTestSuite.java 
b/testing/dom-testsuite/src/main/java/org/apache/axiom/ts/dom/DOMTestSuite.java
index e36e6cff4..82a7963d3 100644
--- 
a/testing/dom-testsuite/src/main/java/org/apache/axiom/ts/dom/DOMTestSuite.java
+++ 
b/testing/dom-testsuite/src/main/java/org/apache/axiom/ts/dom/DOMTestSuite.java
@@ -30,6 +30,7 @@ import org.apache.axiom.testutils.suite.FanOutNode;
 import org.apache.axiom.testutils.suite.InjectorNode;
 import org.apache.axiom.testutils.suite.LabelBinding;
 import org.apache.axiom.testutils.suite.MatrixTest;
+import org.apache.axiom.testutils.suite.MatrixTestContainer;
 import org.apache.axiom.testutils.suite.MatrixTestNode;
 import org.apache.axiom.testutils.suite.ParentNode;
 import org.apache.axiom.ts.jaxp.dom.DOMImplementation;
@@ -100,10 +101,7 @@ public class DOMTestSuite {
                         new 
MatrixTest(org.apache.axiom.ts.dom.document.TestLookupPrefixWithEmptyDocument.class),
                         new 
MatrixTest(org.apache.axiom.ts.dom.document.TestNormalizeDocumentNamespace.class),
                         new 
MatrixTest(org.apache.axiom.ts.dom.document.TestValidator.class),
-                        new 
MatrixTest(org.apache.axiom.ts.dom.documentfragment.TestCloneNodeDeep.class),
-                        new 
MatrixTest(org.apache.axiom.ts.dom.documentfragment.TestCloneNodeShallow.class),
-                        new 
MatrixTest(org.apache.axiom.ts.dom.documentfragment.TestLookupNamespaceURI.class),
-                        new 
MatrixTest(org.apache.axiom.ts.dom.documentfragment.TestLookupPrefix.class),
+                        new 
MatrixTestContainer(org.apache.axiom.ts.dom.documentfragment.DocumentFragmentTests.class),
                         new 
MatrixTest(org.apache.axiom.ts.dom.documenttype.TestWithParser1.class),
                         new 
MatrixTest(org.apache.axiom.ts.dom.documenttype.TestWithParser2.class),
                         new 
MatrixTest(org.apache.axiom.ts.dom.element.TestAppendChild.class),
diff --git 
a/testing/dom-testsuite/src/main/java/org/apache/axiom/ts/dom/documentfragment/DocumentFragmentTests.java
 
b/testing/dom-testsuite/src/main/java/org/apache/axiom/ts/dom/documentfragment/DocumentFragmentTests.java
new file mode 100644
index 000000000..663e050da
--- /dev/null
+++ 
b/testing/dom-testsuite/src/main/java/org/apache/axiom/ts/dom/documentfragment/DocumentFragmentTests.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.axiom.ts.dom.documentfragment;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.google.inject.Inject;
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilderFactory;
+import org.apache.axiom.testutils.suite.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentFragment;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+/** Tests for {@link DocumentFragment}. */
+public class DocumentFragmentTests {
+    @Inject
+    private DocumentBuilderFactory dbf;
+
+    /** Tests {@link Node#cloneNode(boolean)} with {@code deep} set to {@code 
true}. */
+    @Test
+    public void cloneNodeDeep() throws Throwable {
+        Document document = dbf.newDocumentBuilder().newDocument();
+        DocumentFragment fragment = document.createDocumentFragment();
+        fragment.appendChild(document.createComment("comment"));
+        fragment.appendChild(document.createElementNS(null, "test"));
+        DocumentFragment clone = (DocumentFragment) fragment.cloneNode(true);
+        assertThat(clone.getOwnerDocument()).isSameAs(document);
+        Node child = clone.getFirstChild();
+        assertThat(child).isNotNull();
+        assertThat(child.getNodeType()).isEqualTo(Node.COMMENT_NODE);
+        child = child.getNextSibling();
+        assertThat(child).isNotNull();
+        assertThat(child.getNodeType()).isEqualTo(Node.ELEMENT_NODE);
+        assertThat(child.getLocalName()).isEqualTo("test");
+        child = child.getNextSibling();
+        assertThat(child).isNull();
+    }
+
+    /** Tests {@link Node#cloneNode(boolean)} with {@code deep} set to {@code 
false}. */
+    @Test
+    public void cloneNodeShallow() throws Throwable {
+        Document document = dbf.newDocumentBuilder().newDocument();
+        DocumentFragment fragment = document.createDocumentFragment();
+        fragment.appendChild(document.createElementNS(null, "test"));
+        DocumentFragment clone = (DocumentFragment) fragment.cloneNode(false);
+        assertThat(clone.getOwnerDocument()).isSameAs(document);
+        assertThat(clone.getFirstChild()).isNull();
+        assertThat(clone.getLastChild()).isNull();
+        assertThat(clone.getChildNodes().getLength()).isEqualTo(0);
+    }
+
+    /**
+     * Tests that a call to {@link Node#lookupNamespaceURI(String)} on a 
{@link DocumentFragment}
+     * always returns {@code null} (in contrast to {@link Document}), even if 
one of its children
+     * has a matching namespace declaration.
+     */
+    @Test
+    public void lookupNamespaceURI() throws Throwable {
+        Document document = dbf.newDocumentBuilder().newDocument();
+        DocumentFragment fragment = document.createDocumentFragment();
+        Element element = document.createElementNS("urn:test", "ns:root");
+        element.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, 
"xmlns:ns", "urn:test");
+        fragment.appendChild(element);
+        assertThat(fragment.lookupNamespaceURI("ns")).isNull();
+    }
+
+    /**
+     * Tests that a call to {@link Node#lookupPrefix(String)} on a {@link 
DocumentFragment} always
+     * returns {@code null} (in contrast to {@link Document}), even if one of 
its children has a
+     * matching namespace declaration.
+     */
+    @Test
+    public void lookupPrefix() throws Throwable {
+        Document document = dbf.newDocumentBuilder().newDocument();
+        DocumentFragment fragment = document.createDocumentFragment();
+        Element element = document.createElementNS("urn:test", "ns:root");
+        element.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, 
"xmlns:ns", "urn:test");
+        fragment.appendChild(element);
+        assertThat(fragment.lookupPrefix("urn:test")).isNull();
+    }
+}
diff --git 
a/testing/dom-testsuite/src/main/java/org/apache/axiom/ts/dom/documentfragment/TestCloneNodeDeep.java
 
b/testing/dom-testsuite/src/main/java/org/apache/axiom/ts/dom/documentfragment/TestCloneNodeDeep.java
deleted file mode 100644
index cf69626eb..000000000
--- 
a/testing/dom-testsuite/src/main/java/org/apache/axiom/ts/dom/documentfragment/TestCloneNodeDeep.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.axiom.ts.dom.documentfragment;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-import com.google.inject.Inject;
-import javax.xml.parsers.DocumentBuilderFactory;
-import org.junit.jupiter.api.function.Executable;
-import org.w3c.dom.Document;
-import org.w3c.dom.DocumentFragment;
-import org.w3c.dom.Node;
-
-/** Tests {@link Node#cloneNode(boolean)} with <code>deep</code> set to 
<code>true</code>. */
-public class TestCloneNodeDeep implements Executable {
-    @Inject
-    private DocumentBuilderFactory dbf;
-
-    @Override
-    public void execute() throws Throwable {
-        Document document = dbf.newDocumentBuilder().newDocument();
-        DocumentFragment fragment = document.createDocumentFragment();
-        fragment.appendChild(document.createComment("comment"));
-        fragment.appendChild(document.createElementNS(null, "test"));
-        DocumentFragment clone = (DocumentFragment) fragment.cloneNode(true);
-        assertThat(clone.getOwnerDocument()).isSameAs(document);
-        Node child = clone.getFirstChild();
-        assertThat(child).isNotNull();
-        assertThat(child.getNodeType()).isEqualTo(Node.COMMENT_NODE);
-        child = child.getNextSibling();
-        assertThat(child).isNotNull();
-        assertThat(child.getNodeType()).isEqualTo(Node.ELEMENT_NODE);
-        assertThat(child.getLocalName()).isEqualTo("test");
-        child = child.getNextSibling();
-        assertThat(child).isNull();
-    }
-}
diff --git 
a/testing/dom-testsuite/src/main/java/org/apache/axiom/ts/dom/documentfragment/TestCloneNodeShallow.java
 
b/testing/dom-testsuite/src/main/java/org/apache/axiom/ts/dom/documentfragment/TestCloneNodeShallow.java
deleted file mode 100644
index 5f8e4b5e5..000000000
--- 
a/testing/dom-testsuite/src/main/java/org/apache/axiom/ts/dom/documentfragment/TestCloneNodeShallow.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.axiom.ts.dom.documentfragment;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-import com.google.inject.Inject;
-import javax.xml.parsers.DocumentBuilderFactory;
-import org.junit.jupiter.api.function.Executable;
-import org.w3c.dom.Document;
-import org.w3c.dom.DocumentFragment;
-import org.w3c.dom.Node;
-
-/** Tests {@link Node#cloneNode(boolean)} with <code>deep</code> set to 
<code>false</code>. */
-public class TestCloneNodeShallow implements Executable {
-    @Inject
-    private DocumentBuilderFactory dbf;
-
-    @Override
-    public void execute() throws Throwable {
-        Document document = dbf.newDocumentBuilder().newDocument();
-        DocumentFragment fragment = document.createDocumentFragment();
-        fragment.appendChild(document.createElementNS(null, "test"));
-        DocumentFragment clone = (DocumentFragment) fragment.cloneNode(false);
-        assertThat(clone.getOwnerDocument()).isSameAs(document);
-        assertThat(clone.getFirstChild()).isNull();
-        assertThat(clone.getLastChild()).isNull();
-        assertThat(clone.getChildNodes().getLength()).isEqualTo(0);
-    }
-}
diff --git 
a/testing/dom-testsuite/src/main/java/org/apache/axiom/ts/dom/documentfragment/TestLookupNamespaceURI.java
 
b/testing/dom-testsuite/src/main/java/org/apache/axiom/ts/dom/documentfragment/TestLookupNamespaceURI.java
deleted file mode 100644
index abefed932..000000000
--- 
a/testing/dom-testsuite/src/main/java/org/apache/axiom/ts/dom/documentfragment/TestLookupNamespaceURI.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.axiom.ts.dom.documentfragment;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-import com.google.inject.Inject;
-import javax.xml.XMLConstants;
-import javax.xml.parsers.DocumentBuilderFactory;
-import org.junit.jupiter.api.function.Executable;
-import org.w3c.dom.Document;
-import org.w3c.dom.DocumentFragment;
-import org.w3c.dom.Element;
-import org.w3c.dom.Node;
-
-/**
- * Tests that a call to {@link Node#lookupNamespaceURI(String)} on a {@link 
DocumentFragment} always
- * returns <code>null</code> (in contrast to {@link Document}), even if one of 
its children has a
- * matching namespace declaration.
- */
-public class TestLookupNamespaceURI implements Executable {
-    @Inject
-    private DocumentBuilderFactory dbf;
-
-    @Override
-    public void execute() throws Throwable {
-        Document document = dbf.newDocumentBuilder().newDocument();
-        DocumentFragment fragment = document.createDocumentFragment();
-        Element element = document.createElementNS("urn:test", "ns:root");
-        element.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, 
"xmlns:ns", "urn:test");
-        fragment.appendChild(element);
-        assertThat(fragment.lookupNamespaceURI("ns")).isNull();
-    }
-}
diff --git 
a/testing/dom-testsuite/src/main/java/org/apache/axiom/ts/dom/documentfragment/TestLookupPrefix.java
 
b/testing/dom-testsuite/src/main/java/org/apache/axiom/ts/dom/documentfragment/TestLookupPrefix.java
deleted file mode 100644
index 607508a5b..000000000
--- 
a/testing/dom-testsuite/src/main/java/org/apache/axiom/ts/dom/documentfragment/TestLookupPrefix.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.axiom.ts.dom.documentfragment;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-import com.google.inject.Inject;
-import javax.xml.XMLConstants;
-import javax.xml.parsers.DocumentBuilderFactory;
-import org.junit.jupiter.api.function.Executable;
-import org.w3c.dom.Document;
-import org.w3c.dom.DocumentFragment;
-import org.w3c.dom.Element;
-import org.w3c.dom.Node;
-
-/**
- * Tests that a call to {@link Node#lookupPrefix(String)} on a {@link 
DocumentFragment} always
- * returns <code>null</code> (in contrast to {@link Document}), even if one of 
its children has a
- * matching namespace declaration.
- */
-public class TestLookupPrefix implements Executable {
-    @Inject
-    private DocumentBuilderFactory dbf;
-
-    @Override
-    public void execute() throws Throwable {
-        Document document = dbf.newDocumentBuilder().newDocument();
-        DocumentFragment fragment = document.createDocumentFragment();
-        Element element = document.createElementNS("urn:test", "ns:root");
-        element.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, 
"xmlns:ns", "urn:test");
-        fragment.appendChild(element);
-        assertThat(fragment.lookupPrefix("urn:test")).isNull();
-    }
-}
diff --git a/testing/matrix-testsuite/README.md 
b/testing/matrix-testsuite/README.md
index c4e550926..bef99df62 100644
--- a/testing/matrix-testsuite/README.md
+++ b/testing/matrix-testsuite/README.md
@@ -144,6 +144,20 @@ through the full `setUp()` → `runTest()` → `tearDown()` 
lifecycle (with
 `tearDown()` called in a `finally` block). The test is skipped if matched by 
the
 exclusion filters.
 
+### `MatrixTestContainer`
+
+Leaf node for classes that contain multiple test methods annotated with
+`@Test`. Produces a `DynamicContainer` named after the class,
+containing one child `DynamicTest` per annotated method. Methods are sorted
+alphabetically for reproducibility. A fresh Guice-injected instance of the test
+class is created for each method invocation.
+
+Each method is evaluated against the exclusion filters independently: a label
+`"test"` set to the method name is added to the inherited label map before
+testing. Methods that match the exclusion predicate are omitted; if all are
+excluded the node produces nothing. The test class must have an injectable
+constructor (no-arg or `@Inject`-annotated) and may use field injection.
+
 ### `InjectorNode`
 
 A node that creates a child Guice injector from the supplied modules and 
threads
@@ -172,29 +186,62 @@ OSGi's `FrameworkUtil.createFilter()`). Built via 
`MatrixTestFilters.builder()`.
 
 ## Writing a test case
 
-Test cases implement `MatrixTestCase` (directly or through a domain-specific 
abstract base class)
-and implement `runTest()`. Dependencies are declared with `@Inject` — either 
on fields or via
-constructor. The test case does **not** receive labels through its constructor 
and
-does **not** call `addLabel()`.
+### Single-method style
+
+The test class implements `Executable` (directly or through a domain-specific 
abstract base class).
+Dependencies are declared with `@Inject` — either on fields or via constructor.
 
 ```java
-public abstract class MyTestCase implements MatrixTestCase {
-    @Inject protected SomeImplementation impl;
-    @Inject protected SomeDimension dimension;
+public class TestSomeBehavior implements Executable {
+    @Inject private SomeImplementation impl;
+    @Inject private SomeDimension dimension;
 
-    // convenience methods using impl and dimension ...
+    @Override
+    public void execute() throws Throwable {
+        // test logic using injected fields
+    }
 }
 ```
 
+Register it as a leaf node:
+
 ```java
-public class TestSomeBehavior extends MyTestCase {
-    @Override
-    public void runTest() throws Throwable {
-        // test logic using inherited injected fields
+new MatrixTest(TestSomeBehavior.class)
+```
+
+### Multi-method style
+
+When several related tests share the same injected dependencies, they can be 
grouped into a single
+class with multiple `@Test`-annotated methods. Each method becomes a separate 
`DynamicTest` inside
+a `DynamicContainer` named after the class. A fresh Guice-injected instance is 
created for each
+method, so methods are fully independent.
+
+Note: use `@org.apache.axiom.testutils.suite.Test`, **not** JUnit 5's `@Test`, 
to prevent Surefire
+or other runners from discovering the class as a standalone JUnit test.
+
+```java
+public class SomeBehaviorTests {
+    @Inject private SomeImplementation impl;
+    @Inject private SomeDimension dimension;
+
+    @Test
+    public void behaviorA() throws Throwable {
+        // test logic
+    }
+
+    @Test
+    public void behaviorB() throws Throwable {
+        // test logic
     }
 }
 ```
 
+Register the whole class as a single leaf node using `MatrixTestContainer`:
+
+```java
+new MatrixTestContainer(SomeBehaviorTests.class)
+```
+
 ## Defining a test suite
 
 The test suite author creates a factory method that builds an `InjectorNode`,
diff --git 
a/testing/matrix-testsuite/src/main/java/org/apache/axiom/testutils/suite/MatrixTestContainer.java
 
b/testing/matrix-testsuite/src/main/java/org/apache/axiom/testutils/suite/MatrixTestContainer.java
new file mode 100644
index 000000000..878bc3af6
--- /dev/null
+++ 
b/testing/matrix-testsuite/src/main/java/org/apache/axiom/testutils/suite/MatrixTestContainer.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.axiom.testutils.suite;
+
+import com.google.inject.Injector;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiPredicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.DynamicContainer;
+import org.junit.jupiter.api.DynamicNode;
+import org.junit.jupiter.api.DynamicTest;
+
+/**
+ * A leaf node that instantiates a test class via Guice and executes all 
methods annotated with
+ * {@link Test @Test}.
+ *
+ * <p>The test class must have an injectable constructor (either a no-arg 
constructor or one
+ * annotated with {@code @Inject}). Field injection is also supported. The 
injector received from
+ * the ancestor {@link FanOutNode} chain will have bindings for all dimension 
types, plus any
+ * implementation-level bindings from the root injector.
+ *
+ * <p>This node produces a {@link DynamicContainer} named after the class, 
containing one
+ * {@link DynamicTest} per {@link Test @Test}-annotated method. Methods are 
sorted alphabetically
+ * for reproducibility. A fresh Guice-injected instance of the test class is 
created for each
+ * method invocation.
+ *
+ * <p>Each method is evaluated against the exclusion filters independently: a 
label {@code "test"}
+ * set to the method name is added to the inherited label map before testing. 
Methods that match
+ * the exclusion predicate are omitted from the container. If all methods are 
excluded the node
+ * produces an empty stream.
+ */
+public class MatrixTestContainer extends MatrixTestNode {
+    private final Class<?> testClass;
+
+    public MatrixTestContainer(Class<?> testClass) {
+        this.testClass = testClass;
+    }
+
+    @Override
+    protected Stream<DynamicNode> toDynamicNodes(
+            Injector injector,
+            Map<String, String> inheritedLabels,
+            BiPredicate<Class<?>, Map<String, String>> excludes) {
+        List<Method> testMethods = Arrays.stream(testClass.getMethods())
+                .filter(m -> m.isAnnotationPresent(Test.class))
+                .sorted(Comparator.comparing(Method::getName))
+                .filter(m -> {
+                    Map<String, String> methodLabels = new 
HashMap<>(inheritedLabels);
+                    methodLabels.put("test", m.getName());
+                    return !excludes.test(testClass, methodLabels);
+                })
+                .collect(Collectors.toList());
+        if (testMethods.isEmpty()) {
+            return Stream.empty();
+        }
+        return Stream.of(DynamicContainer.dynamicContainer(
+                testClass.getSimpleName(),
+                testMethods.stream()
+                        .map(method -> 
DynamicTest.dynamicTest(method.getName(), () -> {
+                            Object testInstance = 
injector.getInstance(testClass);
+                            try {
+                                method.invoke(testInstance);
+                            } catch (InvocationTargetException e) {
+                                throw e.getCause() != null ? e.getCause() : e;
+                            }
+                        }))));
+    }
+}
diff --git 
a/testing/matrix-testsuite/src/main/java/org/apache/axiom/testutils/suite/Test.java
 
b/testing/matrix-testsuite/src/main/java/org/apache/axiom/testutils/suite/Test.java
new file mode 100644
index 000000000..dfe0e6337
--- /dev/null
+++ 
b/testing/matrix-testsuite/src/main/java/org/apache/axiom/testutils/suite/Test.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.axiom.testutils.suite;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a method as a test case inside a class registered with {@link 
MatrixTestContainer}. This
+ * annotation is intentionally distinct from JUnit 5's {@code @Test} to 
prevent Surefire or other
+ * runners from discovering and executing the annotated class directly as a 
standalone JUnit test.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface Test {}

Reply via email to