You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by si...@apache.org on 2018/11/06 15:26:18 UTC

[sling-org-apache-sling-feature-analyser] 01/01: SLING-8078 - New Analyser task which is able to detect Export-Package dependencies between regions

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

simonetripodi pushed a commit to branch SLING-8078
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-feature-analyser.git

commit a26609ddd4e6d5104b89399fad439d4559707384
Author: Simo Tripodi <st...@adobe.com>
AuthorDate: Tue Nov 6 16:25:53 2018 +0100

    SLING-8078 - New Analyser task which is able to detect Export-Package
    dependencies between regions
---
 .../task/impl/AbstractApiRegionsAnalyserTask.java  |  68 +++++++++
 .../feature/analyser/task/impl/ApiRegions.java     | 133 +++++++++++++++++
 .../analyser/task/impl/CheckApiRegions.java        | 158 +--------------------
 .../task/impl/CheckApiRegionsDependencies.java     |  71 +++++++++
 .../apache/sling/feature/scanner/PackageInfo.java  |  13 ++
 .../feature/scanner/impl/BundleDescriptorImpl.java |  17 ++-
 ...ava => AbstractApiRegionsAnalyserTaskTest.java} |  46 +++---
 .../task/impl/CheckApiRegionsDependenciesTest.java |  49 +++++++
 .../analyser/task/impl/CheckApiRegionsTest.java    | 141 +-----------------
 9 files changed, 380 insertions(+), 316 deletions(-)

diff --git a/src/main/java/org/apache/sling/feature/analyser/task/impl/AbstractApiRegionsAnalyserTask.java b/src/main/java/org/apache/sling/feature/analyser/task/impl/AbstractApiRegionsAnalyserTask.java
new file mode 100644
index 0000000..d6b1d80
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/analyser/task/impl/AbstractApiRegionsAnalyserTask.java
@@ -0,0 +1,68 @@
+/*
+ * 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.sling.feature.analyser.task.impl;
+
+import javax.json.stream.JsonParsingException;
+
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.Extensions;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.analyser.task.AnalyserTask;
+import org.apache.sling.feature.analyser.task.AnalyserTaskContext;
+
+public abstract class AbstractApiRegionsAnalyserTask implements AnalyserTask {
+
+    protected static final String API_REGIONS_KEY = "api-regions";
+
+    @Override
+    public final void execute(AnalyserTaskContext ctx) throws Exception {
+        Feature feature = ctx.getFeature();
+
+        // extract and check the api-regions
+
+        Extensions extensions = feature.getExtensions();
+        Extension apiRegionsExtension = extensions.getByName(API_REGIONS_KEY);
+        if (apiRegionsExtension == null) {
+            // no need to be analyzed
+            return;
+        }
+
+        String jsonRepresentation = apiRegionsExtension.getJSON();
+        if (jsonRepresentation == null || jsonRepresentation.isEmpty()) {
+            // no need to be analyzed
+            return;
+        }
+
+        // read the api-regions and create a Sieve data structure for checks
+
+        ApiRegions apiRegions;
+        try {
+            apiRegions = ApiRegions.fromJson(jsonRepresentation);
+        } catch (JsonParsingException e) {
+            ctx.reportError("API Regions '"
+                    + jsonRepresentation
+                    + "' does not represent a valid JSON 'api-regions': "
+                    + e.getMessage());
+            return;
+        }
+
+        execute(apiRegions, ctx);
+    }
+
+    protected abstract void execute(ApiRegions apiRegions, AnalyserTaskContext ctx) throws Exception;
+
+}
diff --git a/src/main/java/org/apache/sling/feature/analyser/task/impl/ApiRegions.java b/src/main/java/org/apache/sling/feature/analyser/task/impl/ApiRegions.java
new file mode 100644
index 0000000..68f2570
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/analyser/task/impl/ApiRegions.java
@@ -0,0 +1,133 @@
+/*
+ * 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.sling.feature.analyser.task.impl;
+
+import java.io.StringReader;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import javax.json.Json;
+import javax.json.stream.JsonParser;
+import javax.json.stream.JsonParser.Event;
+
+/**
+ * A "Sieve" data structure to check exported packages
+ */
+final class ApiRegions {
+
+    private static final String NAME_KEY = "name";
+
+    private static final String EXPORTS_KEY = "exports";
+
+    public static ApiRegions fromJson(String jsonRepresentation) {
+        ApiRegions apiRegions = new ApiRegions();
+
+        // pointers
+        Event event;
+        String region = null;
+        Collection<String> apis = null;
+
+        JsonParser parser = Json.createParser(new StringReader(jsonRepresentation));
+        while (parser.hasNext()) {
+            event = parser.next();
+            if (Event.KEY_NAME == event) {
+                switch (parser.getString()) {
+                    case NAME_KEY:
+                        parser.next();
+                        region = parser.getString();
+                        break;
+
+                    case EXPORTS_KEY:
+                        apis = new LinkedList<>();
+
+                        // start array
+                        parser.next();
+
+                        while (parser.hasNext() && Event.VALUE_STRING == parser.next()) {
+                            String api = parser.getString();
+                            // skip comments
+                            if ('#' != api.charAt(0)) {
+                                apis.add(api);
+                            }
+                        }
+
+                        break;
+
+                    default:
+                        break;
+                }
+            } else if (Event.END_OBJECT == event) {
+                if (region != null && apis != null) {
+                    apiRegions.add(region, apis);
+                }
+
+                region = null;
+                apis = null;
+            }
+        }
+
+        return apiRegions;
+    }
+
+    // class members
+
+    private final Map<String, Set<String>> apis = new TreeMap<>();
+
+    // ctor
+
+    protected ApiRegions() {
+        // it should not be directly instantiated outside this package
+    }
+
+    // methods
+
+    public void add(String region, Collection<String> exportedApis) {
+        apis.computeIfAbsent(region, k -> new TreeSet<>()).addAll(exportedApis);
+    }
+
+    public Iterable<String> getRegions() {
+        return apis.keySet();
+    }
+
+    public Set<String> getApis(String region) {
+        return apis.computeIfAbsent(region, k -> Collections.emptySet());
+    }
+
+    public void remove(String packageName) {
+        apis.values().forEach(apis -> apis.remove(packageName));
+    }
+
+    public boolean isEmpty() {
+        for (Set<String> packages : apis.values()) {
+            if (!packages.isEmpty()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return apis.toString().replace(',', '\n');
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckApiRegions.java b/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckApiRegions.java
index cb0502f..6ea7f82 100644
--- a/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckApiRegions.java
+++ b/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckApiRegions.java
@@ -16,37 +16,16 @@
  */
 package org.apache.sling.feature.analyser.task.impl;
 
-import java.io.StringReader;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.Formatter;
-import java.util.LinkedList;
-import java.util.Map;
 import java.util.Set;
-import java.util.TreeMap;
-import java.util.TreeSet;
 
-import javax.json.Json;
-import javax.json.stream.JsonParser;
-import javax.json.stream.JsonParser.Event;
-import javax.json.stream.JsonParsingException;
-
-import org.apache.sling.feature.Extension;
-import org.apache.sling.feature.Extensions;
-import org.apache.sling.feature.Feature;
-import org.apache.sling.feature.analyser.task.AnalyserTask;
 import org.apache.sling.feature.analyser.task.AnalyserTaskContext;
 import org.apache.sling.feature.scanner.BundleDescriptor;
 import org.apache.sling.feature.scanner.FeatureDescriptor;
 import org.apache.sling.feature.scanner.PackageInfo;
 
-public class CheckApiRegions implements AnalyserTask {
-
-    private static final String API_REGIONS_KEY = "api-regions";
-
-    private static final String NAME_KEY = "name";
-
-    private static final String EXPORTS_KEY = "exports";
+public class CheckApiRegions extends AbstractApiRegionsAnalyserTask {
 
     @Override
     public String getId() {
@@ -59,38 +38,8 @@ public class CheckApiRegions implements AnalyserTask {
     }
 
     @Override
-    public void execute(AnalyserTaskContext ctx) throws Exception {
-        Feature feature = ctx.getFeature();
-
-        // extract and check the api-regions
-
-        Extensions extensions = feature.getExtensions();
-        Extension apiRegionsExtension = extensions.getByName(API_REGIONS_KEY);
-        if (apiRegionsExtension == null) {
-            // no need to be analyzed
-            return;
-        }
-
-        String jsonRepresentation = apiRegionsExtension.getJSON();
-        if (jsonRepresentation == null || jsonRepresentation.isEmpty()) {
-            // no need to be analyzed
-            return;
-        }
-
-        // read the api-regions and create a Sieve data structure for checks
-
-        ApiRegions apiRegions;
-        try {
-            apiRegions = fromJson(jsonRepresentation);
-        } catch (JsonParsingException e) {
-            ctx.reportError("API Regions '"
-                    + jsonRepresentation
-                    + "' does not represent a valid JSON 'api-regions': "
-                    + e.getMessage());
-            return;
-        }
-
-        // then, for each bundle, get the Export-Package and process the packages
+    protected void execute(ApiRegions apiRegions, AnalyserTaskContext ctx) throws Exception {
+        // for each bundle, get the Export-Package and process the packages
 
         FeatureDescriptor featureDescriptor = ctx.getFeatureDescriptor();
         for (BundleDescriptor bundleDescriptor : featureDescriptor.getBundleDescriptors()) {
@@ -110,7 +59,7 @@ public class CheckApiRegions implements AnalyserTask {
                     Formatter formatter = new Formatter();
                     formatter.format("Region '%s' defined in feature '%s' declares %s package%s which %s not exported by any bundle:%n",
                                      region,
-                                     feature.getId(),
+                                     ctx.getFeature().getId(),
                                      apis.size(),
                                      getExtension(apis, "", "s"),
                                      getExtension(apis, "is", "are"));
@@ -130,103 +79,4 @@ public class CheckApiRegions implements AnalyserTask {
         return collection.size() > 1 ? plural : singular;
     }
 
-    private static ApiRegions fromJson(String jsonRepresentation) {
-        ApiRegions apiRegions = new ApiRegions();
-
-        // pointers
-        Event event;
-        String region = null;
-        Collection<String> apis = null;
-
-        JsonParser parser = Json.createParser(new StringReader(jsonRepresentation));
-        while (parser.hasNext()) {
-            event = parser.next();
-            if (Event.KEY_NAME == event) {
-                switch (parser.getString()) {
-                    case NAME_KEY:
-                        parser.next();
-                        region = parser.getString();
-                        break;
-
-                    case EXPORTS_KEY:
-                        apis = new LinkedList<>();
-
-                        // start array
-                        parser.next();
-
-                        while (parser.hasNext() && Event.VALUE_STRING == parser.next()) {
-                            String api = parser.getString();
-                            // skip comments
-                            if ('#' != api.charAt(0)) {
-                                apis.add(api);
-                            }
-                        }
-
-                        break;
-
-                    default:
-                        break;
-                }
-            } else if (Event.END_OBJECT == event) {
-                if (region != null && apis != null) {
-                    apiRegions.add(region, apis);
-                }
-
-                region = null;
-                apis = null;
-            }
-        }
-
-        return apiRegions;
-    }
-
-    // the Sieve data structure to check exported packages
-
-    private static final class ApiRegions {
-
-        // class members
-
-        private final Map<String, Set<String>> apis = new TreeMap<>();
-
-        // ctor
-
-        protected ApiRegions() {
-            // it should not be directly instantiated outside this package
-        }
-
-        // methods
-
-        public void add(String region, Collection<String> exportedApis) {
-            apis.computeIfAbsent(region, k -> new TreeSet<>()).addAll(exportedApis);
-        }
-
-        public Iterable<String> getRegions() {
-            return apis.keySet();
-        }
-
-        public Set<String> getApis(String region) {
-            return apis.computeIfAbsent(region, k -> Collections.emptySet());
-        }
-
-        public void remove(String packageName) {
-            apis.values().forEach(apis -> apis.remove(packageName));
-        }
-
-        public boolean isEmpty() {
-            for (Set<String> packages : apis.values()) {
-                if (!packages.isEmpty()) {
-                    return false;
-                }
-            }
-            return true;
-        }
-
-        @Override
-        public String toString() {
-            return apis.toString().replace(',', '\n');
-        }
-
-    }
-
-
 }
diff --git a/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckApiRegionsDependencies.java b/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckApiRegionsDependencies.java
new file mode 100644
index 0000000..93dfb7c
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckApiRegionsDependencies.java
@@ -0,0 +1,71 @@
+/*
+ * 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.sling.feature.analyser.task.impl;
+
+import java.util.Set;
+
+import org.apache.sling.feature.analyser.task.AnalyserTaskContext;
+import org.apache.sling.feature.scanner.BundleDescriptor;
+import org.apache.sling.feature.scanner.FeatureDescriptor;
+import org.apache.sling.feature.scanner.PackageInfo;
+import org.osgi.framework.Constants;
+
+public class CheckApiRegionsDependencies extends AbstractApiRegionsAnalyserTask {
+
+    private static final String GLOBAL_REGION_NAME = "global";
+
+    private static final String DEPRECATED_REGION_NAME = "deprecated";
+
+    @Override
+    public String getId() {
+        return API_REGIONS_KEY + "-dependencies";
+    }
+
+    @Override
+    public String getName() {
+        return "Api Regions dependecies analyser task";
+    }
+
+    @Override
+    protected void execute(ApiRegions apiRegions, AnalyserTaskContext ctx) throws Exception {
+        Set<String> globalApis = apiRegions.getApis(GLOBAL_REGION_NAME);
+        Set<String> deprectaedApis = apiRegions.getApis(DEPRECATED_REGION_NAME);
+
+        FeatureDescriptor featureDescriptor = ctx.getFeatureDescriptor();
+        for (BundleDescriptor bundleDescriptor : featureDescriptor.getBundleDescriptors()) {
+            for (PackageInfo packageInfo : bundleDescriptor.getExportedPackages()) {
+                String exportedPackage = packageInfo.getName();
+
+                if (globalApis.contains(exportedPackage)) {
+                    for (String uses : packageInfo.getUses()) {
+                        if (deprectaedApis.contains(uses)) {
+                            String errorMessage = String.format(
+                                    "Bundle '%s', defined in feature '%s', declares '%s' in the '%s' header which requires '%s' package that is in the 'deprecated' region",
+                                    bundleDescriptor.getArtifact().getId(),
+                                    ctx.getFeature().getId(),
+                                    exportedPackage,
+                                    Constants.EXPORT_PACKAGE,
+                                    uses);
+                            ctx.reportError(errorMessage);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/feature/scanner/PackageInfo.java b/src/main/java/org/apache/sling/feature/scanner/PackageInfo.java
index bb7081b..9979aab 100644
--- a/src/main/java/org/apache/sling/feature/scanner/PackageInfo.java
+++ b/src/main/java/org/apache/sling/feature/scanner/PackageInfo.java
@@ -16,6 +16,9 @@
  */
 package org.apache.sling.feature.scanner;
 
+import java.util.Collections;
+import java.util.Set;
+
 import org.osgi.framework.Version;
 import org.osgi.framework.VersionRange;
 
@@ -24,11 +27,17 @@ public class PackageInfo implements Comparable<PackageInfo> {
     private final boolean optional;
     private final String name;
     private final String version;
+    private final Set<String> uses;
 
     public PackageInfo(final String name, final String version, final boolean optional) {
+        this(name, version, optional, Collections.emptySet());
+    }
+
+    public PackageInfo(final String name, final String version, final boolean optional, Set<String> uses) {
         this.name = name;
         this.version = version;
         this.optional = optional;
+        this.uses = uses;
     }
 
     public String getName() {
@@ -57,6 +66,10 @@ public class PackageInfo implements Comparable<PackageInfo> {
         return new VersionRange(this.version);
     }
 
+    public Set<String> getUses() {
+        return uses;
+    }
+
     @Override
     public String toString() {
         return "Package " + name
diff --git a/src/main/java/org/apache/sling/feature/scanner/impl/BundleDescriptorImpl.java b/src/main/java/org/apache/sling/feature/scanner/impl/BundleDescriptorImpl.java
index d933060..f8d3fe7 100644
--- a/src/main/java/org/apache/sling/feature/scanner/impl/BundleDescriptorImpl.java
+++ b/src/main/java/org/apache/sling/feature/scanner/impl/BundleDescriptorImpl.java
@@ -20,7 +20,10 @@ import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
+import java.util.StringTokenizer;
 import java.util.jar.JarFile;
 import java.util.jar.Manifest;
 import java.util.stream.Collectors;
@@ -169,9 +172,21 @@ public class BundleDescriptorImpl
                     final String resolution = entry.getDirective("resolution");
                     optional = "optional".equalsIgnoreCase(resolution);
                 }
+
+                Set<String> uses = new HashSet<>();
+                String usesAttribute = entry.getDirective("uses");
+                if (usesAttribute != null && !usesAttribute.isEmpty()) {
+                    StringTokenizer tokenizer = new StringTokenizer(usesAttribute, ",");
+                    while (tokenizer.hasMoreTokens()) {
+                        String usePackage = tokenizer.nextToken();
+                        uses.add(usePackage);
+                    }
+                }
+
                 final PackageInfo pck = new PackageInfo(entry.getName(),
                     version,
-                    optional);
+                    optional,
+                    uses);
                 pcks.add(pck);
             }
 
diff --git a/src/test/java/org/apache/sling/feature/analyser/task/impl/CheckApiRegionsTest.java b/src/test/java/org/apache/sling/feature/analyser/task/impl/AbstractApiRegionsAnalyserTaskTest.java
similarity index 82%
copy from src/test/java/org/apache/sling/feature/analyser/task/impl/CheckApiRegionsTest.java
copy to src/test/java/org/apache/sling/feature/analyser/task/impl/AbstractApiRegionsAnalyserTaskTest.java
index bfbf446..fdb1ba8 100644
--- a/src/test/java/org/apache/sling/feature/analyser/task/impl/CheckApiRegionsTest.java
+++ b/src/test/java/org/apache/sling/feature/analyser/task/impl/AbstractApiRegionsAnalyserTaskTest.java
@@ -24,6 +24,7 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import java.io.File;
+import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.jar.Manifest;
@@ -38,11 +39,25 @@ import org.apache.sling.feature.analyser.task.AnalyserTaskContext;
 import org.apache.sling.feature.scanner.BundleDescriptor;
 import org.apache.sling.feature.scanner.FeatureDescriptor;
 import org.apache.sling.feature.scanner.PackageInfo;
+import org.junit.After;
+import org.junit.Before;
 import org.junit.Test;
 
-public class CheckApiRegionsTest {
+public abstract class AbstractApiRegionsAnalyserTaskTest<T extends AbstractApiRegionsAnalyserTask> {
 
-    private AnalyserTask checkApiRegions = new CheckApiRegions();
+    protected abstract T newTask();
+
+    private AnalyserTask analyserTask;
+
+    @Before
+    public void setUp() {
+        analyserTask = newTask();
+    }
+
+    @After
+    public void tearDown() {
+        analyserTask = null;
+    }
 
     @Test
     public void testNoApiRegionsExtension() throws Exception {
@@ -70,30 +85,14 @@ public class CheckApiRegionsTest {
         assertTrue(errors.iterator().next().contains("does not represent a valid JSON 'api-regions'"));
     }
 
-    @Test
-    public void testNotValidApiRegionJson() throws Exception {
-        List<String> errors = execute("[{\"name\": \"global\",\"exports\": [\"org.osgi.util.function.doesnotexist\"]}]");
-
-        assertFalse(errors.isEmpty());
-        assertTrue(errors.iterator()
-                .next()
-                .startsWith("Region 'global' defined in feature 'org.apache.sling.testing:org.apache.sling.testing.apiregions:1.0.0' declares 1 package which is not exported by any bundle"));
-    }
-
-    @Test
-    public void testValidApiRegionJson() throws Exception {
-        List<String> errors = execute("[{\"name\": \"global\",\"exports\": [\"org.osgi.util.function\"]}]");
-        assertTrue(errors.isEmpty());
-    }
-
-    private List<String> execute(String apiRegionJSON) throws Exception {
+    protected final List<String> execute(String apiRegionJSON) throws Exception {
         Extension extension = mock(Extension.class);
         when(extension.getJSON()).thenReturn(apiRegionJSON);
 
         return execute(extension);
     }
 
-    private List<String> execute(Extension extension) throws Exception {
+    protected final List<String> execute(Extension extension) throws Exception {
         Extensions extensions = mock(Extensions.class);
         when(extensions.getByName("api-regions")).thenReturn(extension);
 
@@ -108,7 +107,7 @@ public class CheckApiRegionsTest {
         AnalyserTaskContext ctx = mock(AnalyserTaskContext.class);
         when(ctx.getFeature()).thenReturn(feature);
 
-        PackageInfo packageInfo = new PackageInfo("org.osgi.util.function", "1.0", false);
+        PackageInfo packageInfo = new PackageInfo("org.osgi.util.function", "1.0", false, Collections.singleton("org.objectweb.asm"));
 
         BundleDescriptor bundleDescriptor = new TestBundleDescriptor();
         bundleDescriptor.getExportedPackages().add(packageInfo);
@@ -125,7 +124,7 @@ public class CheckApiRegionsTest {
             return null;
         }).when(ctx).reportError(anyString());
 
-        checkApiRegions.execute(ctx);
+        analyserTask.execute(ctx);
 
         return errors;
     }
@@ -149,8 +148,7 @@ public class CheckApiRegionsTest {
 
         @Override
         public Artifact getArtifact() {
-            // do nothing
-            return null;
+            return new Artifact(ArtifactId.fromMvnId("org.osgi:org.osgi.util.function:1.0.0"));
         }
 
         @Override
diff --git a/src/test/java/org/apache/sling/feature/analyser/task/impl/CheckApiRegionsDependenciesTest.java b/src/test/java/org/apache/sling/feature/analyser/task/impl/CheckApiRegionsDependenciesTest.java
new file mode 100644
index 0000000..09eed5a
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/analyser/task/impl/CheckApiRegionsDependenciesTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.sling.feature.analyser.task.impl;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+
+import org.junit.Test;
+
+public class CheckApiRegionsDependenciesTest extends AbstractApiRegionsAnalyserTaskTest<CheckApiRegionsDependencies> {
+
+    @Override
+    protected CheckApiRegionsDependencies newTask() {
+        return new CheckApiRegionsDependencies();
+    }
+
+    @Test
+    public void testNotValidApiRegionJson() throws Exception {
+        List<String> errors = execute("[{\"name\": \"global\",\"exports\": [\"org.osgi.util.function\"]},{\"name\": \"deprecated\",\"exports\": [\"org.objectweb.asm\"]}]");
+
+        assertFalse(errors.isEmpty());
+        assertTrue(errors.iterator()
+                .next()
+                .startsWith("Bundle 'org.osgi:org.osgi.util.function:1.0.0', defined in feature 'org.apache.sling.testing:org.apache.sling.testing.apiregions:1.0.0', declares 'org.osgi.util.function' in the 'Export-Package' header which requires 'org.objectweb.asm' package that is in the 'deprecated' region"));
+    }
+
+    @Test
+    public void testValidApiRegionJson() throws Exception {
+        List<String> errors = execute("[{\"name\": \"global\",\"exports\": [\"org.osgi.util.function\"]}]");
+        assertTrue(errors.isEmpty());
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/feature/analyser/task/impl/CheckApiRegionsTest.java b/src/test/java/org/apache/sling/feature/analyser/task/impl/CheckApiRegionsTest.java
index bfbf446..ab73570 100644
--- a/src/test/java/org/apache/sling/feature/analyser/task/impl/CheckApiRegionsTest.java
+++ b/src/test/java/org/apache/sling/feature/analyser/task/impl/CheckApiRegionsTest.java
@@ -18,56 +18,16 @@ package org.apache.sling.feature.analyser.task.impl;
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
 
-import java.io.File;
-import java.util.LinkedList;
 import java.util.List;
-import java.util.jar.Manifest;
 
-import org.apache.sling.feature.Artifact;
-import org.apache.sling.feature.ArtifactId;
-import org.apache.sling.feature.Extension;
-import org.apache.sling.feature.Extensions;
-import org.apache.sling.feature.Feature;
-import org.apache.sling.feature.analyser.task.AnalyserTask;
-import org.apache.sling.feature.analyser.task.AnalyserTaskContext;
-import org.apache.sling.feature.scanner.BundleDescriptor;
-import org.apache.sling.feature.scanner.FeatureDescriptor;
-import org.apache.sling.feature.scanner.PackageInfo;
 import org.junit.Test;
 
-public class CheckApiRegionsTest {
+public class CheckApiRegionsTest extends AbstractApiRegionsAnalyserTaskTest<CheckApiRegions> {
 
-    private AnalyserTask checkApiRegions = new CheckApiRegions();
-
-    @Test
-    public void testNoApiRegionsExtension() throws Exception {
-        List<String> errors = execute((Extension) null);
-        assertTrue(errors.isEmpty());
-    }
-
-    @Test
-    public void testNullApiRegionsJSON() throws Exception {
-        List<String> errors = execute((String) null);
-        assertTrue(errors.isEmpty());
-    }
-
-    @Test
-    public void testEmptyApiRegionsJSON() throws Exception {
-        List<String> errors = execute("");
-        assertTrue(errors.isEmpty());
-    }
-
-    @Test
-    public void testApiRegionsIsNotJSON() throws Exception {
-        List<String> errors = execute("this is not a JSON string");
-
-        assertFalse(errors.isEmpty());
-        assertTrue(errors.iterator().next().contains("does not represent a valid JSON 'api-regions'"));
+    @Override
+    protected CheckApiRegions newTask() {
+        return new CheckApiRegions();
     }
 
     @Test
@@ -86,97 +46,4 @@ public class CheckApiRegionsTest {
         assertTrue(errors.isEmpty());
     }
 
-    private List<String> execute(String apiRegionJSON) throws Exception {
-        Extension extension = mock(Extension.class);
-        when(extension.getJSON()).thenReturn(apiRegionJSON);
-
-        return execute(extension);
-    }
-
-    private List<String> execute(Extension extension) throws Exception {
-        Extensions extensions = mock(Extensions.class);
-        when(extensions.getByName("api-regions")).thenReturn(extension);
-
-        Feature feature = mock(Feature.class);
-        when(feature.getId()).thenReturn(new ArtifactId("org.apache.sling.testing",
-                                                        "org.apache.sling.testing.apiregions", 
-                                                        "1.0.0",
-                                                        null,
-                                                        null));
-        when(feature.getExtensions()).thenReturn(extensions);
-
-        AnalyserTaskContext ctx = mock(AnalyserTaskContext.class);
-        when(ctx.getFeature()).thenReturn(feature);
-
-        PackageInfo packageInfo = new PackageInfo("org.osgi.util.function", "1.0", false);
-
-        BundleDescriptor bundleDescriptor = new TestBundleDescriptor();
-        bundleDescriptor.getExportedPackages().add(packageInfo);
-
-        FeatureDescriptor featureDescriptor = new TestFeatureDescriptor();
-        featureDescriptor.getBundleDescriptors().add(bundleDescriptor);
-
-        when(ctx.getFeatureDescriptor()).thenReturn(featureDescriptor);
-
-        List<String> errors = new LinkedList<>();
-        doAnswer(invocation -> {
-            String error = invocation.getArgument(0);
-            errors.add(error);
-            return null;
-        }).when(ctx).reportError(anyString());
-
-        checkApiRegions.execute(ctx);
-
-        return errors;
-    }
-
-    private static final class TestFeatureDescriptor extends FeatureDescriptor {
-
-        @Override
-        public Feature getFeature() {
-            return null;
-        }
-
-    }
-
-    private static final class TestBundleDescriptor extends BundleDescriptor {
-
-        @Override
-        public File getArtifactFile() {
-            // do nothing
-            return null;
-        }
-
-        @Override
-        public Artifact getArtifact() {
-            // do nothing
-            return null;
-        }
-
-        @Override
-        public Manifest getManifest() {
-            // do nothing
-            return null;
-        }
-
-        @Override
-        public String getBundleVersion() {
-            // do nothing
-            return null;
-        }
-
-        @Override
-        public String getBundleSymbolicName() {
-            // do nothing
-            return null;
-        }
-
-        @Override
-        public int getBundleStartLevel() {
-            // do nothing
-            return 0;
-        }
-
-    }
-
 }