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 {}