Skip to content

[BUG] Location processing crashes with UnsupportedOperationException when a window has no visits #1019

@SuperCorleone

Description

@SuperCorleone

Describe the Bug

The core location-processing pipeline (processLocationEvent) throws java.lang.UnsupportedOperationException whenever a processing window produces no visits. mergeVisits returns an immutable List.of() for the empty case, and detectTrips then calls .sort(...) on that list, which immutable lists do not support. This aborts the whole processing run for the user.

The empty-visits case is normal and common: a window with no GPS data, sparse data, or where all points fall below the minimum-stay duration filter.

Root Cause

mergeVisits early-returns an immutable list:

// UnifiedLocationProcessingService.mergeVisits, ~L345
if (allVisits.isEmpty()) {
    return new VisitMergingResult(List.of(), List.of(), searchStart, searchEnd, System.currentTimeMillis() - start);
}

processLocationEvent passes mergingResult.processedVisits straight into detectTrips (no copy), whose first action mutates it:

// UnifiedLocationProcessingService.detectTrips, L371
processedVisits.sort(Comparator.comparing(ProcessedVisit::getStartTime));  // UnsupportedOperationException on List.of()

The non-empty path builds processedVisits via mergeVisitsChronologically(...)/bulkInsert(...) (mutable), so only the empty path is affected.

Steps to Reproduce

  1. Trigger processLocationEvent for a user/time-window that yields no detected visits (e.g. clustered-points query returns empty).
  2. Observe the pipeline abort.

Expected Behavior

With no visits, the pipeline completes (no trips created), no exception.

Observed Behavior

java.lang.UnsupportedOperationException
  at java.base/java.util.ImmutableCollections.uoe(...)
  at …UnifiedLocationProcessingService.detectTrips(UnifiedLocationProcessingService.java:371)

Suggested Fix

Return a mutable list from the empty branch, or copy before sorting:

// option A — empty branch
return new VisitMergingResult(new ArrayList<>(), new ArrayList<>(), searchStart, searchEnd, ...);
// option B — detectTrips
List<ProcessedVisit> sorted = new ArrayList<>(processedVisits);
sorted.sort(Comparator.comparing(ProcessedVisit::getStartTime));

Corresponding Test (generated)

@Test
public void testProcessLocationEventWithEmptyVisits() {
    // Arrange
    User user = new User(1L, "testuser", "password", "Test User", null, null, com.dedicatedcode.reitti.model.Role.USER, 1L);
    LocationProcessEvent event = new LocationProcessEvent("testuser", Instant.now().minusSeconds(3600), Instant.now(), null, null);

    when(userJdbcService.findByUsername("testuser")).thenReturn(Optional.of(user));

    // Mock detection parameters
    DetectionParameter.VisitDetection visitDetection = new DetectionParameter.VisitDetection(60L, 300L);
    DetectionParameter.VisitMerging visitMerging = new DetectionParameter.VisitMerging(1L, 1800L, 500L);
    DetectionParameter mockConfig = new DetectionParameter(1L, visitDetection, visitMerging, null, Instant.now(), null);
    when(visitDetectionParametersService.getCurrentConfiguration(any(User.class), any(Instant.class)))
            .thenReturn(mockConfig);

    // Mock existing visits
    when(processedVisitJdbcService.findByUserAndStartTimeBeforeEqualAndEndTimeAfterEqual(
            any(User.class), any(Instant.class), any(Instant.class)))
            .thenReturn(new ArrayList<>());

    // Mock clustered points with empty list
    when(rawLocationPointJdbcService.findClusteredPointsInTimeRangeForUser(
            any(User.class), any(Instant.class), any(Instant.class), anyInt(), anyDouble()))
            .thenReturn(Collections.emptyList());

    // Act
    unifiedLocationProcessingService.processLocationEvent(event);

    // Assert
    verify(userJdbcService).findByUsername("testuser");
    verify(visitDetectionParametersService).getCurrentConfiguration(any(User.class), any(Instant.class));
    verify(rawLocationPointJdbcService).findClusteredPointsInTimeRangeForUser(
            any(User.class), any(Instant.class), any(Instant.class), anyInt(), anyDouble());
    verify(processedVisitJdbcService).findByUserAndStartTimeBeforeEqualAndEndTimeAfterEqual(
            any(User.class), any(Instant.class), any(Instant.class));
}

This input was generated by the test case generator TestFusion developed in our STAR lab.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions