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 2019/06/13 10:33:52 UTC

[sling-org-apache-sling-feature-analyser] branch SLING-8251_merged_after_releases created (now 3847fea)

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

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


      at 3847fea  SLING-8251 - Support checking dependencies for content packages

This branch includes the following new commits:

     new 3847fea  SLING-8251 - Support checking dependencies for content packages

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[sling-org-apache-sling-feature-analyser] 01/01: SLING-8251 - Support checking dependencies for content packages

Posted by si...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 3847feaa94055082c1d96f15583db64adde96d78
Author: Simo Tripodi <st...@adobe.com>
AuthorDate: Thu Jun 13 12:31:11 2019 +0200

    SLING-8251 - Support checking dependencies for content packages
---
 pom.xml                                            |  28 ++-
 .../impl/CheckContentPackagesDependencies.java     | 133 ++++++++++++
 .../scanner/impl/ContentPackageScanner.java        | 226 ++++++++++-----------
 .../feature/scanner/impl/DeleteDirectoryHook.java  |  73 +++++++
 .../impl/CheckContentPackagesDependenciesTest.java | 116 +++++++++++
 src/test/resources/test_a-1.0.zip                  | Bin 0 -> 4470 bytes
 src/test/resources/test_b-1.0.zip                  | Bin 0 -> 4442 bytes
 src/test/resources/test_c-1.0.zip                  | Bin 0 -> 4402 bytes
 src/test/resources/test_d-1.0.zip                  | Bin 0 -> 4450 bytes
 src/test/resources/test_e-1.0.zip                  | Bin 0 -> 4439 bytes
 10 files changed, 454 insertions(+), 122 deletions(-)

diff --git a/pom.xml b/pom.xml
index 775f419..3fdeb9a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -143,7 +143,33 @@
             <version>1.11.0</version>
             <scope>provided</scope>
         </dependency>
-        
+
+        <!-- Content-Package check -->
+        <dependency>
+            <groupId>org.apache.jackrabbit.vault</groupId>
+            <artifactId>org.apache.jackrabbit.vault</artifactId>
+            <version>3.2.6</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.jackrabbit</groupId>
+            <artifactId>jackrabbit-spi-commons</artifactId>
+            <version>2.19.1</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.jcr</groupId>
+            <artifactId>jcr</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.6</version>
+            <scope>provided</scope>
+        </dependency>
+        <!-- END Content-Package check -->
+
         <!-- Testing -->
         <dependency>
             <groupId>junit</groupId>
diff --git a/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckContentPackagesDependencies.java b/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckContentPackagesDependencies.java
new file mode 100644
index 0000000..c15d08a
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckContentPackagesDependencies.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.File;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Queue;
+import java.util.Set;
+
+import org.apache.jackrabbit.vault.packaging.Dependency;
+import org.apache.jackrabbit.vault.packaging.PackageId;
+import org.apache.jackrabbit.vault.packaging.PackageManager;
+import org.apache.jackrabbit.vault.packaging.VaultPackage;
+import org.apache.jackrabbit.vault.packaging.impl.PackageManagerImpl;
+import org.apache.sling.feature.analyser.task.AnalyserTask;
+import org.apache.sling.feature.analyser.task.AnalyserTaskContext;
+import org.apache.sling.feature.scanner.ArtifactDescriptor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class CheckContentPackagesDependencies implements AnalyserTask {
+
+    private final PackageManager packageManager = new PackageManagerImpl();
+
+    private final Logger logger = LoggerFactory.getLogger(getClass());
+
+    @Override
+    public String getId() {
+        return "content-packages-dependencies";
+    }
+
+    @Override
+    public String getName() {
+        return "Content-packages dependencies checker";
+    }
+
+    @Override
+    public void execute(AnalyserTaskContext ctx) throws Exception {
+        Set<ArtifactDescriptor> descriptors = ctx.getFeatureDescriptor().getArtifactDescriptors();
+        if (descriptors == null || descriptors.isEmpty()) {
+            return;
+        }
+
+        Map<PackageId, Dependency[]> dependenciesMap = new HashMap<>();
+
+        for (ArtifactDescriptor descriptor : descriptors) {
+            onDescriptor(ctx, descriptor, dependenciesMap);
+        }
+
+        for (PackageId contentPackageId : dependenciesMap.keySet()) {
+            verifyDependenciesTree(ctx, contentPackageId, dependenciesMap);
+        }
+    }
+
+    private void onDescriptor(AnalyserTaskContext ctx, ArtifactDescriptor descriptor, Map<PackageId, Dependency[]> dependenciesMap) throws Exception {
+        URL resourceUrl = descriptor.getArtifactFile();
+        String filename = resourceUrl.getFile().replace('/', File.separatorChar);
+        File artifactFile = new File(filename);
+        if (!artifactFile.exists() || !artifactFile.isFile()) {
+            ctx.reportError("Artifact file " + artifactFile + " does not exist or it is not a file");
+            return;
+        }
+
+        try (VaultPackage vaultPackage = packageManager.open(artifactFile, true)) {
+            PackageId packageId = vaultPackage.getId();
+
+            logger.debug("Collecting " + packageId + " dependencies...");
+
+            dependenciesMap.put(packageId, vaultPackage.getDependencies());
+
+            logger.debug(packageId + " dependencies collected.");
+        }
+    }
+
+    private void verifyDependenciesTree(AnalyserTaskContext ctx, PackageId root, Map<PackageId, Dependency[]> dependenciesMap) {
+        logger.debug("Verifying " + root + " transitive dependencies...");
+
+        Queue<Dependency> toBeVisited = new LinkedList<>();
+        enqueue(toBeVisited, dependenciesMap.get(root));
+
+        Set<Dependency> alreadyVisited = new HashSet<>();
+
+        while (!toBeVisited.isEmpty()) {
+            Dependency current = toBeVisited.poll();
+
+            if (alreadyVisited.add(current)) {
+                boolean found = false;
+
+                for (Entry<PackageId, Dependency[]> entry : dependenciesMap.entrySet()) {
+                    if (current.matches(entry.getKey())) {
+                        enqueue(toBeVisited, entry.getValue());
+                        found = true;
+                        break;
+                    }
+                }
+
+                if (!found) {
+                    ctx.reportError("Missing " + current + " dependency for " + root);
+                }
+            }
+        }
+    }
+
+    private static void enqueue(Queue<Dependency> target, Dependency...dependencies) {
+        if (dependencies == null) {
+            return;
+        }
+
+        for (Dependency dependency : dependencies) {
+            target.offer(dependency);
+        }
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/feature/scanner/impl/ContentPackageScanner.java b/src/main/java/org/apache/sling/feature/scanner/impl/ContentPackageScanner.java
index b57b199..0024a6c 100644
--- a/src/main/java/org/apache/sling/feature/scanner/impl/ContentPackageScanner.java
+++ b/src/main/java/org/apache/sling/feature/scanner/impl/ContentPackageScanner.java
@@ -43,6 +43,8 @@ public class ContentPackageScanner {
 
     private final Logger logger = LoggerFactory.getLogger(this.getClass());
 
+    private final DeleteDirectoryHook deleteDirectoryHook = new DeleteDirectoryHook();
+
     private final byte[] buffer = new byte[65536];
 
     private enum FileType {
@@ -51,6 +53,10 @@ public class ContentPackageScanner {
         PACKAGE
     }
 
+    public ContentPackageScanner() {
+        Runtime.getRuntime().addShutdownHook(deleteDirectoryHook);
+    }
+
     public Set<ContentPackageDescriptor> scan(final Artifact desc, final URL file) throws IOException {
 
         final Set<ContentPackageDescriptor> contentPackages = new HashSet<>();
@@ -74,40 +80,56 @@ public class ContentPackageScanner {
     throws IOException {
         logger.debug("Analyzing Content Package {}", archive);
 
-        final File tempDir = Files.createTempDirectory(null).toFile();
-        try {
-            final File toDir = new File(tempDir, archive.getPath().substring(archive.getPath().lastIndexOf("/") + 1));
-            toDir.mkdirs();
-
-            final List<File> toProcess = new ArrayList<>();
-
-            try (final ZipInputStream zis = new ZipInputStream(archive.openStream()) ) {
-                boolean done = false;
-                while ( !done ) {
-                    final ZipEntry entry = zis.getNextEntry();
-                    if ( entry == null ) {
-                        done = true;
-                    } else {
-                        final String entryName = entry.getName();
-                        if ( !entryName.endsWith("/") && entryName.startsWith("jcr_root/") ) {
-                            final String contentPath = entryName.substring(8);
-
-                            FileType fileType = null;
-
-                            if ( entryName.endsWith(".zip") ) {
-                                // embedded content package
-                                fileType = FileType.PACKAGE;
-
-                                // check for libs or apps
-                            } else if ( entryName.startsWith("jcr_root/libs/") || entryName.startsWith("jcr_root/apps/") ) {
-
-                                // check if this is an install folder (I)
-                                // install folders are either named:
-                                // "install" or
-                                // "install.{runmode}"
-                                boolean isInstall = entryName.indexOf("/install/") != -1;
+        final File tempDir = Files.createTempDirectory(null).toFile();;
+        deleteDirectoryHook.markToBeDeleted(tempDir);
+
+        final File toDir = new File(tempDir, archive.getPath().substring(archive.getPath().lastIndexOf("/") + 1));
+        toDir.mkdirs();
+
+        final List<File> toProcess = new ArrayList<>();
+
+        try (final ZipInputStream zis = new ZipInputStream(archive.openStream()) ) {
+            boolean done = false;
+            while ( !done ) {
+                final ZipEntry entry = zis.getNextEntry();
+                if ( entry == null ) {
+                    done = true;
+                } else {
+                    final String entryName = entry.getName();
+                    if ( !entryName.endsWith("/") && entryName.startsWith("jcr_root/") ) {
+                        final String contentPath = entryName.substring(8);
+
+                        FileType fileType = null;
+
+                        if ( entryName.endsWith(".zip") ) {
+                            // embedded content package
+                            fileType = FileType.PACKAGE;
+
+                            // check for libs or apps
+                        } else if ( entryName.startsWith("jcr_root/libs/") || entryName.startsWith("jcr_root/apps/") ) {
+
+                            // check if this is an install folder (I)
+                            // install folders are either named:
+                            // "install" or
+                            // "install.{runmode}"
+                            boolean isInstall = entryName.indexOf("/install/") != -1;
+                            if ( !isInstall ) {
+                                final int pos = entryName.indexOf("/install.");
+                                if ( pos != -1 ) {
+                                    final int endSlashPos = entryName.indexOf('/', pos + 1);
+                                    if ( endSlashPos != -1 ) {
+                                        isInstall = true;
+                                    }
+                                }
+                            }
+                            if ( !isInstall ) {
+                                // check if this is an install folder (II)
+                                // config folders are either named:
+                                // "config" or
+                                // "config.{runmode}"
+                                isInstall = entryName.indexOf("/config/") != -1;
                                 if ( !isInstall ) {
-                                    final int pos = entryName.indexOf("/install.");
+                                    final int pos = entryName.indexOf("/config.");
                                     if ( pos != -1 ) {
                                         final int endSlashPos = entryName.indexOf('/', pos + 1);
                                         if ( endSlashPos != -1 ) {
@@ -115,117 +137,79 @@ public class ContentPackageScanner {
                                         }
                                     }
                                 }
-                                if ( !isInstall ) {
-                                    // check if this is an install folder (II)
-                                    // config folders are either named:
-                                    // "config" or
-                                    // "config.{runmode}"
-                                    isInstall = entryName.indexOf("/config/") != -1;
-                                    if ( !isInstall ) {
-                                        final int pos = entryName.indexOf("/config.");
-                                        if ( pos != -1 ) {
-                                            final int endSlashPos = entryName.indexOf('/', pos + 1);
-                                            if ( endSlashPos != -1 ) {
-                                                isInstall = true;
-                                            }
-                                        }
-                                    }
-                                }
+                            }
 
-                                if (isInstall ) {
+                            if (isInstall ) {
 
-                                   if ( entryName.endsWith(".jar") ) {
-                                       fileType = FileType.BUNDLE;
-                                   } else if ( entryName.endsWith(".xml") || entryName.endsWith(".config") ) {
-                                       fileType = FileType.CONFIG;
-                                   }
-                                }
+                               if ( entryName.endsWith(".jar") ) {
+                                   fileType = FileType.BUNDLE;
+                               } else if ( entryName.endsWith(".xml") || entryName.endsWith(".config") ) {
+                                   fileType = FileType.CONFIG;
+                               }
                             }
+                        }
 
-                            if ( fileType != null ) {
-                                logger.debug("- extracting : {}", entryName);
-                                final File newFile = new File(toDir, entryName.replace('/', File.separatorChar));
-                                newFile.getParentFile().mkdirs();
+                        if ( fileType != null ) {
+                            logger.debug("- extracting : {}", entryName);
+                            final File newFile = new File(toDir, entryName.replace('/', File.separatorChar));
+                            newFile.getParentFile().mkdirs();
 
-                                try (final FileOutputStream fos = new FileOutputStream(newFile)) {
-                                    int len;
-                                    while ((len = zis.read(buffer)) > -1) {
-                                        fos.write(buffer, 0, len);
-                                    }
+                            try (final FileOutputStream fos = new FileOutputStream(newFile)) {
+                                int len;
+                                while ((len = zis.read(buffer)) > -1) {
+                                    fos.write(buffer, 0, len);
                                 }
+                            }
 
-                                if ( fileType == FileType.BUNDLE ) {
-                                    int startLevel = 20;
-                                    final int lastSlash = contentPath.lastIndexOf('/');
-                                    final int nextSlash = contentPath.lastIndexOf('/', lastSlash - 1);
-                                    final String part = contentPath.substring(nextSlash + 1, lastSlash);
-                                    try {
-                                        startLevel = Integer.valueOf(part);
-                                    } catch ( final NumberFormatException ignore ) {
-                                        // ignore
-                                    }
-
-                                    final Artifact bundle = new Artifact(extractArtifactId(tempDir, newFile));
-                                    final BundleDescriptor info = new BundleDescriptorImpl(bundle, newFile.toURI().toURL(), startLevel);
-                                    bundle.getMetadata().put("content-package", cp.getArtifact().getId().toMvnId());
-                                    bundle.getMetadata().put("content-path", contentPath);
+                            if ( fileType == FileType.BUNDLE ) {
+                                int startLevel = 20;
+                                final int lastSlash = contentPath.lastIndexOf('/');
+                                final int nextSlash = contentPath.lastIndexOf('/', lastSlash - 1);
+                                final String part = contentPath.substring(nextSlash + 1, lastSlash);
+                                try {
+                                    startLevel = Integer.valueOf(part);
+                                } catch ( final NumberFormatException ignore ) {
+                                    // ignore
+                                }
 
-                                    cp.bundles.add(info);
+                                final Artifact bundle = new Artifact(extractArtifactId(tempDir, newFile));
+                                final BundleDescriptor info = new BundleDescriptorImpl(bundle, newFile.toURI().toURL(), startLevel);
+                                bundle.getMetadata().put("content-package", cp.getArtifact().getId().toMvnId());
+                                bundle.getMetadata().put("content-path", contentPath);
 
-                                } else if ( fileType == FileType.CONFIG ) {
+                                cp.bundles.add(info);
 
-                                    final Configuration configEntry = this.process(newFile, cp.getArtifact(), contentPath);
-                                    if ( configEntry != null ) {
+                            } else if ( fileType == FileType.CONFIG ) {
 
-                                        cp.configs.add(configEntry);
-                                    }
+                                final Configuration configEntry = this.process(newFile, cp.getArtifact(), contentPath);
+                                if ( configEntry != null ) {
 
-                                } else if ( fileType == FileType.PACKAGE ) {
-                                    toProcess.add(newFile);
+                                    cp.configs.add(configEntry);
                                 }
 
+                            } else if ( fileType == FileType.PACKAGE ) {
+                                toProcess.add(newFile);
                             }
 
                         }
-                        zis.closeEntry();
+
                     }
+                    zis.closeEntry();
                 }
-
             }
 
-            for(final File f : toProcess) {
-                extractContentPackage(cp, infos, f.toURI().toURL());
-                final ContentPackageDescriptor i = new ContentPackageDescriptor(f.getName());
-                final int lastDot = f.getName().lastIndexOf(".");
-                i.setName(f.getName().substring(0, lastDot));
-                i.setArtifactFile(f.toURI().toURL());
-                i.setContentPackageInfo(cp.getArtifact(), f.getName());
-                infos.add(i);
-
-                i.lock();
-            }
-        } finally {
-            deleteRecursive(tempDir);
         }
-    }
 
-    private boolean deleteRecursive(File file) {
-        if (file.isDirectory()) {
-            File[] childs = file.listFiles();
-            if (childs != null) {
-                for (File child : childs) {
-                    if (!deleteRecursive(child)) {
-                        return false;
-                    }
-                }
-                return file.delete();
-            }
-            else {
-                return false;
-            }
-        }
-        else {
-            return file.delete();
+        for(final File f : toProcess) {
+            extractContentPackage(cp, infos, f.toURI().toURL());
+            final ContentPackageDescriptor i = new ContentPackageDescriptor(f.getName());
+            final int lastDot = f.getName().lastIndexOf(".");
+            i.setName(f.getName().substring(0, lastDot));
+            i.setArtifactFile(f.toURI().toURL());
+            i.setContentPackageInfo(cp.getArtifact(), f.getName());
+            infos.add(i);
+
+            i.lock();
         }
     }
 
diff --git a/src/main/java/org/apache/sling/feature/scanner/impl/DeleteDirectoryHook.java b/src/main/java/org/apache/sling/feature/scanner/impl/DeleteDirectoryHook.java
new file mode 100644
index 0000000..bfea678
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/scanner/impl/DeleteDirectoryHook.java
@@ -0,0 +1,73 @@
+/*
+ * 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.scanner.impl;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+final class DeleteDirectoryHook extends Thread {
+
+    private final Logger logger = LoggerFactory.getLogger(getClass());
+
+    private final List<File> toBeDeletedList = new ArrayList<>();
+
+    public void markToBeDeleted(File toBeDeleted) {
+        if (toBeDeleted != null && toBeDeleted.exists()) {
+            toBeDeletedList.add(toBeDeleted);
+        }
+    }
+
+    @Override
+    public void run() {
+        for (File toBeDeleted : toBeDeletedList) {
+            logger.debug("Deleting recursively " + toBeDeleted + "...");
+
+            if (!deleteRecursive(toBeDeleted)) {
+                logger.warn("Somethign went wrong when deleting directory "
+                            + toBeDeleted
+                            + " please verify the current user has enough rights to delete such dir.");
+            } else {
+                logger.debug(toBeDeleted + " successfully deleted");
+            }
+        }
+    }
+
+    private static boolean deleteRecursive(File file) {
+        if (file.isDirectory()) {
+            File[] childs = file.listFiles();
+            if (childs != null) {
+                for (File child : childs) {
+                    if (!deleteRecursive(child)) {
+                        return false;
+                    }
+                }
+                return file.delete();
+            }
+            else {
+                return false;
+            }
+        }
+        else {
+            return file.delete();
+        }
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/feature/analyser/task/impl/CheckContentPackagesDependenciesTest.java b/src/test/java/org/apache/sling/feature/analyser/task/impl/CheckContentPackagesDependenciesTest.java
new file mode 100644
index 0000000..a2d961f
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/analyser/task/impl/CheckContentPackagesDependenciesTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.assertEquals;
+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.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.ArtifactId;
+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.ArtifactDescriptor;
+import org.apache.sling.feature.scanner.FeatureDescriptor;
+import org.apache.sling.feature.scanner.impl.FeatureDescriptorImpl;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class CheckContentPackagesDependenciesTest {
+
+    private AnalyserTask task;
+
+    @Before
+    public void setUp() {
+        task = new CheckContentPackagesDependencies();
+    }
+
+    @After
+    public void tearDown() {
+        task = null;
+    }
+
+    @Test
+    public void dependenciesNotResolvedThrowsError() throws Exception {
+        List<String> errors = execute("test_a-1.0.zip");
+
+        assertFalse(errors.isEmpty());
+
+        Iterator<String> errorsIterator = errors.iterator();
+        assertEquals("Missing my_packages:test_b dependency for my_packages:test_a:1.0", errorsIterator.next());
+        assertEquals("Missing my_packages:test_c:[1.0,2.0) dependency for my_packages:test_a:1.0", errorsIterator.next());
+    }
+
+    @Test
+    public void dependenciesResolved() throws Exception {
+        List<String> errors = execute("test_a-1.0.zip",
+                                      "test_b-1.0.zip",
+                                      "test_c-1.0.zip",
+                                      "test_d-1.0.zip",
+                                      "test_e-1.0.zip");
+        assertTrue(errors.isEmpty());
+    }
+
+    private List<String> execute(String...resources) throws Exception {
+        Feature feature = mock(Feature.class);
+        when(feature.getId()).thenReturn(new ArtifactId("org.apache.sling.testing",
+                                                        "org.apache.sling.testing.contentpackages",
+                                                        "1.0.0",
+                                                        null,
+                                                        null));
+        FeatureDescriptor featureDescriptor = new FeatureDescriptorImpl(feature);
+
+        for (String resource : resources) {
+            ArtifactId id = mock(ArtifactId.class);
+            Artifact artifact = mock(Artifact.class);
+            when(artifact.getId()).thenReturn(id);
+
+            ArtifactDescriptor descriptor = mock(ArtifactDescriptor.class);
+            when(descriptor.getArtifact()).thenReturn(artifact);
+            when(descriptor.getArtifactFile()).thenReturn(getClass().getClassLoader().getResource(resource));
+
+            featureDescriptor.getArtifactDescriptors().add(descriptor);
+        }
+
+        AnalyserTaskContext ctx = mock(AnalyserTaskContext.class);
+
+        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());
+
+        task.execute(ctx);
+
+        return errors;
+    }
+
+}
diff --git a/src/test/resources/test_a-1.0.zip b/src/test/resources/test_a-1.0.zip
new file mode 100644
index 0000000..08df03a
Binary files /dev/null and b/src/test/resources/test_a-1.0.zip differ
diff --git a/src/test/resources/test_b-1.0.zip b/src/test/resources/test_b-1.0.zip
new file mode 100644
index 0000000..85fac13
Binary files /dev/null and b/src/test/resources/test_b-1.0.zip differ
diff --git a/src/test/resources/test_c-1.0.zip b/src/test/resources/test_c-1.0.zip
new file mode 100644
index 0000000..245291a
Binary files /dev/null and b/src/test/resources/test_c-1.0.zip differ
diff --git a/src/test/resources/test_d-1.0.zip b/src/test/resources/test_d-1.0.zip
new file mode 100644
index 0000000..7acf3d1
Binary files /dev/null and b/src/test/resources/test_d-1.0.zip differ
diff --git a/src/test/resources/test_e-1.0.zip b/src/test/resources/test_e-1.0.zip
new file mode 100644
index 0000000..372bbb9
Binary files /dev/null and b/src/test/resources/test_e-1.0.zip differ