diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/bitbake/parse/GraphNodeLabelParser.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/bitbake/parse/GraphNodeLabelParser.java index 36c9c209f5..f5d87439c4 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/bitbake/parse/GraphNodeLabelParser.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/bitbake/parse/GraphNodeLabelParser.java @@ -29,14 +29,26 @@ public Optional parseLayerFromLabel(String label, Set knownLayer if (!recipeSpec.isPresent()) { return Optional.empty(); } + + // Find the deepest (rightmost) matching layer in the path + // This ensures we match the actual layer folder, not parent folders + // that happen to share a name with a known layer + String deepestMatch = null; + int deepestPosition = -1; + for (String candidateLayerName : knownLayerNames) { String possibleLayerPathSubstring = LABEL_PATH_SEPARATOR + candidateLayerName + LABEL_PATH_SEPARATOR; - if (recipeSpec.get().contains(possibleLayerPathSubstring)) { - return Optional.of(candidateLayerName); + int position = recipeSpec.get().lastIndexOf(possibleLayerPathSubstring); + if (position > deepestPosition) { + deepestPosition = position; + deepestMatch = candidateLayerName; } } - logger.warn("Graph Node recipe '{}' does not correspond to any known layer ({})", label, knownLayerNames); - return Optional.empty(); + + if (deepestMatch == null) { + logger.warn("Graph Node recipe '{}' does not correspond to any known layer ({})", label, knownLayerNames); + } + return Optional.ofNullable(deepestMatch); } @NotNull diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/bitbake/transform/BitbakeDependencyGraphTransformer.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/bitbake/transform/BitbakeDependencyGraphTransformer.java index 5b58b1ad94..a03e4f3032 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/bitbake/transform/BitbakeDependencyGraphTransformer.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/bitbake/transform/BitbakeDependencyGraphTransformer.java @@ -125,7 +125,9 @@ private Optional generateExternalId(String dependencyName, String de ExternalId externalId = null; if (recipeLayerNames != null) { dependencyLayer = chooseRecipeLayer(dependencyName, dependencyLayer, recipeLayerNames); - externalId = ExternalId.FACTORY.createYoctoExternalId(dependencyLayer, dependencyName, dependencyVersion); + if (dependencyLayer != null) { + externalId = ExternalId.FACTORY.createYoctoExternalId(dependencyLayer, dependencyName, dependencyVersion); + } } else { logger.debug("Failed to find component '{}' in component layer map. [dependencyVersion: {}; dependencyLayer: {}", dependencyName, dependencyVersion, dependencyLayer); if (dependencyName.endsWith(NATIVE_SUFFIX)) { @@ -144,17 +146,49 @@ private Optional generateExternalId(String dependencyName, String de return Optional.ofNullable(externalId); } + @Nullable private String chooseRecipeLayer(String dependencyName, @Nullable String dependencyLayer, List recipeLayerNames) { + // Validate: path-matched layer must exist in authoritative show-recipes list + if (dependencyLayer != null && recipeLayerNames.contains(dependencyLayer)) { + logger.trace("For dependency recipe {}: using layer {} parsed from task-depends.dot", dependencyName, dependencyLayer); + return dependencyLayer; + } + + if (recipeLayerNames.isEmpty()) { + if (dependencyLayer != null) { + logger.warn( + "No authoritative show-recipes layer was parsed for dependency {}; using layer '{}' from task-depends.dot instead", + dependencyName, + dependencyLayer + ); + return dependencyLayer; + } + + logger.warn( + "No authoritative show-recipes layer or task-depends.dot layer was available for dependency {}; skipping external ID creation", + dependencyName + ); + return null; + } + + String authoritativeLayer = recipeLayerNames.get(0); + + // Path-matched layer is null or invalid - fall back to authoritative source if (dependencyLayer == null) { logger.warn( "Did not parse a layer for dependency {} from task-depends.dot; falling back to layer {} (first from show-recipes output)", dependencyName, - recipeLayerNames.get(0) + authoritativeLayer ); - dependencyLayer = recipeLayerNames.get(0); } else { - logger.trace("For dependency recipe {}: using layer {} parsed from task-depends.dot", dependencyName, dependencyLayer); + logger.debug( + "Path-matched layer '{}' not valid for recipe '{}' (valid layers: {}); using '{}'", + dependencyLayer, + dependencyName, + recipeLayerNames, + authoritativeLayer + ); } - return dependencyLayer; + return authoritativeLayer; } } diff --git a/detectable/src/test/java/com/blackduck/integration/detectable/detectables/bitbake/unit/BitbakeDependencyGraphTransformerTest.java b/detectable/src/test/java/com/blackduck/integration/detectable/detectables/bitbake/unit/BitbakeDependencyGraphTransformerTest.java index 233c06db6b..5c1f30e969 100644 --- a/detectable/src/test/java/com/blackduck/integration/detectable/detectables/bitbake/unit/BitbakeDependencyGraphTransformerTest.java +++ b/detectable/src/test/java/com/blackduck/integration/detectable/detectables/bitbake/unit/BitbakeDependencyGraphTransformerTest.java @@ -80,4 +80,84 @@ public void ignoredNoVersion() { graphAssert.hasNoDependency(externalIdFactory.createYoctoExternalId("meta", "example", null)); graphAssert.hasRootSize(0); } + + @Test + public void fallsBackToDependencyLayerWhenAuthoritativeLayerListIsEmpty() { + ExternalIdFactory externalIdFactory = new ExternalIdFactory(); + BitbakeGraph bitbakeGraph = new BitbakeGraph(); + bitbakeGraph.addNode("example", "75", "meta-from-graph"); + + Map> recipeToLayerMap = new HashMap<>(); + recipeToLayerMap.put("example", Collections.emptyList()); + + BitbakeDependencyGraphTransformer bitbakeDependencyGraphTransformer = new BitbakeDependencyGraphTransformer(EnumListFilter.excludeNone()); + DependencyGraph dependencyGraph = bitbakeDependencyGraphTransformer.transform(bitbakeGraph, recipeToLayerMap, null); + + NameVersionGraphAssert graphAssert = new NameVersionGraphAssert(Forge.YOCTO, dependencyGraph); + graphAssert.hasRootSize(1); + graphAssert.hasDependency(externalIdFactory.createYoctoExternalId("meta-from-graph", "example", "75")); + } + + @Test + public void skipsDependencyWhenNoLayerIsAvailable() { + ExternalIdFactory externalIdFactory = new ExternalIdFactory(); + BitbakeGraph bitbakeGraph = new BitbakeGraph(); + bitbakeGraph.addNode("example", "75", null); + + Map> recipeToLayerMap = new HashMap<>(); + recipeToLayerMap.put("example", Collections.emptyList()); + + BitbakeDependencyGraphTransformer bitbakeDependencyGraphTransformer = new BitbakeDependencyGraphTransformer(EnumListFilter.excludeNone()); + DependencyGraph dependencyGraph = bitbakeDependencyGraphTransformer.transform(bitbakeGraph, recipeToLayerMap, null); + + GraphAssert graphAssert = new GraphAssert(Forge.YOCTO, dependencyGraph); + graphAssert.hasNoDependency(externalIdFactory.createYoctoExternalId("meta", "example", "75")); + graphAssert.hasRootSize(0); + } + + /** + * Graph layer parsed from task-depends.dot does not appear in the authoritative show-recipes list. + * Expected: transformer ignores the invalid graph layer and uses the first authoritative layer instead. + */ + @Test + public void usesAuthoritativeLayerWhenGraphLayerIsInvalid() { + ExternalIdFactory externalIdFactory = new ExternalIdFactory(); + BitbakeGraph bitbakeGraph = new BitbakeGraph(); + // node carries "wrong-layer" which is NOT in the authoritative map + bitbakeGraph.addNode("example", "75", "wrong-layer"); + + Map> recipeToLayerMap = new HashMap<>(); + recipeToLayerMap.put("example", Collections.singletonList("meta-authoritative")); + + BitbakeDependencyGraphTransformer bitbakeDependencyGraphTransformer = new BitbakeDependencyGraphTransformer(EnumListFilter.excludeNone()); + DependencyGraph dependencyGraph = bitbakeDependencyGraphTransformer.transform(bitbakeGraph, recipeToLayerMap, null); + + NameVersionGraphAssert graphAssert = new NameVersionGraphAssert(Forge.YOCTO, dependencyGraph); + graphAssert.hasRootSize(1); + // must use the authoritative layer, not the invalid graph layer + graphAssert.hasDependency(externalIdFactory.createYoctoExternalId("meta-authoritative", "example", "75")); + graphAssert.hasNoDependency(externalIdFactory.createYoctoExternalId("wrong-layer", "example", "75")); + } + + /** + * No layer is parsed from task-depends.dot for the node (null). + * Expected: transformer falls back to the first authoritative layer from show-recipes. + */ + @Test + public void usesAuthoritativeLayerWhenGraphLayerIsNull() { + ExternalIdFactory externalIdFactory = new ExternalIdFactory(); + BitbakeGraph bitbakeGraph = new BitbakeGraph(); + // node has no layer from task-depends.dot + bitbakeGraph.addNode("example", "75", null); + + Map> recipeToLayerMap = new HashMap<>(); + recipeToLayerMap.put("example", Collections.singletonList("meta-authoritative")); + + BitbakeDependencyGraphTransformer bitbakeDependencyGraphTransformer = new BitbakeDependencyGraphTransformer(EnumListFilter.excludeNone()); + DependencyGraph dependencyGraph = bitbakeDependencyGraphTransformer.transform(bitbakeGraph, recipeToLayerMap, null); + + NameVersionGraphAssert graphAssert = new NameVersionGraphAssert(Forge.YOCTO, dependencyGraph); + graphAssert.hasRootSize(1); + graphAssert.hasDependency(externalIdFactory.createYoctoExternalId("meta-authoritative", "example", "75")); + } } diff --git a/detectable/src/test/java/com/blackduck/integration/detectable/detectables/bitbake/unit/BitbakeRecipesParserTest.java b/detectable/src/test/java/com/blackduck/integration/detectable/detectables/bitbake/unit/BitbakeRecipesParserTest.java index 8ac7589b9d..bfaeb5bb35 100644 --- a/detectable/src/test/java/com/blackduck/integration/detectable/detectables/bitbake/unit/BitbakeRecipesParserTest.java +++ b/detectable/src/test/java/com/blackduck/integration/detectable/detectables/bitbake/unit/BitbakeRecipesParserTest.java @@ -35,4 +35,22 @@ void test() { assertEquals(4, results.getRecipesWithLayers().size()); assertTrue(results.getRecipesWithLayers().containsKey("adcli")); } + + @Test + void preservesRecipeWithEmptyLayerListWhenRecipeDetailsCannotBeParsed() { + List malformedShowRecipesOutputLines = Arrays.asList( + "=== Available recipes: ===\n", + "broken-recipe:\n", + " malformed-layer-line\n", + "valid-recipe:\n", + " meta 1.0\n" + ); + + BitbakeRecipesParser parser = new BitbakeRecipesParser(); + ShowRecipesResults results = parser.parseShowRecipes(malformedShowRecipesOutputLines); + + assertTrue(results.getRecipesWithLayers().containsKey("broken-recipe")); + assertTrue(results.getRecipesWithLayers().get("broken-recipe").isEmpty()); + assertEquals(Arrays.asList("meta"), results.getRecipesWithLayers().get("valid-recipe")); + } } diff --git a/detectable/src/test/java/com/blackduck/integration/detectable/detectables/bitbake/unit/GraphNodeLabelParserTest.java b/detectable/src/test/java/com/blackduck/integration/detectable/detectables/bitbake/unit/GraphNodeLabelParserTest.java index 6153b9d469..28aebc8581 100644 --- a/detectable/src/test/java/com/blackduck/integration/detectable/detectables/bitbake/unit/GraphNodeLabelParserTest.java +++ b/detectable/src/test/java/com/blackduck/integration/detectable/detectables/bitbake/unit/GraphNodeLabelParserTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Optional; import java.util.Set; @@ -32,7 +33,97 @@ void testLayer() { knownLayers.add("meta"); Optional layer = parser.parseLayerFromLabel(labelValue, knownLayers); - + + assertTrue(layer.isPresent()); + assertEquals("meta", layer.get()); + } + + /** + * Path: /home/user/wrlinux/project/poky/meta/recipes-support/openssl.bb + * ^^^^^^^^ ^^^^ + * User folder Actual layer (deepest) + * + * Verifies: Returns "meta" (deepest), not "wrlinux" (shallow parent folder). + */ + @Test + void testLayerWithParentFolderCollision() { + String labelValue = "openssl do_compile\\n:3.0.8-r0\\n/home/user/wrlinux/project/poky/meta/recipes-support/openssl_3.0.8.bb"; + GraphNodeLabelParser parser = new GraphNodeLabelParser(); + + Set knownLayers = new LinkedHashSet<>(); + knownLayers.add("wrlinux"); + knownLayers.add("meta"); + knownLayers.add("meta-poky"); + + Optional layer = parser.parseLayerFromLabel(labelValue, knownLayers); + + assertTrue(layer.isPresent()); + assertEquals("meta", layer.get()); + } + + /** + * Same path as above, but layers in different order. + * + * Verifies: Result is deterministic regardless of iteration order. + */ + @Test + void testLayerParsingDeterministic() { + String labelValue = "openssl do_compile\\n:3.0.8-r0\\n/home/user/wrlinux/project/poky/meta/recipes-support/openssl_3.0.8.bb"; + GraphNodeLabelParser parser = new GraphNodeLabelParser(); + + Set knownLayers = new LinkedHashSet<>(); + knownLayers.add("meta"); + knownLayers.add("wrlinux"); + knownLayers.add("meta-poky"); + + Optional layer = parser.parseLayerFromLabel(labelValue, knownLayers); + + assertTrue(layer.isPresent()); + assertEquals("meta", layer.get()); + } + + /** + * Path: /builds/core/yocto/meta-custom/recipes-support/curl.bb + * ^^^^ ^^^^^^^^^^^ + * Parent Actual layer (deepest) + * + * Verifies: Any parent folder collision is handled, not just specific names. + */ + @Test + void testLayerIgnoresArbitraryParentFolder() { + String labelValue = "curl do_compile\\n:7.88.0-r0\\n/builds/core/yocto/meta-custom/recipes-support/curl_7.88.0.bb"; + GraphNodeLabelParser parser = new GraphNodeLabelParser(); + + Set knownLayers = new LinkedHashSet<>(); + knownLayers.add("core"); + knownLayers.add("meta-custom"); + knownLayers.add("meta"); + + Optional layer = parser.parseLayerFromLabel(labelValue, knownLayers); + + assertTrue(layer.isPresent()); + assertEquals("meta-custom", layer.get()); + } + + /** + * Path: /home/user/wrlinux/project/poky/meta/recipes-support/openssl.bb + * ^^^^^^^^ ^^^^ ^^^^ + * Pos 10 23 31 (deepest wins) + * + * Verifies: With multiple matches, deepest is selected. + */ + @Test + void testLayerSelectsDeepestMatch() { + String labelValue = "openssl do_compile\\n:3.0.8-r0\\n/home/user/wrlinux/project/poky/meta/recipes-support/openssl_3.0.8.bb"; + GraphNodeLabelParser parser = new GraphNodeLabelParser(); + + Set knownLayers = new LinkedHashSet<>(); + knownLayers.add("wrlinux"); + knownLayers.add("poky"); + knownLayers.add("meta"); + + Optional layer = parser.parseLayerFromLabel(labelValue, knownLayers); + assertTrue(layer.isPresent()); assertEquals("meta", layer.get()); }