Batch Import
This commit is contained in:
parent
7ecb5456cc
commit
a19d4870f7
30 changed files with 3387 additions and 48 deletions
|
|
@ -35,6 +35,12 @@ class HeatmapGridServiceTest {
|
|||
@Mock
|
||||
private ActivityRepository activityRepository;
|
||||
|
||||
@Mock
|
||||
private jakarta.persistence.EntityManager entityManager;
|
||||
|
||||
@Mock
|
||||
private org.springframework.jdbc.core.JdbcTemplate jdbcTemplate;
|
||||
|
||||
private HeatmapGridService heatmapGridService;
|
||||
private ObjectMapper objectMapper;
|
||||
private GeometryFactory geometryFactory;
|
||||
|
|
@ -45,7 +51,9 @@ class HeatmapGridServiceTest {
|
|||
heatmapGridService = new HeatmapGridService(
|
||||
heatmapGridRepository,
|
||||
activityRepository,
|
||||
objectMapper
|
||||
objectMapper,
|
||||
entityManager,
|
||||
jdbcTemplate
|
||||
);
|
||||
geometryFactory = new GeometryFactory(new PrecisionModel(), 4326);
|
||||
}
|
||||
|
|
@ -160,12 +168,14 @@ class HeatmapGridServiceTest {
|
|||
.thenReturn(activities);
|
||||
when(heatmapGridRepository.saveAll(anyList()))
|
||||
.thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(jdbcTemplate.update(anyString(), any(UUID.class)))
|
||||
.thenReturn(10); // Simulate deleting 10 rows
|
||||
|
||||
// Execute
|
||||
heatmapGridService.recalculateUserHeatmap(user);
|
||||
|
||||
// Verify
|
||||
verify(heatmapGridRepository).deleteByUserId(userId);
|
||||
verify(jdbcTemplate).update(anyString(), eq(userId));
|
||||
verify(activityRepository).findByUserIdOrderByStartedAtDesc(userId);
|
||||
verify(heatmapGridRepository, atLeastOnce()).saveAll(anyList());
|
||||
}
|
||||
|
|
|
|||
311
src/test/java/org/operaton/fitpub/util/DatePersistenceTest.java
Normal file
311
src/test/java/org/operaton/fitpub/util/DatePersistenceTest.java
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
package org.operaton.fitpub.util;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.operaton.fitpub.model.entity.Activity;
|
||||
import org.operaton.fitpub.model.entity.User;
|
||||
import org.operaton.fitpub.repository.ActivityRepository;
|
||||
import org.operaton.fitpub.repository.UserRepository;
|
||||
import org.operaton.fitpub.service.ActivityFileService;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Integration test to verify that activity dates are correctly persisted to
|
||||
* and retrieved from the database.
|
||||
*/
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
@Slf4j
|
||||
@Transactional
|
||||
class DatePersistenceTest {
|
||||
|
||||
@Autowired
|
||||
private ActivityFileService activityFileService;
|
||||
|
||||
@Autowired
|
||||
private ActivityRepository activityRepository;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private FitParser fitParser;
|
||||
|
||||
@Autowired
|
||||
private GpxParser gpxParser;
|
||||
|
||||
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
|
||||
|
||||
@Test
|
||||
@DisplayName("FIT file dates should persist correctly to database")
|
||||
void testFitFileDatePersistence() throws IOException {
|
||||
log.info("=== TESTING FIT FILE DATE PERSISTENCE ===");
|
||||
|
||||
// Create test user
|
||||
User user = createTestUser();
|
||||
|
||||
// Load FIT file
|
||||
String fitFileName = "/69287079d5e0a4532ba818ee.fit";
|
||||
byte[] fileData = loadTestFile(fitFileName);
|
||||
|
||||
// Parse first to see what we expect
|
||||
ParsedActivityData parsedData = fitParser.parse(fileData);
|
||||
LocalDateTime expectedStartTime = parsedData.getStartTime();
|
||||
LocalDateTime expectedEndTime = parsedData.getEndTime();
|
||||
|
||||
log.info("BEFORE DATABASE:");
|
||||
log.info(" Parsed start time: {}", expectedStartTime);
|
||||
log.info(" Parsed end time: {}", expectedEndTime);
|
||||
|
||||
// Upload via service (which saves to DB)
|
||||
MockMultipartFile mockFile = new MockMultipartFile(
|
||||
"file",
|
||||
"test-activity.fit",
|
||||
"application/octet-stream",
|
||||
fileData
|
||||
);
|
||||
|
||||
Activity savedActivity = activityFileService.processActivityFile(
|
||||
mockFile,
|
||||
user.getId(),
|
||||
"Test Activity",
|
||||
"Testing date persistence",
|
||||
Activity.Visibility.PUBLIC
|
||||
);
|
||||
|
||||
assertNotNull(savedActivity);
|
||||
assertNotNull(savedActivity.getId());
|
||||
|
||||
log.info("");
|
||||
log.info("AFTER SAVING TO DATABASE:");
|
||||
log.info(" Saved ID: {}", savedActivity.getId());
|
||||
log.info(" Saved start time: {}", savedActivity.getStartedAt());
|
||||
log.info(" Saved end time: {}", savedActivity.getEndedAt());
|
||||
log.info(" Saved timezone: {}", savedActivity.getTimezone());
|
||||
|
||||
// Compare
|
||||
assertEquals(expectedStartTime, savedActivity.getStartedAt(),
|
||||
"Start time should match parsed value");
|
||||
assertEquals(expectedEndTime, savedActivity.getEndedAt(),
|
||||
"End time should match parsed value");
|
||||
|
||||
// Flush to ensure write to DB
|
||||
activityRepository.flush();
|
||||
|
||||
// Query back from database
|
||||
Activity queriedActivity = activityRepository.findById(savedActivity.getId())
|
||||
.orElseThrow(() -> new AssertionError("Activity should exist in database"));
|
||||
|
||||
log.info("");
|
||||
log.info("AFTER QUERYING FROM DATABASE:");
|
||||
log.info(" Queried start time: {}", queriedActivity.getStartedAt());
|
||||
log.info(" Queried end time: {}", queriedActivity.getEndedAt());
|
||||
log.info(" Queried timezone: {}", queriedActivity.getTimezone());
|
||||
|
||||
// Verify dates survived round-trip
|
||||
assertEquals(expectedStartTime, queriedActivity.getStartedAt(),
|
||||
"Queried start time should match original parsed value");
|
||||
assertEquals(expectedEndTime, queriedActivity.getEndedAt(),
|
||||
"Queried end time should match original parsed value");
|
||||
|
||||
// Also verify it's recent (within last 2 months)
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime twoMonthsAgo = now.minusMonths(2);
|
||||
|
||||
assertTrue(queriedActivity.getStartedAt().isAfter(twoMonthsAgo),
|
||||
String.format("Activity should be recent. Expected after %s, got %s",
|
||||
twoMonthsAgo.format(FORMATTER),
|
||||
queriedActivity.getStartedAt().format(FORMATTER)));
|
||||
|
||||
log.info("");
|
||||
log.info("✅ FIT file date persistence: PASSED");
|
||||
log.info(" Expected: {}", expectedStartTime);
|
||||
log.info(" Got: {}", queriedActivity.getStartedAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GPX file dates should persist correctly to database")
|
||||
void testGpxFileDatePersistence() throws IOException {
|
||||
log.info("=== TESTING GPX FILE DATE PERSISTENCE ===");
|
||||
|
||||
// Create test user
|
||||
User user = createTestUser();
|
||||
|
||||
// Load GPX file
|
||||
String gpxFileName = "/7410863774.gpx";
|
||||
byte[] fileData = loadTestFile(gpxFileName);
|
||||
|
||||
// Parse first to see what we expect
|
||||
ParsedActivityData parsedData = gpxParser.parse(fileData);
|
||||
LocalDateTime expectedStartTime = parsedData.getStartTime();
|
||||
LocalDateTime expectedEndTime = parsedData.getEndTime();
|
||||
|
||||
log.info("BEFORE DATABASE:");
|
||||
log.info(" Parsed start time: {}", expectedStartTime);
|
||||
log.info(" Parsed end time: {}", expectedEndTime);
|
||||
log.info(" Parsed timezone: {}", parsedData.getTimezone());
|
||||
|
||||
// Upload via service
|
||||
MockMultipartFile mockFile = new MockMultipartFile(
|
||||
"file",
|
||||
"test-activity.gpx",
|
||||
"application/gpx+xml",
|
||||
fileData
|
||||
);
|
||||
|
||||
Activity savedActivity = activityFileService.processActivityFile(
|
||||
mockFile,
|
||||
user.getId(),
|
||||
"Test GPX Activity",
|
||||
"Testing GPX date persistence",
|
||||
Activity.Visibility.PUBLIC
|
||||
);
|
||||
|
||||
assertNotNull(savedActivity);
|
||||
|
||||
log.info("");
|
||||
log.info("AFTER SAVING TO DATABASE:");
|
||||
log.info(" Saved start time: {}", savedActivity.getStartedAt());
|
||||
log.info(" Saved end time: {}", savedActivity.getEndedAt());
|
||||
log.info(" Saved timezone: {}", savedActivity.getTimezone());
|
||||
|
||||
// Compare
|
||||
assertEquals(expectedStartTime, savedActivity.getStartedAt(),
|
||||
"Start time should match parsed value");
|
||||
assertEquals(expectedEndTime, savedActivity.getEndedAt(),
|
||||
"End time should match parsed value");
|
||||
|
||||
// Query back
|
||||
activityRepository.flush();
|
||||
Activity queriedActivity = activityRepository.findById(savedActivity.getId())
|
||||
.orElseThrow(() -> new AssertionError("Activity should exist"));
|
||||
|
||||
log.info("");
|
||||
log.info("AFTER QUERYING FROM DATABASE:");
|
||||
log.info(" Queried start time: {}", queriedActivity.getStartedAt());
|
||||
log.info(" Queried end time: {}", queriedActivity.getEndedAt());
|
||||
|
||||
// Verify round-trip
|
||||
assertEquals(expectedStartTime, queriedActivity.getStartedAt(),
|
||||
"Queried start time should match original");
|
||||
assertEquals(expectedEndTime, queriedActivity.getEndedAt(),
|
||||
"Queried end time should match original");
|
||||
|
||||
// This GPX file is from 2022, verify that
|
||||
assertEquals(2022, queriedActivity.getStartedAt().getYear(),
|
||||
"GPX file is from 2022");
|
||||
|
||||
log.info("");
|
||||
log.info("✅ GPX file date persistence: PASSED");
|
||||
log.info(" Expected: {}", expectedStartTime);
|
||||
log.info(" Got: {}", queriedActivity.getStartedAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Query activities ordered by date should show correct chronological order")
|
||||
void testQueryActivitiesOrderedByDate() throws IOException {
|
||||
log.info("=== TESTING ACTIVITY ORDERING BY DATE ===");
|
||||
|
||||
User user = createTestUser();
|
||||
|
||||
// Upload FIT file (Nov 2025 - recent)
|
||||
byte[] fitData = loadTestFile("/69287079d5e0a4532ba818ee.fit");
|
||||
MockMultipartFile fitFile = new MockMultipartFile(
|
||||
"file", "recent.fit", "application/octet-stream", fitData
|
||||
);
|
||||
Activity fitActivity = activityFileService.processActivityFile(
|
||||
fitFile, user.getId(), "Recent FIT", null, Activity.Visibility.PUBLIC
|
||||
);
|
||||
|
||||
// Upload GPX file (July 2022 - old)
|
||||
byte[] gpxData = loadTestFile("/7410863774.gpx");
|
||||
MockMultipartFile gpxFile = new MockMultipartFile(
|
||||
"file", "old.gpx", "application/gpx+xml", gpxData
|
||||
);
|
||||
Activity gpxActivity = activityFileService.processActivityFile(
|
||||
gpxFile, user.getId(), "Old GPX", null, Activity.Visibility.PUBLIC
|
||||
);
|
||||
|
||||
log.info("");
|
||||
log.info("UPLOADED ACTIVITIES:");
|
||||
log.info(" FIT (recent): {} - {}", fitActivity.getTitle(), fitActivity.getStartedAt());
|
||||
log.info(" GPX (old): {} - {}", gpxActivity.getTitle(), gpxActivity.getStartedAt());
|
||||
|
||||
activityRepository.flush();
|
||||
|
||||
// Query ordered by date DESC (newest first)
|
||||
List<Activity> activitiesNewestFirst = activityRepository
|
||||
.findByUserIdOrderByStartedAtDesc(user.getId());
|
||||
|
||||
log.info("");
|
||||
log.info("QUERY RESULT (newest first):");
|
||||
for (int i = 0; i < activitiesNewestFirst.size(); i++) {
|
||||
Activity a = activitiesNewestFirst.get(i);
|
||||
log.info(" [{}] {} - {}", i, a.getTitle(), a.getStartedAt());
|
||||
}
|
||||
|
||||
// Verify order
|
||||
assertEquals(2, activitiesNewestFirst.size(), "Should have 2 activities");
|
||||
|
||||
Activity first = activitiesNewestFirst.get(0);
|
||||
Activity second = activitiesNewestFirst.get(1);
|
||||
|
||||
// First should be the FIT file (Nov 2025)
|
||||
assertEquals("Recent FIT", first.getTitle(),
|
||||
"Newest activity should be the FIT file");
|
||||
assertEquals(2025, first.getStartedAt().getYear(),
|
||||
"Newest activity should be from 2025");
|
||||
|
||||
// Second should be the GPX file (July 2022)
|
||||
assertEquals("Old GPX", second.getTitle(),
|
||||
"Older activity should be the GPX file");
|
||||
assertEquals(2022, second.getStartedAt().getYear(),
|
||||
"Older activity should be from 2022");
|
||||
|
||||
// Verify chronological order
|
||||
assertTrue(first.getStartedAt().isAfter(second.getStartedAt()),
|
||||
String.format("First activity (%s) should be after second (%s)",
|
||||
first.getStartedAt(), second.getStartedAt()));
|
||||
|
||||
log.info("");
|
||||
log.info("✅ Activity ordering: PASSED");
|
||||
log.info(" Newest: {} ({})", first.getTitle(), first.getStartedAt());
|
||||
log.info(" Oldest: {} ({})", second.getTitle(), second.getStartedAt());
|
||||
}
|
||||
|
||||
private User createTestUser() {
|
||||
User user = User.builder()
|
||||
.id(UUID.randomUUID())
|
||||
.username("testuser_" + System.currentTimeMillis())
|
||||
.email("test_" + System.currentTimeMillis() + "@example.com")
|
||||
.passwordHash("dummy_hash")
|
||||
.displayName("Test User")
|
||||
.publicKey("-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtest\n-----END PUBLIC KEY-----")
|
||||
.privateKey("-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtest\n-----END PRIVATE KEY-----")
|
||||
.enabled(true)
|
||||
.build();
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
private byte[] loadTestFile(String resourcePath) throws IOException {
|
||||
InputStream inputStream = getClass().getResourceAsStream(resourcePath);
|
||||
assertNotNull(inputStream, "Test file should exist: " + resourcePath);
|
||||
byte[] data = inputStream.readAllBytes();
|
||||
inputStream.close();
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
|
@ -64,10 +64,23 @@ class FitParserIntegrationTest {
|
|||
|
||||
if (parsedData.getStartTime() != null) {
|
||||
log.info(" Start time: {}", parsedData.getStartTime());
|
||||
// Verify timestamp is reasonable (within 5 years of current time)
|
||||
long currentUnixTime = System.currentTimeMillis() / 1000;
|
||||
long activityUnixTime = parsedData.getStartTime()
|
||||
.atZone(java.time.ZoneId.systemDefault()).toEpochSecond();
|
||||
long diffDays = Math.abs(currentUnixTime - activityUnixTime) / (24 * 60 * 60);
|
||||
assertTrue(diffDays < 5 * 365,
|
||||
String.format("Start time should be within 5 years of now. Got %s (diff: %d days)",
|
||||
parsedData.getStartTime(), diffDays));
|
||||
}
|
||||
|
||||
if (parsedData.getEndTime() != null) {
|
||||
log.info(" End time: {}", parsedData.getEndTime());
|
||||
// End time should be after start time
|
||||
if (parsedData.getStartTime() != null) {
|
||||
assertTrue(!parsedData.getEndTime().isBefore(parsedData.getStartTime()),
|
||||
"End time should be after or equal to start time");
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedData.getTotalDistance() != null) {
|
||||
|
|
|
|||
|
|
@ -69,10 +69,23 @@ class GpxParserIntegrationTest {
|
|||
|
||||
if (parsedData.getStartTime() != null) {
|
||||
log.info(" Start time: {}", parsedData.getStartTime());
|
||||
// Verify timestamp is reasonable (within 10 years of current time for GPX files)
|
||||
long currentUnixTime = System.currentTimeMillis() / 1000;
|
||||
long activityUnixTime = parsedData.getStartTime()
|
||||
.atZone(java.time.ZoneId.systemDefault()).toEpochSecond();
|
||||
long diffDays = Math.abs(currentUnixTime - activityUnixTime) / (24 * 60 * 60);
|
||||
assertTrue(diffDays < 10 * 365,
|
||||
String.format("Start time should be within 10 years of now. Got %s (diff: %d days)",
|
||||
parsedData.getStartTime(), diffDays));
|
||||
}
|
||||
|
||||
if (parsedData.getEndTime() != null) {
|
||||
log.info(" End time: {}", parsedData.getEndTime());
|
||||
// End time should be after start time
|
||||
if (parsedData.getStartTime() != null) {
|
||||
assertTrue(!parsedData.getEndTime().isBefore(parsedData.getStartTime()),
|
||||
"End time should be after or equal to start time");
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedData.getTotalDistance() != null) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,281 @@
|
|||
package org.operaton.fitpub.util;
|
||||
|
||||
import com.garmin.fit.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Debugging test to investigate timestamp parsing issues in FIT and GPX files.
|
||||
* This test logs detailed information about raw timestamps and their conversions.
|
||||
*/
|
||||
@Slf4j
|
||||
class TimestampDebuggingTest {
|
||||
|
||||
private FitParser fitParser;
|
||||
private GpxParser gpxParser;
|
||||
private SpeedSmoother speedSmoother;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
speedSmoother = new SpeedSmoother();
|
||||
fitParser = new FitParser(speedSmoother);
|
||||
gpxParser = new GpxParser(speedSmoother);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Debug FIT file timestamp parsing with detailed logging")
|
||||
void debugFitTimestampParsing() throws IOException {
|
||||
log.info("=== FIT FILE TIMESTAMP DEBUGGING ===");
|
||||
log.info("Current system time: {}", LocalDateTime.now());
|
||||
log.info("Current system timezone: {}", ZoneId.systemDefault());
|
||||
log.info("Current Unix timestamp: {}", System.currentTimeMillis() / 1000);
|
||||
log.info("");
|
||||
|
||||
// Load FIT file
|
||||
String fitFileName = "/69287079d5e0a4532ba818ee.fit";
|
||||
InputStream inputStream = getClass().getResourceAsStream(fitFileName);
|
||||
assertNotNull(inputStream, "FIT file should exist");
|
||||
byte[] fileData = inputStream.readAllBytes();
|
||||
inputStream.close();
|
||||
|
||||
// Parse with FIT SDK directly to inspect raw values
|
||||
Decode decode = new Decode();
|
||||
MesgBroadcaster broadcaster = new MesgBroadcaster(decode);
|
||||
|
||||
final long FIT_EPOCH_OFFSET = 631065600L;
|
||||
|
||||
// Capture session message
|
||||
broadcaster.addListener(new SessionMesgListener() {
|
||||
@Override
|
||||
public void onMesg(SessionMesg mesg) {
|
||||
log.info("--- SESSION MESSAGE ---");
|
||||
if (mesg.getStartTime() != null) {
|
||||
DateTime startTime = mesg.getStartTime();
|
||||
long rawTimestamp = startTime.getTimestamp();
|
||||
long unixTimestamp = rawTimestamp + FIT_EPOCH_OFFSET;
|
||||
Instant instant = Instant.ofEpochSecond(unixTimestamp);
|
||||
LocalDateTime ldt = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
|
||||
ZonedDateTime zdt = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC"));
|
||||
|
||||
log.info("Raw FIT timestamp: {} seconds", rawTimestamp);
|
||||
log.info("FIT epoch offset: {} seconds", FIT_EPOCH_OFFSET);
|
||||
log.info("Unix timestamp (raw + offset): {} seconds", unixTimestamp);
|
||||
log.info("Unix timestamp as instant: {}", instant);
|
||||
log.info("As UTC ZonedDateTime: {}", zdt);
|
||||
log.info("As LocalDateTime (system TZ): {}", ldt);
|
||||
log.info("Expected if recent 2024: ~2024-11-27T15:49:09");
|
||||
log.info("");
|
||||
|
||||
// Verify timestamp is reasonable (not in far future or past)
|
||||
long currentUnixTime = System.currentTimeMillis() / 1000;
|
||||
long diffSeconds = Math.abs(currentUnixTime - unixTimestamp);
|
||||
long diffDays = diffSeconds / (24 * 60 * 60);
|
||||
log.info("Difference from current time: {} days", diffDays);
|
||||
|
||||
// Reasonable range: within 5 years
|
||||
long maxDiffDays = 5 * 365;
|
||||
assertTrue(diffDays < maxDiffDays,
|
||||
"Timestamp should be within 5 years of current time. Diff: " + diffDays + " days");
|
||||
}
|
||||
|
||||
if (mesg.getTimestamp() != null) {
|
||||
DateTime timestamp = mesg.getTimestamp();
|
||||
long rawTimestamp = timestamp.getTimestamp();
|
||||
long unixTimestamp = rawTimestamp + FIT_EPOCH_OFFSET;
|
||||
Instant instant = Instant.ofEpochSecond(unixTimestamp);
|
||||
|
||||
log.info("Session timestamp (FIT): {} seconds", rawTimestamp);
|
||||
log.info("Session timestamp (Unix): {} seconds", unixTimestamp);
|
||||
log.info("Session timestamp (UTC): {}", ZonedDateTime.ofInstant(instant, ZoneId.of("UTC")));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Capture first record message
|
||||
final boolean[] firstRecordCaptured = {false};
|
||||
broadcaster.addListener(new RecordMesgListener() {
|
||||
@Override
|
||||
public void onMesg(RecordMesg mesg) {
|
||||
if (!firstRecordCaptured[0] && mesg.getTimestamp() != null) {
|
||||
firstRecordCaptured[0] = true;
|
||||
log.info("--- FIRST RECORD MESSAGE ---");
|
||||
DateTime timestamp = mesg.getTimestamp();
|
||||
long rawTimestamp = timestamp.getTimestamp();
|
||||
long unixTimestamp = rawTimestamp + FIT_EPOCH_OFFSET;
|
||||
Instant instant = Instant.ofEpochSecond(unixTimestamp);
|
||||
|
||||
log.info("First record raw timestamp: {} seconds", rawTimestamp);
|
||||
log.info("First record Unix timestamp: {} seconds", unixTimestamp);
|
||||
log.info("First record as UTC: {}", ZonedDateTime.ofInstant(instant, ZoneId.of("UTC")));
|
||||
log.info("First record as LocalDateTime: {}",
|
||||
LocalDateTime.ofInstant(instant, ZoneId.systemDefault()));
|
||||
|
||||
if (mesg.getPositionLat() != null && mesg.getPositionLong() != null) {
|
||||
double lat = mesg.getPositionLat() * (180.0 / Math.pow(2, 31));
|
||||
double lon = mesg.getPositionLong() * (180.0 / Math.pow(2, 31));
|
||||
log.info("First record position: lat={}, lon={}", lat, lon);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Decode the file
|
||||
boolean success = decode.read(new ByteArrayInputStream(fileData), broadcaster);
|
||||
assertTrue(success, "FIT file should decode successfully");
|
||||
|
||||
log.info("");
|
||||
log.info("=== PARSING WITH FitParser ===");
|
||||
ParsedActivityData parsedData = fitParser.parse(fileData);
|
||||
log.info("Parsed start time: {}", parsedData.getStartTime());
|
||||
log.info("Parsed end time: {}", parsedData.getEndTime());
|
||||
log.info("Activity type: {}", parsedData.getActivityType());
|
||||
log.info("Track points: {}", parsedData.getTrackPoints().size());
|
||||
|
||||
if (!parsedData.getTrackPoints().isEmpty()) {
|
||||
ParsedActivityData.TrackPointData firstPoint = parsedData.getTrackPoints().get(0);
|
||||
log.info("First track point timestamp: {}", firstPoint.getTimestamp());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Debug GPX file timestamp parsing with detailed logging")
|
||||
void debugGpxTimestampParsing() throws IOException {
|
||||
log.info("=== GPX FILE TIMESTAMP DEBUGGING ===");
|
||||
log.info("Current system time: {}", LocalDateTime.now());
|
||||
log.info("Current system timezone: {}", ZoneId.systemDefault());
|
||||
log.info("");
|
||||
|
||||
// Load GPX file
|
||||
String gpxFileName = "/7410863774.gpx";
|
||||
InputStream inputStream = getClass().getResourceAsStream(gpxFileName);
|
||||
assertNotNull(inputStream, "GPX file should exist");
|
||||
byte[] fileData = inputStream.readAllBytes();
|
||||
inputStream.close();
|
||||
|
||||
// Parse GPX file
|
||||
ParsedActivityData parsedData = gpxParser.parse(fileData);
|
||||
|
||||
log.info("--- PARSED GPX DATA ---");
|
||||
log.info("Parsed start time: {}", parsedData.getStartTime());
|
||||
log.info("Parsed end time: {}", parsedData.getEndTime());
|
||||
log.info("Activity type: {}", parsedData.getActivityType());
|
||||
log.info("Timezone: {}", parsedData.getTimezone());
|
||||
log.info("Track points: {}", parsedData.getTrackPoints().size());
|
||||
|
||||
if (!parsedData.getTrackPoints().isEmpty()) {
|
||||
ParsedActivityData.TrackPointData firstPoint = parsedData.getTrackPoints().get(0);
|
||||
log.info("First track point timestamp: {}", firstPoint.getTimestamp());
|
||||
log.info("First track point lat/lon: {}, {}", firstPoint.getLatitude(), firstPoint.getLongitude());
|
||||
|
||||
// Check if timestamp is reasonable
|
||||
if (firstPoint.getTimestamp() != null) {
|
||||
long currentUnixTime = System.currentTimeMillis() / 1000;
|
||||
long pointUnixTime = firstPoint.getTimestamp().atZone(ZoneId.systemDefault()).toEpochSecond();
|
||||
long diffSeconds = Math.abs(currentUnixTime - pointUnixTime);
|
||||
long diffDays = diffSeconds / (24 * 60 * 60);
|
||||
log.info("Difference from current time: {} days", diffDays);
|
||||
|
||||
// Verify within reasonable range
|
||||
long maxDiffDays = 10 * 365; // 10 years
|
||||
assertTrue(diffDays < maxDiffDays,
|
||||
"Timestamp should be within 10 years of current time. Diff: " + diffDays + " days");
|
||||
}
|
||||
}
|
||||
|
||||
// Extract and log raw XML timestamp from file
|
||||
String xmlContent = new String(fileData);
|
||||
int timeIdx = xmlContent.indexOf("<time>");
|
||||
if (timeIdx > 0) {
|
||||
int endIdx = xmlContent.indexOf("</time>", timeIdx);
|
||||
if (endIdx > 0) {
|
||||
String rawTimeString = xmlContent.substring(timeIdx + 6, endIdx);
|
||||
log.info("");
|
||||
log.info("Raw XML timestamp string: {}", rawTimeString);
|
||||
log.info("This should be in ISO-8601 format (YYYY-MM-DDTHH:MM:SSZ)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Verify FIT epoch offset calculation")
|
||||
void verifyFitEpochOffset() {
|
||||
log.info("=== FIT EPOCH OFFSET VERIFICATION ===");
|
||||
|
||||
// FIT epoch: 1989-12-31T00:00:00Z
|
||||
// Unix epoch: 1970-01-01T00:00:00Z
|
||||
|
||||
LocalDateTime fitEpoch = LocalDateTime.of(1989, 12, 31, 0, 0, 0);
|
||||
LocalDateTime unixEpoch = LocalDateTime.of(1970, 1, 1, 0, 0, 0);
|
||||
|
||||
ZonedDateTime fitEpochUtc = fitEpoch.atZone(ZoneId.of("UTC"));
|
||||
ZonedDateTime unixEpochUtc = unixEpoch.atZone(ZoneId.of("UTC"));
|
||||
|
||||
long fitEpochSeconds = fitEpochUtc.toEpochSecond();
|
||||
long unixEpochSeconds = unixEpochUtc.toEpochSecond();
|
||||
|
||||
long calculatedOffset = fitEpochSeconds - unixEpochSeconds;
|
||||
final long EXPECTED_OFFSET = 631065600L;
|
||||
|
||||
log.info("Unix epoch: {}", unixEpochUtc);
|
||||
log.info("Unix epoch seconds: {}", unixEpochSeconds);
|
||||
log.info("FIT epoch: {}", fitEpochUtc);
|
||||
log.info("FIT epoch seconds: {}", fitEpochSeconds);
|
||||
log.info("Calculated offset: {} seconds", calculatedOffset);
|
||||
log.info("Expected offset: {} seconds", EXPECTED_OFFSET);
|
||||
log.info("Offset in days: {} days", calculatedOffset / (24 * 60 * 60));
|
||||
log.info("Offset in years: {} years (approx)", calculatedOffset / (24 * 60 * 60 * 365.25));
|
||||
|
||||
assertEquals(EXPECTED_OFFSET, calculatedOffset,
|
||||
"Calculated FIT epoch offset should match expected value");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test manual timestamp conversion examples")
|
||||
void testManualTimestampConversions() {
|
||||
log.info("=== MANUAL TIMESTAMP CONVERSION EXAMPLES ===");
|
||||
|
||||
final long FIT_EPOCH_OFFSET = 631065600L;
|
||||
|
||||
// Example: Convert a known date to see what FIT timestamp it should have
|
||||
// Let's say we know the activity should be from 2024-11-27T15:49:09 UTC
|
||||
LocalDateTime expectedDate = LocalDateTime.of(2024, 11, 27, 15, 49, 9);
|
||||
ZonedDateTime expectedUtc = expectedDate.atZone(ZoneId.of("UTC"));
|
||||
long expectedUnixTimestamp = expectedUtc.toEpochSecond();
|
||||
long expectedFitTimestamp = expectedUnixTimestamp - FIT_EPOCH_OFFSET;
|
||||
|
||||
log.info("Expected date: {}", expectedDate);
|
||||
log.info("Expected Unix timestamp: {}", expectedUnixTimestamp);
|
||||
log.info("Expected FIT timestamp (Unix - offset): {}", expectedFitTimestamp);
|
||||
log.info("");
|
||||
|
||||
// Now reverse: what date do we get if FIT timestamp is X?
|
||||
// This simulates what the parser does
|
||||
long simulatedFitTimestamp = expectedFitTimestamp;
|
||||
long calculatedUnixTimestamp = simulatedFitTimestamp + FIT_EPOCH_OFFSET;
|
||||
Instant instant = Instant.ofEpochSecond(calculatedUnixTimestamp);
|
||||
LocalDateTime calculatedDate = LocalDateTime.ofInstant(instant, ZoneId.of("UTC"));
|
||||
|
||||
log.info("Simulated FIT timestamp: {}", simulatedFitTimestamp);
|
||||
log.info("Calculated Unix timestamp (FIT + offset): {}", calculatedUnixTimestamp);
|
||||
log.info("Calculated date: {}", calculatedDate);
|
||||
log.info("");
|
||||
|
||||
// They should match
|
||||
assertEquals(expectedDate, calculatedDate,
|
||||
"Round-trip conversion should produce the same date");
|
||||
log.info("Round-trip conversion: PASSED ✓");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue