Skip to content

Conversation

@ctron
Copy link
Contributor

@ctron ctron commented Nov 20, 2025

This extracts a few improvements of the analysis tests from PR #2118

Notably:

  • Split up bigger tests into smaller ones using rstest (requires Rust 1.91)
  • Pull out common code

Summary by Sourcery

Extract analysis tests improvements by parameterizing endpoint tests and centralizing request logic, and refactor core service modules for better error propagation, modularity, and SBOM graph loading.

New Features:

  • Introduce parameterized endpoint tests using rstest with multiple cases

Enhancements:

  • Refactor Collector to return Results, extract collect_external and collect_package helpers, and propagate errors
  • Convert resolve_external_sbom and related functions to return Results with improved error handling and logging
  • Streamline recursive SBOM graph loading in load_graphs_inner with improved duplication tracking

Build:

  • Upgrade Rust toolchain to 1.91, bump test-context to 0.5, and add rstest, csaf, and packageurl dependencies

Tests:

  • Extract common request-building code into a shared req.rs helper
  • Split larger integration tests into smaller, parameterized rstest cases

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Nov 20, 2025

Reviewer's Guide

This PR isolates analysis test improvements from a draft PR by parameterizing endpoint tests with rstest and a common Req helper, refactoring Collector and service code to propagate errors and extract subroutines, overhauling external SBOM resolution and recursive graph loading logic, and bumping the Rust toolchain and related dependencies.

Sequence diagram for recursive graph loading with external SBOM resolution

sequenceDiagram
    participant AnalysisService
    participant InnerService
    participant sbom_external_node_Entity
    participant Collector
    participant resolve_external_sbom
    participant load_graph
    participant load_graphs_inner
    AnalysisService->>InnerService: load_graphs_inner(connection, sbom_ids, seen)
    loop for each sbom_id
        InnerService->>sbom_external_node_Entity: find external nodes for sbom_id
        sbom_external_node_Entity-->>InnerService: external_sboms
        loop for each external_sbom
            InnerService->>resolve_external_sbom: resolve external sbom
            resolve_external_sbom-->>InnerService: Option<ResolvedSbom>
            alt resolved
                InnerService->>load_graph: load graph for resolved sbom
                load_graph-->>InnerService: Arc<PackageGraph>
                InnerService->>InnerService: load_graphs_inner(connection, [resolved_sbom_id], seen)
            else not resolved
                InnerService-->>InnerService: skip
            end
        end
    end
Loading

Sequence diagram for Collector::collect with error propagation and subroutines

sequenceDiagram
    participant Collector
    participant "collect_external()"
    participant "collect_package()"
    participant "collect_graph()"
    Collector->>Collector: collect()
    alt depth == 0 or already visited
        Collector-->>Collector: return Ok(None)
    else node is ExternalNode
        Collector->>"collect_external()": collect_external(external_node)
        "collect_external()"-->>Collector: Result<Option<Vec<Node>>, Error>
    else node is PackageNode
        Collector->>"collect_package()": collect_package(current_node)
        "collect_package()"-->>Collector: Result<Option<Vec<Node>>, Error>
    else other node
        Collector->>"collect_graph()": collect_graph()
        "collect_graph()"-->>Collector: Result<Vec<Node>, Error>
        Collector-->>Collector: Ok(Some(nodes))
    end
Loading

Class diagram for refactored Collector and related service functions

classDiagram
    class Collector {
        +collect() Result<Option<Vec<Node>>, Error>
        +collect_graph() Result<Vec<Node>, Error>
        +collect_external(external_node: &ExternalNode) Result<Option<Vec<Node>>, Error>
        +collect_package(current_node: &PackageNode) Result<Option<Vec<Node>>, Error>
    }
    class AnalysisService {
        +collect_components() Result<Vec<Node>, Error>
        +collect_relationships() Result<Vec<Node>, Error>
        +collect_ancestors() Result<Vec<Node>, Error>
        +collect_descendants() Result<Vec<Node>, Error>
        +collect_graphs_inner() Result<Vec<(String, Arc<PackageGraph>)>, Error>
        +load_graphs_inner() Result<Vec<(String, Arc<PackageGraph>)>, Error>
    }
    Collector --> AnalysisService : uses
    AnalysisService --> Collector : creates
    Collector --> Node
    Collector --> ExternalNode
    Collector --> PackageNode
    AnalysisService --> PackageGraph
    AnalysisService --> Node
    AnalysisService --> Error
Loading

Class diagram for new and refactored SBOM resolution helpers

classDiagram
    class ResolvedSbom {
        +sbom_id: Uuid
        +node_id: String
    }
    class ExternalType {
        <<enum>>
        SPDX
        CycloneDx
        RedHatProductComponent
    }
    class DiscriminatorType {
        <<enum>>
        Sha256
        // ...
    }
    class sbom_external_node_Entity {
        +find()
        +discriminator_type
        +discriminator_value
        +external_type
        +external_node_ref
        +external_doc_ref
        +sbom_id
        +node_id
    }
    class sbom_node_checksum_Entity {
        +find()
        +value
        +sbom_id
        +node_id
    }
    ResolvedSbom <.. sbom_external_node_Entity
    ResolvedSbom <.. sbom_node_checksum_Entity
Loading

File-Level Changes

Change Details Files
Parameterize and simplify endpoint tests
  • Migrate tests to rstest with multiple #[case] and #[values] parameters
  • Extract common request-building logic into a Req struct and ReqExt trait
  • Update tests mod.rs to include the new req module
modules/analysis/src/endpoints/tests/latest_filters.rs
modules/analysis/src/endpoints/tests/mod.rs
modules/analysis/src/endpoints/tests/req.rs
Unify Collector.collect signature and extract subroutines
  • Change collect to return Result<Option<Vec>, Error> instead of Option
  • Extract collect_external and collect_package methods
  • Update collect_graph to use try_collect and propagate errors
modules/analysis/src/service/collector.rs
Convert SBOM resolution helpers to use Result and add logging
  • Change resolve_external_sbom and resolve_rh_external_* functions to return Result<Option, Error>
  • Add instrumentation annotations and debug logs
  • Introduce early returns on missing data
modules/analysis/src/service/mod.rs
Simplify load_graphs_inner loop and signature
  • Change load_graphs_inner signature to return Result<Vec<(String,Arc)>, Error>
  • Perform seen-ID deduplication with early continue
  • Streamline external SBOM resolution recursion
  • Initialize seen_sbom_ids inline
modules/analysis/src/service/load.rs
Update AnalysisService methods to propagate errors
  • Change AnalysisService query methods to return Result<Vec, Error>
  • Replace collect and map calls with try_collect and propagate ancestor/descendant errors
  • Await and unwrap service calls in endpoints
modules/analysis/src/service/mod.rs
Bump Rust toolchain and update dependencies
  • Upgrade rust-toolchain to 1.91.0
  • Add rstest, csaf, packageurl dependencies and bump test-context version
  • Reorder and clean up Cargo.toml entries
modules/analysis/Cargo.toml
Cargo.toml
rust-toolchain.toml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@ctron ctron force-pushed the feature/fix_tc_3170_2 branch from 3a1eec7 to 54b972c Compare November 20, 2025 14:59
Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes - here's some feedback:

  • Consider refactoring the URI construction in req.rs to use a builder or a dedicated URI helper instead of manual string concatenation to ensure proper encoding and reduce boilerplate.
  • The new collect_external and collect_package methods in Collector would benefit from brief doc comments explaining their inputs, outputs, and error behavior to improve maintainability.
  • In load_graphs_inner, you could extract the external SBOM resolution and loading logic into a separate helper function to flatten the loop nesting and make the flow clearer.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider refactoring the URI construction in `req.rs` to use a builder or a dedicated URI helper instead of manual string concatenation to ensure proper encoding and reduce boilerplate.
- The new `collect_external` and `collect_package` methods in `Collector` would benefit from brief doc comments explaining their inputs, outputs, and error behavior to improve maintainability.
- In `load_graphs_inner`, you could extract the external SBOM resolution and loading logic into a separate helper function to flatten the loop nesting and make the flow clearer.

## Individual Comments

### Comment 1
<location> `modules/analysis/src/service/mod.rs:225-241` </location>
<code_context>
+
+    log::debug!("Found {} nodes by checksum", matches.len());
+
+    Ok(matches
+        .into_iter()
+        // The use of .find here ensures we never match on cdx top level metadata component
+        // which has not defined a bom-ref - we can 'sniff' this because such nodes always
+        // are ingested with a uuid node-id.
+        .find(|model| {
+            if Uuid::parse_str(&model.node_id).is_err() {
+                // failed to parse, we keep it
+                true
+            } else {
+                log::debug!("Dropping suspected top-level node ID: {}", model.node_id);
+                false
             }
-        }
-        _ => None,
-    }
+        })
+        .map(|matched_model| ResolvedSbom {
+            sbom_id: matched_model.sbom_id,
+            node_id: matched_model.node_id,
</code_context>

<issue_to_address>
**suggestion:** The logic for filtering top-level nodes by UUID parsing may be fragile.

If the assumption that only top-level nodes have UUIDs changes, this filter may fail. Consider documenting this dependency or using a dedicated marker for top-level nodes.

```suggestion
    // NOTE: The following filter relies on the assumption that only top-level nodes have UUID node-ids.
    // If this assumption changes, this logic may become invalid. 
    // For future maintainability, consider using a dedicated marker or field to identify top-level nodes.
    Ok(matches
        .into_iter()
        .find(|model| {
            if Uuid::parse_str(&model.node_id).is_err() {
                // failed to parse, we keep it
                true
            } else {
                log::debug!("Dropping suspected top-level node ID: {}", model.node_id);
                false
            }
        })
        .map(|matched_model| ResolvedSbom {
            sbom_id: matched_model.sbom_id,
            node_id: matched_model.node_id,
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@ctron ctron force-pushed the feature/fix_tc_3170_2 branch 2 times, most recently from 98fa4ba to 5ed02a2 Compare November 20, 2025 15:02
@ctron ctron enabled auto-merge November 20, 2025 15:02
@codecov
Copy link

codecov bot commented Nov 20, 2025

Codecov Report

❌ Patch coverage is 77.77778% with 30 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.08%. Comparing base (77e02e6) to head (27a92d8).
⚠️ Report is 7 commits behind head on main.

Files with missing lines Patch % Lines
modules/analysis/src/service/collector.rs 74.74% 14 Missing and 11 partials ⚠️
modules/analysis/src/service/mod.rs 83.87% 0 Missing and 5 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2125      +/-   ##
==========================================
- Coverage   68.16%   68.08%   -0.09%     
==========================================
  Files         371      371              
  Lines       20838    20753      -85     
  Branches    20838    20753      -85     
==========================================
- Hits        14204    14129      -75     
+ Misses       5797     5778      -19     
- Partials      837      846       +9     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ctron ctron force-pushed the feature/fix_tc_3170_2 branch 2 times, most recently from a359b20 to ec672fa Compare November 21, 2025 09:08
use serde_json::Value;

#[derive(Default, Copy, Clone)]
pub struct Req<'a> {
Copy link
Contributor

@dejanb dejanb Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add some comments here and maybe some more descriptive naming especially for Loc enum and its members. It'd make it more readable

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do that. With the exception of the Req one. Normally I'd be all for full names. However, in this specific case, which is limited to the analysis tests, I'd like to keep it short to not make #[case] attributes more cluttered.


let mut uri = match loc {
Loc::None => {
format!("/api/v2/analysis/{latest}component?",)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use /api/v2/analysis/ as a shared prefix? Or use some URI build pattern?

@ctron ctron force-pushed the feature/fix_tc_3170_2 branch from 95b3e64 to 27a92d8 Compare November 21, 2025 11:09
@ctron
Copy link
Contributor Author

ctron commented Nov 21, 2025

Addressed the comments and rebased.

Copy link
Contributor

@dejanb dejanb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good

@ctron ctron added this pull request to the merge queue Nov 21, 2025
Merged via the queue into guacsec:main with commit e149e96 Nov 21, 2025
6 checks passed
@ctron ctron deleted the feature/fix_tc_3170_2 branch November 21, 2025 12:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants