Repository: jakartaee/data
Pull request #83 (2022-11-17 00:18:22)

Write unit tests for TCK Frameworks
Introduce unit tests to verify behavior of the `ee.jakarta.tck.data.framework.*` packages.

We want to verify the following: 
- TCKArchiveProcessor correctly adds framework packages based on archive type and test class annotations.
- AssertionNameGenerator correctly modifies test classes with the platform, and test methods with an assertion ID
- URLBuilder correctly constructs a URL with given paths and queries. 

This testing relieved some edge cases that I had to fix.


Patch:
diff --git a/api/src/main/java/jakarta/data/repository/Pageable.java b/api/src/main/java/jakarta/data/repository/Pageable.java
index 699aafc1..27922066 100644
--- a/api/src/main/java/jakarta/data/repository/Pageable.java
+++ b/api/src/main/java/jakarta/data/repository/Pageable.java
@@ -199,7 +199,7 @@ public interface Pageable {
      * @param pageNumber The page number
      * @return a new instance of <code>Pageable</code>. This method never returns <code>null</code>.
      */
-    Pageable page(long pageNumber);
+    Pageable newPage(long pageNumber);
 
     /**
      * <p>Creates a new <code>Pageable</code> instance representing the same
@@ -208,7 +208,7 @@ public interface Pageable {
      * @param maxPageSize the number of query results in a full page.
      * @return a new instance of <code>Pageable</code>. This method never returns <code>null</code>.
      */
-    Pageable size(int maxPageSize);
+    Pageable newSize(int maxPageSize);
 
     /**
      * <p>Creates a new <code>Pageable</code> instance representing the same
diff --git a/api/src/main/java/jakarta/data/repository/Pagination.java b/api/src/main/java/jakarta/data/repository/Pagination.java
index 99ecabf9..f4a5e670 100644
--- a/api/src/main/java/jakarta/data/repository/Pagination.java
+++ b/api/src/main/java/jakarta/data/repository/Pagination.java
@@ -148,12 +148,12 @@ final class Pagination implements Pageable {
     }
 
     @Override
-    public Pageable page(long pageNumber) {
+    public Pageable newPage(long pageNumber) {
         return new Pagination(pageNumber, size, sorts, mode, cursor);
     }
 
     @Override
-    public Pageable size(int maxPageSize) {
+    public Pageable newSize(int maxPageSize) {
         return new Pagination(page, maxPageSize, sorts, mode, cursor);
     }
 
diff --git a/tck/src/main/java/ee/jakarta/tck/data/framework/junit/anno/Assertion.java b/tck/src/main/java/ee/jakarta/tck/data/framework/junit/anno/Assertion.java
index 6d7405fe..cc5364b4 100644
--- a/tck/src/main/java/ee/jakarta/tck/data/framework/junit/anno/Assertion.java
+++ b/tck/src/main/java/ee/jakarta/tck/data/framework/junit/anno/Assertion.java
@@ -30,5 +30,5 @@ import java.lang.annotation.RetentionPolicy;
 public @interface Assertion {
     String id();
 
-    String strategy();
+    String strategy() default "";
 }
diff --git a/tck/src/main/java/ee/jakarta/tck/data/framework/junit/extensions/AssertionNameGenerator.java b/tck/src/main/java/ee/jakarta/tck/data/framework/junit/extensions/AssertionNameGenerator.java
index 1a15874a..664e0f1f 100644
--- a/tck/src/main/java/ee/jakarta/tck/data/framework/junit/extensions/AssertionNameGenerator.java
+++ b/tck/src/main/java/ee/jakarta/tck/data/framework/junit/extensions/AssertionNameGenerator.java
@@ -15,82 +15,74 @@
  */
 package ee.jakarta.tck.data.framework.junit.extensions;
 
-import java.lang.annotation.Annotation;
 import java.lang.reflect.Method;
-import java.util.Arrays;
-
 import org.junit.jupiter.api.DisplayNameGenerator;
 
 import ee.jakarta.tck.data.framework.junit.anno.Assertion;
-import ee.jakarta.tck.data.framework.junit.anno.Core;
-import ee.jakarta.tck.data.framework.junit.anno.Full;
 import ee.jakarta.tck.data.framework.junit.anno.Standalone;
-import ee.jakarta.tck.data.framework.junit.anno.Web;
 
 /**
  * This custom JUnit name generator will append the {@code @Assertion.id}
- * if one exists to the name of the test at runtime.
+ * if one exists to the name of the test method at runtime.
  * 
- * We will also append the platform the test ran on by using a combination of
- * annotations and system properties. 
+ * This custom JUnit name generator will also append the platform 
+ * the test ran on by using the system property jakarta.tck.platform. 
  * 
  * @see ee.jakarta.tck.data.framework.junit.anno.Assertion
- * @see ee.jakarta.tck.data.framework.junit.anno
+ * @see ee.jakarta.tck.data.framework.junit.anno.Standalone
  */
-public class AssertionNameGenerator extends DisplayNameGenerator.Standard implements DisplayNameGenerator {
+public class AssertionNameGenerator extends DisplayNameGenerator.Simple implements DisplayNameGenerator {
     
     /**
      * Optional property that can be provided by test runners to append the platform to the method name.
      * This is useful if an test aggregator is being used to run multiple platforms. 
      * Having the platform name appended to the test will help distinguish the multiple runs.
      */
-    private static final String platform = System.getProperty("jakarta.tck.platform");
+    public static final String platformProperty = "jakarta.tck.platform";
     
     @Override
-    public String generateDisplayNameForClass(Class<?> testClass) {  
+    public String generateDisplayNameForClass(Class<?> testClass) {
         //If user provided a platform, then use that
+        String platform = System.getProperty(platformProperty);
         if(platform != null && !platform.isEmpty()) {
             return testClass.getSimpleName() + "#" + platform.replace("#", "");
         }
         
-        //Otherwise, use the annotation on the class
-        return testClass.getSimpleName() + getPlatform(testClass);
+        //If standalone test is going to be deployed to a server make it clear
+        boolean isStandalone = Boolean.getBoolean(StandaloneExtension.isStandaloneProperty);
+        if(testClass.isAnnotationPresent(Standalone.class) && !isStandalone) {
+            return testClass.getSimpleName() + "#Deployed";
+        }
+        
+        //Otherwise, use the super class
+        return super.generateDisplayNameForClass(testClass);
     }
     
     @Override
     public String generateDisplayNameForNestedClass(Class<?> nestedClass) {
         //If user provided a platform, then use that
+        String platform = System.getProperty(platformProperty);
         if(platform != null && !platform.isEmpty()) {
             return nestedClass.getSimpleName() + "#" + platform.replace("#", "");
         }
         
-        //Otherwise, use the annotation on the class
-        return nestedClass.getSimpleName() + getPlatform(nestedClass);
+        //If standalone test is going to be deployed to a server make it clear
+        boolean isStandalone = Boolean.getBoolean(StandaloneExtension.isStandaloneProperty);
+        if(nestedClass.isAnnotationPresent(Standalone.class) && !isStandalone) {
+            return nestedClass.getSimpleName() + "#Deployed";
+        }
+        
+        //Otherwise, use the super class
+        return super.generateDisplayNameForNestedClass(nestedClass);
     }
     
     @Override
     public String generateDisplayNameForMethod(Class<?> testClass, Method testMethod) {
-        String standardDisplayName = super.generateDisplayNameForMethod(testClass, testMethod);
+        
         Assertion instance = testMethod.getAnnotation(Assertion.class);
         if(instance != null) {
             return testMethod.getName() + "@Assertion.id:" + instance.id();
         }
-        return standardDisplayName;
-    }
-    
-    private String getPlatform(Class<?> testClass) {
-        //If the test is annotated with Standalone, but we are going to deploy the test on a server.
-        if (testClass.isAnnotationPresent(Standalone.class) && !Boolean.getBoolean(StandaloneExtension.isStandaloneProperty)) {
-            return "#DeployedStandalone";
-        }
-        
-        //Otherwise, use the annoation from the class
-        for(Class<? extends Annotation> annotation : Arrays.asList(Standalone.class, Core.class, Web.class, Full.class)) {
-            if(testClass.isAnnotationPresent(annotation)) {
-                return "#" + annotation.getSimpleName();
-            }
-        }
-        
-        return ""; //Otherwise, ignore the platform
+        return super.generateDisplayNameForMethod(testClass, testMethod);
     }
 }
diff --git a/tck/src/main/java/ee/jakarta/tck/data/framework/servlet/URLBuilder.java b/tck/src/main/java/ee/jakarta/tck/data/framework/servlet/URLBuilder.java
index 26dfd5a0..64b01ac7 100644
--- a/tck/src/main/java/ee/jakarta/tck/data/framework/servlet/URLBuilder.java
+++ b/tck/src/main/java/ee/jakarta/tck/data/framework/servlet/URLBuilder.java
@@ -19,8 +19,7 @@ import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.logging.Logger;
@@ -33,14 +32,16 @@ public class URLBuilder {
     
     private static final Logger log = Logger.getLogger(URLBuilder.class.getCanonicalName());
 
-    private URL baseURL;
-    private List<String> paths;
-    private Map<String, String> queries;
+    private final URL baseURL;
+    
+    //Use list/map that maintains insertion order
+    private final ArrayList<String> paths;
+    private final LinkedHashMap<String, String> queries;
 
     /*
      * Private constructor - builder class
      */
-    private URLBuilder(URL baseURL, List<String> paths, Map<String, String> queries) {
+    private URLBuilder(URL baseURL, ArrayList<String> paths, LinkedHashMap<String, String> queries) {
         this.baseURL = baseURL;
         this.paths = paths;
         this.queries = queries;
@@ -66,23 +67,22 @@ public class URLBuilder {
      * @return an instance of URLBuilder
      */
     public static URLBuilder fromURL(URL url) {
-        List<String> paths = null;
-        Map<String, String> queries = null;
-
-        String strPaths = url.getPath();
-        if (strPaths != null && strPaths != "") {
-            paths = new ArrayList<>();
-            for (String path : strPaths.split("/")) {
-                paths.add(path);
-            }
+        log.finer("Entering URLBuilder.fromURL(" + url.toString() + ")");
+        
+        ArrayList<String> paths = new ArrayList<>(Arrays.asList(url.getPath().split("/")));
+        LinkedHashMap<String, String> queries = new LinkedHashMap<>();
+        
+        while(paths.remove("")) {
+            //Do nothing
         }
 
         String strQueries = url.getQuery();
-        if (strQueries != null && strQueries != "") {
-            queries = new HashMap<>();
+        if (strQueries != null) {
             for (String query : strQueries.split("&")) {
-                int equalIndex = query.indexOf("=");
-                queries.put(query.substring(0, equalIndex), query.substring(equalIndex, query.length()));
+                String[] keyValue = query.split("=");
+                if(keyValue.length == 2) {
+                    queries.put(keyValue[0], keyValue[1]);
+                }
             }
         }
 
@@ -92,8 +92,10 @@ public class URLBuilder {
         } catch (MalformedURLException e) {
             throw new RuntimeException("Unable to create a baseURL", e);
         }
-
-        return new URLBuilder(baseURL, paths, queries);
+        
+        URLBuilder result = new URLBuilder(baseURL, paths, queries);
+        log.finer("Exiting URLBuilder.fromURL " + result.toString());
+        return result;
     }
 
     /**
@@ -105,15 +107,12 @@ public class URLBuilder {
      * 
      * returns: http://localhost:80/servlet/endpoint1/
      * </pre>
-     * @param path - The path to append
+     * 
+     * @param paths - The paths to append
      * @return This URL builder object
      */
-    public URLBuilder withPath(String path) {
-        if (this.paths == null) {
-            this.paths = new ArrayList<>(Arrays.asList(path));
-        } else {
-            this.paths.add(path);
-        }
+    public URLBuilder withPath(String... paths) {
+        this.paths.addAll(Arrays.asList(paths));
         return this;
     }
 
@@ -132,11 +131,7 @@ public class URLBuilder {
      * @return This URL builder object
      */
     public URLBuilder withQuery(String key, String value) {
-        if (this.queries == null) {
-            this.queries = new HashMap<>(Collections.singletonMap(key, value));
-        } else {
-            this.queries.put(key, value);
-        }
+        this.queries.put(key, value);
         return this;
     }
 
@@ -152,7 +147,7 @@ public class URLBuilder {
         extendedURL = extendQuery(extendedURL, queries);
         extendedURL = extendPath(extendedURL, paths);
         
-        log.info("Built extended URL: " + extendedURL.toString());
+        log.info("Built URL: " + extendedURL.toString());
 
         return extendedURL;
     }
@@ -162,8 +157,7 @@ public class URLBuilder {
             return baseURL;
 
         // Get existing query part
-        boolean existingQuery = baseURL.getQuery() != null;
-        String extendedQuery = existingQuery ? "?" + baseURL.getQuery() : "?";
+        String extendedQuery = baseURL.getQuery() != null ? "?" + baseURL.getQuery() : "?";
 
         // Append additional query parts
         for (Map.Entry<String, String> entry : queries.entrySet()) {
@@ -186,18 +180,19 @@ public class URLBuilder {
         if (paths == null)
             return baseURL;
 
-        // Get existing path part
-        boolean existingPath = baseURL.getPath() != null;
-        String extendedPath = existingPath ? baseURL.getPath() : "";
+        // baseURL could have an existing path, typically "/"
+        String extendedPath = baseURL.getPath().isBlank() ? "/" : baseURL.getPath();
 
         // Append additional path parts
         for (String pathPart : paths) {
-            pathPart = pathPart.replace("/", ""); // Remove existing /
+            pathPart = pathPart.replace("/", ""); // Remove existing / if it exists
             extendedPath += pathPart + "/";
         }
 
         // cleanup trailing symbol(s)
-        extendedPath = extendedPath.substring(0, extendedPath.length() - 1);
+        if(extendedPath.lastIndexOf("/") == extendedPath.length() -1 ) {
+            extendedPath = extendedPath.substring(0, extendedPath.length() - 1);
+        }
 
         // Generate and return new URL
         try {
@@ -207,4 +202,17 @@ public class URLBuilder {
             throw new RuntimeException(e);
         }
     }
+    
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("URLBuilder: ");
+        builder.append(" baseURL:[" + baseURL.toString() + "]");
+        builder.append(", Paths:" + paths.toString());
+        builder.append(", Queries: {");
+        for(String key : queries.keySet()) {
+            builder.append("[" + key + ", " + queries.get(key) + "]");
+        }
+        builder.append("}");
+        return builder.toString();
+    }
 }


Test patch:
diff --git a/api/src/test/java/jakarta/data/repository/KeysetPageableTest.java b/api/src/test/java/jakarta/data/repository/KeysetPageableTest.java
index 365331e6..ad9410df 100644
--- a/api/src/test/java/jakarta/data/repository/KeysetPageableTest.java
+++ b/api/src/test/java/jakarta/data/repository/KeysetPageableTest.java
@@ -66,7 +66,7 @@ class KeysetPageableTest {
     @Test
     @DisplayName("Should include keyset values in previous Pageable")
     void shouldCreatePageableBeforeKeyset() {
-        Pageable pageable = Pageable.ofSize(30).sortBy(Sort.desc("yearBorn"), Sort.asc("ssn")).beforeKeyset(1991, "123-45-6789").page(10);
+        Pageable pageable = Pageable.ofSize(30).sortBy(Sort.desc("yearBorn"), Sort.asc("ssn")).beforeKeyset(1991, "123-45-6789").newPage(10);
 
         assertSoftly(softly -> {
             softly.assertThat(pageable.size()).isEqualTo(30);
@@ -144,7 +144,7 @@ class KeysetPageableTest {
         Pageable pageable25p1s0a1 = Pageable.ofSize(25).afterKeyset("keyval1", '2', 3);
         Pageable pageable25p1s0b1 = Pageable.ofSize(25).beforeKeyset("keyval1", '2', 3);
         Pageable pageable25p1s0a1match = Pageable.ofSize(25).afterKeysetCursor(new KeysetCursor("keyval1", '2', 3));
-        Pageable pageable25p2s0a1 = Pageable.ofPage(2).size(25).afterKeysetCursor(new KeysetCursor("keyval1", '2', 3));
+        Pageable pageable25p2s0a1 = Pageable.ofPage(2).newSize(25).afterKeysetCursor(new KeysetCursor("keyval1", '2', 3));
         Pageable pageable25p1s1a1 = Pageable.ofSize(25).sortBy(Sort.desc("d"), Sort.asc("a"), Sort.asc("id")).afterKeyset("keyval1", '2', 3);
         Pageable pageable25p1s2a1 = Pageable.ofSize(25).sortBy(Sort.desc("d"), Sort.asc("a"), Sort.desc("id")).afterKeyset("keyval1", '2', 3);
         Pageable pageable25p1s0a2 = Pageable.ofSize(25).afterKeyset("keyval2", '2', 3);
@@ -203,7 +203,7 @@ class KeysetPageableTest {
     @DisplayName("Keyset should be replaced on new instance of Pageable")
     public void shouldReplaceKeyset() {
         Pageable p1 = Pageable.ofSize(30).sortBy(Sort.asc("lastName"), Sort.asc("firstName"), Sort.asc("id"))
-                                         .afterKeyset("last1", "fname1", 100).page(12);
+                                         .afterKeyset("last1", "fname1", 100).newPage(12);
         Pageable p2 = p1.beforeKeyset("lname2", "fname2", 200);
 
         assertSoftly(softly -> {
diff --git a/api/src/test/java/jakarta/data/repository/PageableTest.java b/api/src/test/java/jakarta/data/repository/PageableTest.java
index 51084133..102c0fb8 100644
--- a/api/src/test/java/jakarta/data/repository/PageableTest.java
+++ b/api/src/test/java/jakarta/data/repository/PageableTest.java
@@ -33,7 +33,7 @@ class PageableTest {
     @Test
     @DisplayName("Should correctly paginate")
     void shouldCreatePageable() {
-        Pageable pageable = Pageable.ofPage(2).size(6);
+        Pageable pageable = Pageable.ofPage(2).newSize(6);
 
         assertSoftly(softly -> {
             softly.assertThat(pageable.page()).isEqualTo(2L);
@@ -55,7 +55,7 @@ class PageableTest {
     @Test
     @DisplayName("Should navigate next")
     void shouldNext() {
-        Pageable pageable = Pageable.ofSize(1).page(2);
+        Pageable pageable = Pageable.ofSize(1).newPage(2);
         Pageable next = pageable.next();
 
         assertSoftly(softly -> {
@@ -95,8 +95,8 @@ class PageableTest {
         Pageable p1 = Pageable.ofPage(1);
         assertThatIllegalArgumentException().isThrownBy(() -> Pageable.ofPage(0));
         assertThatIllegalArgumentException().isThrownBy(() -> Pageable.ofPage(-1));
-        assertThatIllegalArgumentException().isThrownBy(() -> p1.size(-1));
-        assertThatIllegalArgumentException().isThrownBy(() -> p1.size(0));
+        assertThatIllegalArgumentException().isThrownBy(() -> p1.newSize(-1));
+        assertThatIllegalArgumentException().isThrownBy(() -> p1.newSize(0));
         assertThatIllegalArgumentException().isThrownBy(() -> Pageable.ofSize(0));
         assertThatIllegalArgumentException().isThrownBy(() -> Pageable.ofSize(-1));
     }
@@ -155,8 +155,8 @@ class PageableTest {
     @Test
     @DisplayName("Page number should be replaced on new instance of Pageable")
     public void shouldReplacePage() {
-        Pageable p6 = Pageable.ofSize(75).page(6).sortBy(Sort.desc("price"));
-        Pageable p7 = p6.page(7);
+        Pageable p6 = Pageable.ofSize(75).newPage(6).sortBy(Sort.desc("price"));
+        Pageable p7 = p6.newPage(7);
 
         assertSoftly(softly -> {
             softly.assertThat(p7.page()).isEqualTo(7L);
@@ -171,8 +171,8 @@ class PageableTest {
     @Test
     @DisplayName("Size should be replaced on new instance of Pageable")
     public void shouldReplaceSize() {
-        Pageable s90 = Pageable.ofPage(4).size(90);
-        Pageable s80 = s90.size(80);
+        Pageable s90 = Pageable.ofPage(4).newSize(90);
+        Pageable s80 = s90.newSize(80);
 
         assertSoftly(softly -> {
             softly.assertThat(s80.size()).isEqualTo(80);
diff --git a/tck/src/main/java/ee/jakarta/tck/data/framework/signature/DataSignatureTestRunner.java b/tck/src/main/java/ee/jakarta/tck/data/framework/signature/DataSignatureTestRunner.java
index 800e82aa..70b1bd1f 100644
--- a/tck/src/main/java/ee/jakarta/tck/data/framework/signature/DataSignatureTestRunner.java
+++ b/tck/src/main/java/ee/jakarta/tck/data/framework/signature/DataSignatureTestRunner.java
@@ -68,8 +68,10 @@ public class DataSignatureTestRunner extends SigTestEE {
         return new String[] { "jakarta.data", "jakarta.data.repository" };
     }
 
+
     /**
      * Returns the classpath for the packages we are interested in.
+     * @return the classpath as a colon (:) delimited string
      */
     protected String getClasspath() {
         final String defined = System.getProperty("signature.sigTestClasspath");
diff --git a/tck/src/test/java/ee/jakarta/tck/data/framework/arquillian/extensions/TCKArchiveProcessorTest.java b/tck/src/test/java/ee/jakarta/tck/data/framework/arquillian/extensions/TCKArchiveProcessorTest.java
new file mode 100644
index 00000000..7205519b
--- /dev/null
+++ b/tck/src/test/java/ee/jakarta/tck/data/framework/arquillian/extensions/TCKArchiveProcessorTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2022 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package ee.jakarta.tck.data.framework.arquillian.extensions;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.jboss.arquillian.test.spi.TestClass;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.jboss.shrinkwrap.impl.base.path.BasicPath;
+import org.junit.jupiter.api.Test;
+
+import ee.jakarta.tck.data.framework.junit.anno.Core;
+import ee.jakarta.tck.data.framework.junit.anno.Full;
+import ee.jakarta.tck.data.framework.junit.anno.Signature;
+import ee.jakarta.tck.data.framework.junit.anno.Web;
+
+/**
+ * Tests to ensure we correctly append framework classes to archives that are deployed using Arquillian
+ */
+public class TCKArchiveProcessorTest {
+    private static final TCKArchiveProcessor processor = new TCKArchiveProcessor();
+    
+    @Core
+    private static class TestCoreClass {}
+    
+    @Test
+    public void processorShouldAlwaysAppendAnnotationsAndExtensions() {
+        JavaArchive testJar = ShrinkWrap.create(JavaArchive.class);
+        assertTrue(testJar.getContent().isEmpty());
+        
+        processor.process(testJar, new TestClass(TestCoreClass.class));
+        assertFalse(testJar.getContent().isEmpty());
+        
+        // Should append Anno and Extension packages
+        assertTrue(testJar.getContent().containsKey(new BasicPath("/ee/jakarta/tck/data/framework/junit/anno")));    
+        assertTrue(testJar.getContent().containsKey(new BasicPath("/ee/jakarta/tck/data/framework/junit/extensions")));
+        
+        //Should not append servlet or signature packages
+        assertFalse(testJar.getContent().containsKey(new BasicPath("/ee/jakarta/tck/data/framework/servlet")));
+        assertFalse(testJar.getContent().containsKey(new BasicPath("/ee/jakarta/tck/data/framework/signature")));
+    }
+    
+    @Web
+    private static class TestWebClass {}
+    
+    @Test
+    public void processorShouldAppendServletFrameworkForWebArchives() {
+        WebArchive testWAR = ShrinkWrap.create(WebArchive.class);
+        assertTrue(testWAR.getContent().isEmpty());
+        
+        processor.process(testWAR, new TestClass(TestWebClass.class));
+        assertFalse(testWAR.getContent().isEmpty());
+        
+        // Should append Anno, Extension, and Servlet packages
+        assertTrue(testWAR.getContent().containsKey(new BasicPath("/WEB-INF/classes/ee/jakarta/tck/data/framework/junit/anno")));      
+        assertTrue(testWAR.getContent().containsKey(new BasicPath("/WEB-INF/classes/ee/jakarta/tck/data/framework/junit/extensions")));      
+        assertTrue(testWAR.getContent().containsKey(new BasicPath("/WEB-INF/classes/ee/jakarta/tck/data/framework/servlet")));    
+        
+        // Should not append signature packages
+        assertFalse(testWAR.getContent().containsKey(new BasicPath("/WEB-INF/classes/ee/jakarta/tck/data/framework/signature")));
+    }
+    
+    @Signature
+    @Full
+    private static class TestSignatureClass {}
+    
+    @Test
+    public void processorShouldAppendSignatureFrameworkForSignatureArchvies() {
+        WebArchive testWAR = ShrinkWrap.create(WebArchive.class);
+        assertTrue(testWAR.getContent().isEmpty());
+        
+        processor.process(testWAR, new TestClass(TestSignatureClass.class));
+        assertFalse(testWAR.getContent().isEmpty());
+        
+        // Should append Anno, Extension, and Servlet packages
+        assertTrue(testWAR.getContent().containsKey(new BasicPath("/WEB-INF/classes/ee/jakarta/tck/data/framework/junit/anno")));      
+        assertTrue(testWAR.getContent().containsKey(new BasicPath("/WEB-INF/classes/ee/jakarta/tck/data/framework/junit/extensions")));      
+        assertTrue(testWAR.getContent().containsKey(new BasicPath("/WEB-INF/classes/ee/jakarta/tck/data/framework/servlet"))); 
+        assertTrue(testWAR.getContent().containsKey(new BasicPath("/WEB-INF/classes/ee/jakarta/tck/data/framework/signature")));
+    }
+    
+}
diff --git a/tck/src/test/java/ee/jakarta/tck/data/framework/junit/extensions/AssertionNameGeneratorTest.java b/tck/src/test/java/ee/jakarta/tck/data/framework/junit/extensions/AssertionNameGeneratorTest.java
new file mode 100644
index 00000000..34abbcce
--- /dev/null
+++ b/tck/src/test/java/ee/jakarta/tck/data/framework/junit/extensions/AssertionNameGeneratorTest.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (c) 2022 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package ee.jakarta.tck.data.framework.junit.extensions;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Set;
+
+import org.junit.jupiter.api.DisplayNameGenerator;
+import org.junit.jupiter.api.DisplayNameGenerator.Simple;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import ee.jakarta.tck.data.framework.junit.anno.Assertion;
+import ee.jakarta.tck.data.framework.junit.anno.Core;
+import ee.jakarta.tck.data.framework.junit.anno.Standalone;
+import ee.jakarta.tck.data.framework.junit.anno.Web;
+
+/**
+ * This custom JUnit name generator will append the {@code @Assertion.id}
+ * if one exists to the name of the test at runtime.
+ * 
+ * We will also append the platform the test ran on by using a combination of
+ * annotations and system properties. 
+ * 
+ * @see ee.jakarta.tck.data.framework.junit.anno.Assertion
+ * @see ee.jakarta.tck.data.framework.junit.anno
+ */
+public class AssertionNameGeneratorTest {
+    
+    @Web
+    public static class TestClass {
+        
+        @Assertion(id = "1.0")
+        public void testMethod() {
+            //Does nothing
+        }
+        
+        public void testSimpleMethod() {
+            //Does nothing
+        }
+        
+        public static class NestedSimpleTestClass {
+            public void testSimpleMethodInNestedClass() {
+                //Does nothing
+            }
+        }
+        
+        @Core
+        public static class NestedCoreTestClass {
+            
+            @Assertion(id = "2.0")
+            public void testMethodInNestedClass() {
+                //Does nothing
+            }
+        }
+        
+        @Standalone
+        public static class NestedStandaloneTestClass {
+            
+            @Assertion(id = "3.0")
+            public void TestMethodInNestedClass() {
+                //Does nothing
+            }
+        }
+    }
+    
+    @ParameterizedTest
+    @ValueSource(strings =  { "standalone", "core", "web", "full", "something" })
+    public void testGenerateDisplayNameForClassWithPlatform(String platform) {
+        for(boolean isStandalone : Set.of(true, false)) {
+            AssertionNameGenerator generator = createNewAssertionNameGenerator(platform, isStandalone);
+
+            String expectedClassName = TestClass.class.getSimpleName() + "#" + platform;
+            String actualClassName = generator.generateDisplayNameForClass(TestClass.class);
+            assertEquals(expectedClassName, actualClassName);
+        }
+    }
+    
+    @ParameterizedTest
+    @NullAndEmptySource
+    public void testGenerateDisplayNameForClassWithoutPlatform(String platform) {
+        Simple simpleGenerator = new DisplayNameGenerator.Simple();
+        AssertionNameGenerator generator;
+        String expectedClassName, actualClassName;
+        
+        //On Server
+        generator = createNewAssertionNameGenerator(platform, false);
+
+        expectedClassName = simpleGenerator.generateDisplayNameForClass(TestClass.class);
+        actualClassName = generator.generateDisplayNameForClass(TestClass.class);
+        assertEquals(expectedClassName, actualClassName);
+        
+        expectedClassName = TestClass.NestedStandaloneTestClass.class.getSimpleName() + "#Deployed";
+        actualClassName = generator.generateDisplayNameForClass(TestClass.NestedStandaloneTestClass.class);
+        assertEquals(expectedClassName, actualClassName);
+        
+        //Standalone
+        generator = createNewAssertionNameGenerator(platform, true);
+
+        expectedClassName = simpleGenerator.generateDisplayNameForClass(TestClass.class);
+        actualClassName = generator.generateDisplayNameForClass(TestClass.class);
+        assertEquals(expectedClassName, actualClassName);
+        
+        expectedClassName = simpleGenerator.generateDisplayNameForClass(TestClass.NestedStandaloneTestClass.class);
+        actualClassName = generator.generateDisplayNameForClass(TestClass.NestedStandaloneTestClass.class);
+        assertEquals(expectedClassName, actualClassName);
+    }
+    
+    @ParameterizedTest
+    @ValueSource(strings =  { "standalone", "core", "web", "full", "something" })
+    public void testGenerateDisplayNameForNestedClassWithPlatform(String platform) {
+        for(boolean isStandalone : Set.of(true, false)) {
+            AssertionNameGenerator generator = createNewAssertionNameGenerator(platform, isStandalone);
+
+            String expectedClassName = TestClass.NestedCoreTestClass.class.getSimpleName() + "#" + platform;
+            String actualClassName = generator.generateDisplayNameForNestedClass(TestClass.NestedCoreTestClass.class);
+            assertEquals(expectedClassName, actualClassName);
+        }
+    }
+    
+    @ParameterizedTest
+    @NullAndEmptySource
+    public void testGenerateDisplayNameForNestedClassWithoutPlatform(String platform) {
+        Simple simpleGenerator = new DisplayNameGenerator.Simple();
+        AssertionNameGenerator generator;
+        String expectedClassName, actualClassName;
+        
+        //On Server
+        generator = createNewAssertionNameGenerator(platform, false);
+
+        expectedClassName = simpleGenerator.generateDisplayNameForNestedClass(TestClass.NestedCoreTestClass.class);
+        actualClassName = generator.generateDisplayNameForNestedClass(TestClass.NestedCoreTestClass.class);
+        assertEquals(expectedClassName, actualClassName);
+        
+        expectedClassName = TestClass.NestedStandaloneTestClass.class.getSimpleName() + "#Deployed";
+        actualClassName = generator.generateDisplayNameForNestedClass(TestClass.NestedStandaloneTestClass.class);
+        assertEquals(expectedClassName, actualClassName);
+        
+        //Standalone
+        generator = createNewAssertionNameGenerator(platform, true);
+
+        expectedClassName = simpleGenerator.generateDisplayNameForNestedClass(TestClass.NestedCoreTestClass.class);
+        actualClassName = generator.generateDisplayNameForNestedClass(TestClass.NestedCoreTestClass.class);
+        assertEquals(expectedClassName, actualClassName);
+        
+        expectedClassName = simpleGenerator.generateDisplayNameForNestedClass(TestClass.NestedStandaloneTestClass.class);
+        actualClassName = generator.generateDisplayNameForNestedClass(TestClass.NestedStandaloneTestClass.class);
+        assertEquals(expectedClassName, actualClassName);
+    }
+    
+    @ParameterizedTest
+    @ValueSource(strings =  { "standalone", "core", "web", "full", "something" })
+    public void testGenerateDisplayNameForMethod(String platform) throws NoSuchMethodException, SecurityException {
+        for(boolean isStandalone : Set.of(true, false)) {
+            AssertionNameGenerator generator = createNewAssertionNameGenerator(platform, isStandalone);
+            
+            String expectedMethodName = "testMethod@Assertion.id:1.0";
+            String actualMethodName = generator.generateDisplayNameForMethod(TestClass.class, TestClass.class.getMethod("testMethod"));
+            assertEquals(expectedMethodName, actualMethodName);
+            
+            String expectedNestedMethodName = "testMethodInNestedClass@Assertion.id:2.0";
+            String actualNestedMethodName = generator.generateDisplayNameForMethod(TestClass.NestedCoreTestClass.class, TestClass.NestedCoreTestClass.class.getMethod("testMethodInNestedClass"));
+            assertEquals(expectedNestedMethodName, actualNestedMethodName);   
+        }
+    }
+    
+    private AssertionNameGenerator createNewAssertionNameGenerator(String platform, boolean isStandalone) {
+        if (platform == null) {
+            System.clearProperty(AssertionNameGenerator.platformProperty);
+        } else {
+            System.setProperty(AssertionNameGenerator.platformProperty, platform);    
+        }
+        
+        System.setProperty(StandaloneExtension.isStandaloneProperty, Boolean.toString(isStandalone));
+        return new AssertionNameGenerator();
+    }
+}
diff --git a/tck/src/test/java/ee/jakarta/tck/data/framework/servlet/URLBuilderTest.java b/tck/src/test/java/ee/jakarta/tck/data/framework/servlet/URLBuilderTest.java
new file mode 100644
index 00000000..0a002f80
--- /dev/null
+++ b/tck/src/test/java/ee/jakarta/tck/data/framework/servlet/URLBuilderTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2022 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package ee.jakarta.tck.data.framework.servlet;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import org.junit.jupiter.api.Test;
+
+public class URLBuilderTest {
+    
+    @Test
+    public void testAppendPath() throws MalformedURLException {
+        URL baseURL = new URL("http://localhost:80/");
+        
+        URL expectedURL = new URL("http://localhost:80/my/custom/url");
+        URL actualURL = URLBuilder.fromURL(baseURL).withPath("my").withPath("custom").withPath("url").build();
+        assertEquals(expectedURL, actualURL);
+        
+        actualURL = URLBuilder.fromURL(baseURL).withPath("my", "custom", "url").build();
+        assertEquals(expectedURL, actualURL);
+        
+        actualURL = URLBuilder.fromURL(baseURL).withPath("my").withPath("custom", "url").build();
+        assertEquals(expectedURL, actualURL);
+        
+        baseURL = new URL("http://localhost:80/my/");
+        actualURL = URLBuilder.fromURL(baseURL).withPath("custom").withPath("url").build();
+        assertEquals(expectedURL, actualURL);
+        
+        actualURL = URLBuilder.fromURL(baseURL).withPath("custom", "url").build();
+        assertEquals(expectedURL, actualURL);
+    }
+    
+    @Test
+    public void testAppendQueries() throws MalformedURLException {
+        URL baseURL = new URL("http://localhost:80/myServlet/");
+        
+        URL expectedURL = new URL("http://localhost:80/myServlet?myKey=myValue");
+        URL actualURL = URLBuilder.fromURL(baseURL).withQuery("myKey", "myValue").build();
+        assertEquals(expectedURL, actualURL);
+        
+        expectedURL = new URL("http://localhost:80/myServlet?myKey=myValue&myKey2=myValue2");
+        actualURL = URLBuilder.fromURL(baseURL).withQuery("myKey", "myValue").withQuery("myKey2", "myValue2").build();
+        assertEquals(expectedURL, actualURL);
+        
+        baseURL = new URL("http://localhost:80/myServlet?myKey=myValue");
+        actualURL = URLBuilder.fromURL(baseURL).withQuery("myKey2", "myValue2").build();
+        assertEquals(expectedURL, actualURL);
+        
+        expectedURL = new URL("http://localhost:80/myServlet?myKey=myValue");
+        actualURL = URLBuilder.fromURL(baseURL).withQuery("myKey", "myValue").build();
+        assertEquals(expectedURL, actualURL);
+    }
+    
+    @Test
+    public void testAppendComplexQueryAndPath() throws MalformedURLException {
+        URL baseURL = new URL("http://localhost:80/myServlet?myKey=myValue");
+        
+        URL expectedURL = new URL("http://localhost:80/myServlet/with/custom/path?myKey=myValue&anotherKey=anotherValue");
+        URL actualURL = URLBuilder.fromURL(baseURL).withPath("with", "custom", "path").withQuery("anotherKey", "anotherValue").build();
+        assertEquals(expectedURL, actualURL);
+    }
+
+}