From 6079961f44b78f04a6d020bbd490c7d21eadfcc3 Mon Sep 17 00:00:00 2001 From: fullex <0xfullex@gmail.com> Date: Mon, 15 Sep 2025 17:36:14 +0800 Subject: [PATCH] feat: enhance testing framework for CacheService and PreferenceService - Updated README.md to reflect the expanded testing framework for CacheService, including detailed test modules and scenarios. - Added CacheService tests for direct API, hooks, advanced features, and stress testing. - Refactored TestApp to incorporate CacheService tests alongside existing PreferenceService tests, improving organization and accessibility. - Adjusted component styling to support dark mode and ensure consistent UI across test components. --- src/main/index.ts | 1 + .../src/windows/dataRefactorTest/README.md | 156 +++++- .../src/windows/dataRefactorTest/TestApp.tsx | 391 +++++++++---- .../components/CacheAdvancedTests.tsx | 472 ++++++++++++++++ .../components/CacheBasicTests.tsx | 438 +++++++++++++++ .../components/CacheServiceTests.tsx | 443 +++++++++++++++ .../components/CacheStressTests.tsx | 524 ++++++++++++++++++ .../components/PreferenceBasicTests.tsx | 26 +- .../components/PreferenceServiceTests.tsx | 20 +- 9 files changed, 2317 insertions(+), 154 deletions(-) create mode 100644 src/renderer/src/windows/dataRefactorTest/components/CacheAdvancedTests.tsx create mode 100644 src/renderer/src/windows/dataRefactorTest/components/CacheBasicTests.tsx create mode 100644 src/renderer/src/windows/dataRefactorTest/components/CacheServiceTests.tsx create mode 100644 src/renderer/src/windows/dataRefactorTest/components/CacheStressTests.tsx diff --git a/src/main/index.ts b/src/main/index.ts index d5c3890892..86a67c0728 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -182,6 +182,7 @@ if (!app.requestSingleInstanceLock()) { // const [x, y] = testWindow1.getPosition() // testWindow2.setPosition(x + 50, y + 50) // }) + /************FOR TESTING ONLY END****************/ // Set app user model id for windows diff --git a/src/renderer/src/windows/dataRefactorTest/README.md b/src/renderer/src/windows/dataRefactorTest/README.md index 28009e7ad2..104fbac7a3 100644 --- a/src/renderer/src/windows/dataRefactorTest/README.md +++ b/src/renderer/src/windows/dataRefactorTest/README.md @@ -1,6 +1,6 @@ -# PreferenceService 测试窗口 +# 数据重构项目测试窗口 -专用于测试 PreferenceService 和 usePreference hooks 功能的独立测试窗口系统。 +专用于测试数据重构项目各项功能的独立测试窗口系统,包括 PreferenceService、CacheService、DataApiService 和相关 React hooks。 ## 🎯 当前实现 @@ -18,6 +18,8 @@ ## 测试组件 +## PreferenceService 测试模块 + ### 1. PreferenceService 基础测试 - 直接测试服务层API:get, set, getCachedValue, isCached, preload, getMultiple @@ -53,6 +55,77 @@ - 性能测试 - 多个hook实例同步测试 +## CacheService 测试模块 + +### 1. CacheService 直接API测试 + +- **三层缓存架构测试**:Memory cache、Shared cache、Persist cache +- **基础操作**: get, set, has, delete 方法的完整测试 +- **TTL支持**: 可配置的过期时间测试(2s、5s、10s) +- **跨窗口同步**: Shared cache 和 Persist cache 的实时同步验证 +- **数据类型支持**: 字符串、数字、对象、数组等多种数据类型 +- **性能优化**: 显示操作计数和自动刷新机制 + +### 2. Cache Hooks 基础测试 + +- **useCache Hook**: 测试内存缓存的React集成 + - 默认值自动设置 + - 实时值更新和类型安全 + - Hook生命周期管理 +- **useSharedCache Hook**: 测试跨窗口缓存同步 + - 跨窗口实时同步验证 + - 广播机制测试 + - 并发更新处理 +- **usePersistCache Hook**: 测试持久化缓存 + - 类型安全的预定义Schema + - localStorage持久化 + - 默认值回退机制 +- **数据类型测试**: + - 数字类型滑块控制 + - 复杂对象结构更新 + - 实时渲染统计 + +### 3. Cache 高级功能测试 + +- **TTL过期机制**: + - 实时倒计时进度条 + - 自动过期验证 + - 懒加载清理机制 +- **Hook引用保护**: + - 活跃Hook的key删除保护 + - 引用计数验证 + - 错误处理测试 +- **深度相等性优化**: + - 相同引用跳过测试 + - 相同内容深度比较 + - 性能优化验证 +- **性能测试**: + - 快速更新测试(100次/秒) + - 订阅触发统计 + - 渲染次数监控 +- **多Hook同步**: + - 同一key的多个hook实例 + - 跨缓存类型同步测试 + +### 4. Cache 压力测试 + +- **快速操作测试**: + - 1000次操作/10秒高频测试 + - 每秒操作数统计 + - 错误率监控 +- **并发更新测试**: + - 多个Hook同时更新 + - 跨窗口并发处理 + - 数据一致性验证 +- **大数据测试**: + - 10KB、100KB、1MB对象存储 + - 内存使用估算 + - 存储限制警告 +- **存储限制测试**: + - localStorage容量测试 + - 缓存大小监控 + - 性能影响评估 + ## 启动方式 **自动启动**:应用正常启动时会自动创建两个测试窗口,窗口会自动错位显示避免重叠 @@ -79,36 +152,75 @@ src/renderer/src/windows/dataRefactorTest/ ├── entryPoint.tsx # 窗口入口 ├── TestApp.tsx # 主应用组件 └── components/ + # PreferenceService 测试组件 ├── PreferenceServiceTests.tsx # 服务层测试 ├── PreferenceBasicTests.tsx # 基础Hook测试 ├── PreferenceHookTests.tsx # 高级Hook测试 - └── PreferenceMultipleTests.tsx # 批量操作测试 + ├── PreferenceMultipleTests.tsx # 批量操作测试 + + # CacheService 测试组件 + ├── CacheServiceTests.tsx # 直接API测试 + ├── CacheBasicTests.tsx # Hook基础测试 + ├── CacheAdvancedTests.tsx # 高级功能测试 + ├── CacheStressTests.tsx # 压力测试 + + # DataApiService 测试组件 + ├── DataApiBasicTests.tsx # 基础CRUD测试 + ├── DataApiAdvancedTests.tsx # 高级功能测试 + ├── DataApiHookTests.tsx # React Hooks测试 + └── DataApiStressTests.tsx # 压力测试 ``` ## 跨窗口同步测试 🔄 **测试场景**: +### PreferenceService 跨窗口同步 1. **实时同步验证**:在窗口#1中修改某个偏好设置,立即观察窗口#2是否同步更新 2. **并发修改测试**:在两个窗口中快速连续修改同一设置,验证数据一致性 3. **批量操作同步**:在一个窗口中批量更新多个设置,观察另一个窗口的同步表现 4. **Hook实例同步**:验证多个usePreference hook实例是否正确同步 +### CacheService 跨窗口同步 +1. **Shared Cache同步**:在窗口#1中设置共享缓存,观察窗口#2的实时更新 +2. **Persist Cache同步**:修改持久化缓存,验证所有窗口的localStorage同步 +3. **TTL跨窗口验证**:在一个窗口设置TTL,观察其他窗口的过期行为 +4. **并发缓存操作**:多窗口同时操作同一缓存key,验证数据一致性 +5. **Hook引用保护**:在一个窗口尝试删除其他窗口正在使用的缓存key + 📋 **测试步骤**: +### PreferenceService 测试步骤 1. 同时打开两个测试窗口(自动启动) 2. 选择相同的偏好设置键进行测试 3. 在窗口#1中修改值,观察窗口#2的反应 4. 检查"Hook 高级功能测试"中的订阅触发次数是否增加 5. 验证缓存状态和实时数据的一致性 +### CacheService 测试步骤 +1. 同时打开两个测试窗口(自动启动) +2. **Memory Cache测试**:仅在当前窗口有效,其他窗口不受影响 +3. **Shared Cache测试**:在窗口#1设置共享缓存,立即检查窗口#2是否同步 +4. **Persist Cache测试**:修改持久化缓存,验证localStorage和跨窗口同步 +5. **TTL测试**:设置带过期时间的缓存,观察倒计时和跨窗口过期行为 +6. **压力测试**:运行高频操作,监控性能指标和错误率 +7. **引用保护测试**:在Hook活跃时尝试删除key,验证保护机制 + ## 注意事项 -⚠️ **所有测试操作都会影响真实的数据库存储!** +⚠️ **重要警告**: -- 测试使用真实的偏好设置系统 -- 修改的值会同步到主应用和所有测试窗口 -- 可以在主应用、测试窗口#1、测试窗口#2之间看到实时同步效果 +### PreferenceService 警告 +- **真实数据库存储**:测试使用真实的偏好设置系统 +- **跨应用同步**:修改的值会同步到主应用和所有测试窗口 +- **持久化影响**:所有更改都会持久化到SQLite数据库 + +### CacheService 警告 +- **内存占用**:压力测试可能消耗大量内存,影响浏览器性能 +- **localStorage影响**:大数据测试会占用浏览器存储空间(最大5-10MB) +- **性能影响**:高频操作测试可能短暂影响UI响应性 +- **跨窗口影响**:Shared和Persist缓存会影响所有打开的窗口 +- **TTL清理**:过期缓存会自动清理,可能影响其他功能的测试数据 ## 开发模式特性 @@ -119,6 +231,7 @@ src/renderer/src/windows/dataRefactorTest/ ## 💡 快速开始 +### PreferenceService 快速测试 1. **启动应用** - 自动打开2个测试窗口 2. **选择测试** - 在"usePreference Hook 测试"中选择要测试的偏好设置键 3. **🎛️ Slider联动测试** - 选择数值类型偏好设置,拖动Slider观察实时变化 @@ -126,11 +239,34 @@ src/renderer/src/windows/dataRefactorTest/ 5. **批量Slider测试** - 切换到"数值设置场景",同时拖动多个滑块测试批量同步 6. **高级测试** - 使用"Hook 高级功能测试"验证订阅和缓存机制 +### CacheService 快速测试 +1. **基础操作** - 使用"CacheService 直接API测试"进行get/set/delete操作 +2. **Hook测试** - 在"Cache Hooks 基础测试"中测试不同数据类型和默认值 +3. **TTL验证** - 设置2秒TTL缓存,观察实时倒计时和自动过期 +4. **跨窗口同步** - 设置Shared Cache,在另一窗口验证实时同步 +5. **持久化测试** - 修改Persist Cache,刷新页面验证localStorage持久化 +6. **压力测试** - 运行"快速操作测试",观察高频操作的性能表现 +7. **引用保护** - 启用Hook后尝试删除key,验证保护机制 + ## 🔧 技术实现 +### 基础架构 - **窗口管理**: DataRefactorMigrateService 单例管理多个测试窗口 -- **数据同步**: 基于真实的 PreferenceService 和 IPC 通信 -- **实时主题**: 使用 useSyncExternalStore 实现主题、缩放等设置的实时UI响应 - **跨窗口识别**: 多源窗口编号支持,确保每个窗口都有唯一标识 - **UI框架**: Ant Design + styled-components + React 18 -- **类型安全**: 完整的 TypeScript 类型检查和偏好设置键约束 +- **类型安全**: 完整的 TypeScript 类型检查和类型约束 + +### PreferenceService 技术实现 +- **数据同步**: 基于真实的 PreferenceService 和 IPC 通信 +- **实时主题**: 使用 useSyncExternalStore 实现主题、缩放等设置的实时UI响应 +- **类型约束**: 偏好设置键的完整TypeScript类型检查 + +### CacheService 技术实现 +- **三层缓存**: Memory (Map) + Shared (Map + IPC) + Persist (Map + localStorage) +- **React集成**: useSyncExternalStore 实现外部状态订阅 +- **性能优化**: Object.is() 浅比较 + 深度相等性检查,跳过无效更新 +- **TTL管理**: 懒加载过期检查,基于时间戳的精确控制 +- **IPC同步**: 跨进程消息广播,支持批量操作和增量更新 +- **引用跟踪**: Set-based Hook引用计数,防止意外删除 +- **错误处理**: 完善的try-catch机制和用户友好的错误提示 +- **内存管理**: 自动清理、定时器管理和资源释放 diff --git a/src/renderer/src/windows/dataRefactorTest/TestApp.tsx b/src/renderer/src/windows/dataRefactorTest/TestApp.tsx index 53610eb6b7..5757db127d 100644 --- a/src/renderer/src/windows/dataRefactorTest/TestApp.tsx +++ b/src/renderer/src/windows/dataRefactorTest/TestApp.tsx @@ -2,11 +2,15 @@ import { AppLogo } from '@renderer/config/env' import { usePreference } from '@renderer/data/hooks/usePreference' import { loggerService } from '@renderer/services/LoggerService' import { ThemeMode } from '@shared/data/preference/preferenceTypes' -import { Button, Card, Col, Divider, Layout, Row, Space, Typography } from 'antd' +import { Button, Card, Col, Divider, Layout, Row, Space, Typography, Tabs } from 'antd' import { Activity, AlertTriangle, Database, FlaskConical, Settings, TestTube, TrendingUp, Zap } from 'lucide-react' import React from 'react' import styled from 'styled-components' +import CacheAdvancedTests from './components/CacheAdvancedTests' +import CacheBasicTests from './components/CacheBasicTests' +import CacheServiceTests from './components/CacheServiceTests' +import CacheStressTests from './components/CacheStressTests' import DataApiAdvancedTests from './components/DataApiAdvancedTests' import DataApiBasicTests from './components/DataApiBasicTests' import DataApiHookTests from './components/DataApiHookTests' @@ -100,149 +104,256 @@ const TestApp: React.FC = () => { - 此测试窗口用于验证数据重构项目的各项功能,包括 PreferenceService、DataApiService 和相关 React hooks + 此测试窗口用于验证数据重构项目的各项功能,包括 PreferenceService、CacheService、DataApiService 和相关 React hooks 的完整测试套件。 - PreferenceService 测试使用真实的偏好设置系统,DataApiService 测试使用专用的测试路由和假数据。 + PreferenceService 测试使用真实的偏好设置系统,CacheService 测试使用三层缓存架构,DataApiService 测试使用专用的测试路由和假数据。 📋 跨窗口测试指南:在一个窗口中修改偏好设置,观察其他窗口是否实时同步更新。 + 🗄️ 缓存系统测试:三层缓存架构(Memory/Shared/Persist),支持跨窗口同步、TTL过期、性能优化。 + + 🚀 数据API测试:包含基础CRUD、高级功能、React hooks和压力测试,全面验证数据请求架构。 - {/* PreferenceService Basic Tests */} + {/* Main Content Tabs */} - - - PreferenceService 基础测试 - - } - size="small" - style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> - - - + + + PreferenceService 测试 + + ), + children: ( + + {/* PreferenceService Basic Tests */} + + + + PreferenceService 基础测试 + + } + size="small" + style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> + + + - {/* Basic Hook Tests */} - - - - usePreference Hook 测试 - - } - size="small" - style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> - - - + {/* Basic Hook Tests */} + + + + usePreference Hook 测试 + + } + size="small" + style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> + + + - {/* Hook Tests */} - - - - Hook 高级功能测试 - - } - size="small" - style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> - - - + {/* Hook Tests */} + + + + Hook 高级功能测试 + + } + size="small" + style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> + + + - {/* Multiple Preferences Tests */} - - - - usePreferences 批量操作测试 - - } - size="small" - style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> - - - - + {/* Multiple Preferences Tests */} + + + + usePreferences 批量操作测试 + + } + size="small" + style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> + + + + + ) + }, + { + key: 'cache', + label: ( + + + CacheService 测试 + + ), + children: ( + + {/* Cache Service Tests */} + + + + CacheService 直接API测试 + + } + size="small" + style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> + + + - - - - DataApiService 功能测试 - - + {/* Cache Basic Tests */} + + + + Cache Hooks 基础测试 + + } + size="small" + style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> + + + - - {/* DataApi Basic Tests */} - - - - DataApi 基础功能测试 (CRUD操作) - - } - size="small" - style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> - - - + {/* Cache Advanced Tests */} + + + + Cache 高级功能测试 + + } + size="small" + style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> + + + - {/* DataApi Advanced Tests */} - - - - DataApi 高级功能测试 (取消、重试、批量) - - } - size="small" - style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> - - - + {/* Cache Stress Tests */} + + + + Cache 压力测试 + + } + size="small" + style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> + + + + + ) + }, + { + key: 'dataapi', + label: ( + + + DataApiService 测试 + + ), + children: ( + + {/* DataApi Basic Tests */} + + + + DataApi 基础功能测试 (CRUD操作) + + } + size="small" + style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> + + + - {/* DataApi Hook Tests */} - - - - DataApi React Hooks 测试 - - } - size="small" - style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> - - - + {/* DataApi Advanced Tests */} + + + + DataApi 高级功能测试 (取消、重试、批量) + + } + size="small" + style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> + + + - {/* DataApi Stress Tests */} - - - - DataApi 压力测试 (性能与错误处理) - - } - size="small" - style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> - - + {/* DataApi Hook Tests */} + + + + DataApi React Hooks 测试 + + } + size="small" + style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> + + + + + {/* DataApi Stress Tests */} + + + + DataApi 压力测试 (性能与错误处理) + + } + size="small" + style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> + + + + + ) + } + ]} + /> @@ -290,4 +401,42 @@ const Container = styled.div` margin: 0 auto; ` +const StyledTabs = styled(Tabs)<{ $isDark: boolean }>` + .ant-tabs-nav { + background: ${props => props.$isDark ? '#262626' : '#fafafa'}; + border-radius: 6px 6px 0 0; + margin-bottom: 0; + } + + .ant-tabs-tab { + color: ${props => props.$isDark ? '#d9d9d9' : '#666'} !important; + + &:hover { + color: ${props => props.$isDark ? '#fff' : '#000'} !important; + } + + &.ant-tabs-tab-active { + color: ${props => props.$isDark ? '#1890ff' : '#1890ff'} !important; + + .ant-tabs-tab-btn { + color: ${props => props.$isDark ? '#1890ff' : '#1890ff'} !important; + } + } + } + + .ant-tabs-ink-bar { + background: ${props => props.$isDark ? '#1890ff' : '#1890ff'}; + } + + .ant-tabs-content { + background: ${props => props.$isDark ? '#1f1f1f' : '#fff'}; + border-radius: 0 0 6px 6px; + padding: 24px 0; + } + + .ant-tabs-tabpane { + color: ${props => props.$isDark ? '#fff' : '#000'}; + } +` + export default TestApp diff --git a/src/renderer/src/windows/dataRefactorTest/components/CacheAdvancedTests.tsx b/src/renderer/src/windows/dataRefactorTest/components/CacheAdvancedTests.tsx new file mode 100644 index 0000000000..e8809e07ee --- /dev/null +++ b/src/renderer/src/windows/dataRefactorTest/components/CacheAdvancedTests.tsx @@ -0,0 +1,472 @@ +import { cacheService } from '@renderer/data/CacheService' +import { useCache, useSharedCache } from '@renderer/data/hooks/useCache' +import { usePreference } from '@renderer/data/hooks/usePreference' +import { loggerService } from '@renderer/services/LoggerService' +import { ThemeMode } from '@shared/data/preference/preferenceTypes' +import { Button, Input, message, Space, Typography, Card, Row, Col, Divider, Progress, Badge, Tag } from 'antd' +import { Clock, Shield, Zap, Activity, AlertTriangle, CheckCircle, XCircle, Timer } from 'lucide-react' +import React, { useState, useEffect, useRef, useCallback } from 'react' +import styled from 'styled-components' + +const { Text, Title } = Typography + +const logger = loggerService.withContext('CacheAdvancedTests') + +/** + * Advanced cache testing component + * Tests TTL expiration, hook reference tracking, deep equality, performance + */ +const CacheAdvancedTests: React.FC = () => { + const [currentTheme] = usePreference('ui.theme_mode') + const isDarkTheme = currentTheme === ThemeMode.dark + + // TTL Testing + const [ttlKey] = useState('test-ttl-cache') + const [ttlValue, setTtlValue] = useCache(ttlKey) + const [ttlExpireTime, setTtlExpireTime] = useState(null) + const [ttlProgress, setTtlProgress] = useState(0) + + // Hook Reference Tracking + const [protectedKey] = useState('test-protected-cache') + const [protectedValue, setProtectedValue] = useCache(protectedKey, 'protected-value') + const [deleteAttemptResult, setDeleteAttemptResult] = useState('') + + // Deep Equality Testing + const [deepEqualKey] = useState('test-deep-equal') + const [objectValue, setObjectValue] = useCache(deepEqualKey, { nested: { count: 0 }, tags: ['initial'] }) + const [updateSkipCount, setUpdateSkipCount] = useState(0) + + // Performance Testing + const [perfKey] = useState('test-performance') + const [perfValue, setPerfValue] = useCache(perfKey, 0) + const [rapidUpdateCount, setRapidUpdateCount] = useState(0) + const [subscriptionTriggers, setSubscriptionTriggers] = useState(0) + const renderCountRef = useRef(0) + const [displayRenderCount, setDisplayRenderCount] = useState(0) + + // Multi-hook testing + const [multiKey] = useState('test-multi-hook') + const [value1] = useCache(multiKey, 'hook-1-default') + const [value2] = useCache(multiKey, 'hook-2-default') + const [value3] = useSharedCache(multiKey, 'hook-3-shared') + + const intervalRef = useRef() + const performanceTestRef = useRef() + + // Update render count without causing re-renders + renderCountRef.current += 1 + + // Track subscription changes + useEffect(() => { + const unsubscribe = cacheService.subscribe(perfKey, () => { + setSubscriptionTriggers(prev => prev + 1) + }) + return unsubscribe + }, [perfKey]) + + // TTL Testing Functions + const startTTLTest = useCallback((ttlMs: number) => { + const testValue = { message: 'TTL Test', timestamp: Date.now() } + cacheService.set(ttlKey, testValue, ttlMs) + setTtlValue(testValue) + + const expireAt = Date.now() + ttlMs + setTtlExpireTime(expireAt) + + // Clear previous interval + if (intervalRef.current) { + clearInterval(intervalRef.current) + } + + // Update progress every 100ms + intervalRef.current = setInterval(() => { + const now = Date.now() + const remaining = Math.max(0, expireAt - now) + const progress = Math.max(0, 100 - (remaining / ttlMs) * 100) + + setTtlProgress(progress) + + if (remaining <= 0) { + clearInterval(intervalRef.current!) + setTtlExpireTime(null) + message.info('TTL expired, checking value...') + + // Check if value is actually expired + setTimeout(() => { + const currentValue = cacheService.get(ttlKey) + if (currentValue === undefined) { + message.success('TTL expiration working correctly!') + } else { + message.warning('TTL expiration may have failed') + } + }, 100) + } + }, 100) + + message.info(`TTL test started: ${ttlMs}ms`) + logger.info('TTL test started', { key: ttlKey, ttl: ttlMs, expireAt }) + }, [ttlKey, setTtlValue]) + + // Hook Reference Tracking Test + const testDeleteProtection = () => { + try { + const deleted = cacheService.delete(protectedKey) + setDeleteAttemptResult(deleted ? 'Deleted (unexpected!)' : 'Protected (expected)') + logger.info('Delete protection test', { key: protectedKey, deleted }) + } catch (error) { + setDeleteAttemptResult(`Error: ${(error as Error).message}`) + logger.error('Delete protection test error', error as Error) + } + } + + // Deep Equality Testing + const testDeepEquality = (operation: string) => { + const currentCount = updateSkipCount + + switch (operation) { + case 'same-reference': + // Set same reference - should skip + setObjectValue(objectValue) + break + + case 'same-content': + // Set same content but different reference - should skip with deep comparison + setObjectValue({ nested: { count: objectValue?.nested?.count || 0 }, tags: [...(objectValue?.tags || [])] }) + break + + case 'different-content': + // Set different content - should update + setObjectValue({ + nested: { count: (objectValue?.nested?.count || 0) + 1 }, + tags: [...(objectValue?.tags || []), `update-${Date.now()}`] + }) + break + } + + // Check if update count changed + setTimeout(() => { + if (currentCount === updateSkipCount) { + message.success('Update skipped due to equality check') + } else { + message.info('Update applied due to content change') + } + }, 100) + + logger.info('Deep equality test', { operation, currentCount, objectValue }) + } + + // Performance Testing + const startRapidUpdates = () => { + let count = 0 + const startTime = Date.now() + + performanceTestRef.current = setInterval(() => { + count++ + setPerfValue(count) + setRapidUpdateCount(count) + + if (count >= 100) { + clearInterval(performanceTestRef.current!) + const duration = Date.now() - startTime + message.success(`Rapid updates test completed: ${count} updates in ${duration}ms`) + logger.info('Rapid updates test completed', { count, duration }) + } + }, 10) // Update every 10ms + } + + const stopRapidUpdates = () => { + if (performanceTestRef.current) { + clearInterval(performanceTestRef.current) + message.info('Rapid updates test stopped') + } + } + + // Cleanup + useEffect(() => { + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current) + } + if (performanceTestRef.current) { + clearInterval(performanceTestRef.current) + } + } + }, []) + + return ( + + +
+ + Advanced Features • Renders: {displayRenderCount || renderCountRef.current} • Subscriptions: {subscriptionTriggers} + + +
+ + + {/* TTL Testing */} + + + + TTL Expiration Testing +
+ } + size="small" + style={{ + backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', + borderColor: isDarkTheme ? '#303030' : '#d9d9d9' + }} + > + + Key: {ttlKey} + + + + + + + + {ttlExpireTime && ( +
+ Expiration Progress: + = 100 ? 'success' : 'active'} + strokeColor={isDarkTheme ? '#1890ff' : undefined} + /> +
+ )} + + + Current Value: +
{ttlValue ? JSON.stringify(ttlValue, null, 2) : 'undefined'}
+
+
+ + + + {/* Hook Reference Tracking */} + + + + Hook Reference Protection + + } + size="small" + style={{ + backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', + borderColor: isDarkTheme ? '#303030' : '#d9d9d9' + }} + > + + Key: {protectedKey} + + + + + {deleteAttemptResult && ( + + {deleteAttemptResult} + + )} + + + Current Value: +
{JSON.stringify(protectedValue, null, 2)}
+
+
+
+ + + + + {/* Deep Equality Testing */} + + + + Deep Equality Optimization + + } + size="small" + style={{ + backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', + borderColor: isDarkTheme ? '#303030' : '#d9d9d9' + }} + > + + Key: {deepEqualKey} + Skip Count: + + + + + + + + + Current Object: +
{JSON.stringify(objectValue, null, 2)}
+
+
+
+ + + {/* Performance Testing */} + + + + Performance Testing + + } + size="small" + style={{ + backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', + borderColor: isDarkTheme ? '#303030' : '#d9d9d9' + }} + > + + Key: {perfKey} + Updates: + + + + + + + + Performance Value: +
{JSON.stringify(perfValue, null, 2)}
+
+
+
+ +
+ + + + {/* Multi-Hook Synchronization */} + + + Multi-Hook Synchronization Test + + } + size="small" + style={{ + backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', + borderColor: isDarkTheme ? '#303030' : '#d9d9d9' + }} + > + + Testing multiple hooks using the same key: {multiKey} + + + + + +
{JSON.stringify(value1, null, 2)}
+
+
+ + + + +
{JSON.stringify(value2, null, 2)}
+
+
+ + + + +
{JSON.stringify(value3, null, 2)}
+
+
+ +
+ + + + + +
+
+ +
+ + 💡 高级功能测试: TTL过期机制、Hook引用保护、深度相等性优化、性能测试、多Hook同步验证 + +
+ +
+ ) +} + +const TestContainer = styled.div<{ $isDark: boolean }>` + color: ${props => props.$isDark ? '#fff' : '#000'}; +` + +const ResultDisplay = styled.div<{ $isDark: boolean }>` + background: ${props => props.$isDark ? '#0d1117' : '#f6f8fa'}; + border: 1px solid ${props => props.$isDark ? '#30363d' : '#d0d7de'}; + border-radius: 6px; + padding: 8px; + font-size: 11px; + max-height: 120px; + overflow-y: auto; + + pre { + margin: 0; + white-space: pre-wrap; + word-break: break-all; + color: ${props => props.$isDark ? '#e6edf3' : '#1f2328'}; + font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; + } +` + +export default CacheAdvancedTests \ No newline at end of file diff --git a/src/renderer/src/windows/dataRefactorTest/components/CacheBasicTests.tsx b/src/renderer/src/windows/dataRefactorTest/components/CacheBasicTests.tsx new file mode 100644 index 0000000000..d44e6e84a5 --- /dev/null +++ b/src/renderer/src/windows/dataRefactorTest/components/CacheBasicTests.tsx @@ -0,0 +1,438 @@ +import { useCache, useSharedCache, usePersistCache } from '@renderer/data/hooks/useCache' +import { usePreference } from '@renderer/data/hooks/usePreference' +import { loggerService } from '@renderer/services/LoggerService' +import type { PersistCacheKey } from '@shared/data/cache/cacheSchemas' +import { ThemeMode } from '@shared/data/preference/preferenceTypes' +import { Button, Input, message, Select, Space, Typography, Card, Row, Col, Divider, Slider } from 'antd' +import { Zap, Database, Eye, Edit, RefreshCw, Users, HardDrive } from 'lucide-react' +import React, { useState, useEffect, useRef } from 'react' +import styled from 'styled-components' + +const { Text, Title } = Typography +const { Option } = Select +const { TextArea } = Input + +const logger = loggerService.withContext('CacheBasicTests') + +/** + * Basic cache hooks testing component + * Tests useCache, useSharedCache, and usePersistCache hooks + */ +const CacheBasicTests: React.FC = () => { + const [currentTheme] = usePreference('ui.theme_mode') + const isDarkTheme = currentTheme === ThemeMode.dark + + // useCache testing + const [memoryCacheKey, setMemoryCacheKey] = useState('test-hook-memory-1') + const [memoryCacheDefault, setMemoryCacheDefault] = useState('default-memory-value') + const [newMemoryValue, setNewMemoryValue] = useState('') + const [memoryValue, setMemoryValue] = useCache(memoryCacheKey, memoryCacheDefault) + + // useSharedCache testing + const [sharedCacheKey, setSharedCacheKey] = useState('test-hook-shared-1') + const [sharedCacheDefault, setSharedCacheDefault] = useState('default-shared-value') + const [newSharedValue, setNewSharedValue] = useState('') + const [sharedValue, setSharedValue] = useSharedCache(sharedCacheKey, sharedCacheDefault) + + // usePersistCache testing + const [persistCacheKey, setPersistCacheKey] = useState('example-1') + const [newPersistValue, setNewPersistValue] = useState('') + const [persistValue, setPersistValue] = usePersistCache(persistCacheKey) + + // Testing different data types + const [numberKey] = useState('test-number-cache') + const [numberValue, setNumberValue] = useCache(numberKey, 42) + + const [objectKey] = useState('test-object-cache') + const [objectValue, setObjectValue] = useCache(objectKey, { name: 'test', count: 0, active: true }) + + // Stats + const renderCountRef = useRef(0) + const [displayRenderCount, setDisplayRenderCount] = useState(0) + const [updateCount, setUpdateCount] = useState(0) + + // Available persist keys + const persistKeys: PersistCacheKey[] = ['example-1', 'example-2', 'example-3', 'example-4'] + + // Update render count without causing re-renders + renderCountRef.current += 1 + + const parseValue = (value: string): any => { + if (!value) return undefined + try { + return JSON.parse(value) + } catch { + return value + } + } + + const formatValue = (value: any): string => { + if (value === undefined) return 'undefined' + if (value === null) return 'null' + if (typeof value === 'string') return `"${value}"` + return JSON.stringify(value, null, 2) + } + + // Memory cache operations + const handleMemoryUpdate = () => { + try { + const parsed = parseValue(newMemoryValue) + setMemoryValue(parsed) + setNewMemoryValue('') + setUpdateCount(prev => prev + 1) + message.success(`Memory cache updated: ${memoryCacheKey}`) + logger.info('Memory cache updated via hook', { key: memoryCacheKey, value: parsed }) + } catch (error) { + message.error(`Memory cache update failed: ${(error as Error).message}`) + } + } + + // Shared cache operations + const handleSharedUpdate = () => { + try { + const parsed = parseValue(newSharedValue) + setSharedValue(parsed) + setNewSharedValue('') + setUpdateCount(prev => prev + 1) + message.success(`Shared cache updated: ${sharedCacheKey} (broadcasted to other windows)`) + logger.info('Shared cache updated via hook', { key: sharedCacheKey, value: parsed }) + } catch (error) { + message.error(`Shared cache update failed: ${(error as Error).message}`) + } + } + + // Persist cache operations + const handlePersistUpdate = () => { + try { + let parsed: any + // Handle different types based on schema + if (persistCacheKey === 'example-1') { + parsed = newPersistValue // string + } else if (persistCacheKey === 'example-2') { + parsed = parseInt(newPersistValue) || 0 // number + } else if (persistCacheKey === 'example-3') { + parsed = newPersistValue === 'true' // boolean + } else if (persistCacheKey === 'example-4') { + parsed = parseValue(newPersistValue) // object + } + + setPersistValue(parsed as any) + setNewPersistValue('') + setUpdateCount(prev => prev + 1) + message.success(`Persist cache updated: ${persistCacheKey} (saved + broadcasted)`) + logger.info('Persist cache updated via hook', { key: persistCacheKey, value: parsed }) + } catch (error) { + message.error(`Persist cache update failed: ${(error as Error).message}`) + } + } + + // Test different data types + const handleNumberUpdate = (newValue: number) => { + setNumberValue(newValue) + setUpdateCount(prev => prev + 1) + logger.info('Number cache updated', { value: newValue }) + } + + const handleObjectUpdate = (field: string, value: any) => { + setObjectValue(prev => ({ ...prev, [field]: value })) + setUpdateCount(prev => prev + 1) + logger.info('Object cache updated', { field, value }) + } + + return ( + + +
+ + React Hook Tests • Renders: {displayRenderCount || renderCountRef.current} • Updates: {updateCount} + + +
+ + + {/* useCache Testing */} + + + + useCache Hook +
+ } + size="small" + style={{ + backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', + borderColor: isDarkTheme ? '#303030' : '#d9d9d9' + }} + > + + setMemoryCacheKey(e.target.value)} + prefix={} + /> + + setMemoryCacheDefault(e.target.value)} + prefix={} + /> + + setNewMemoryValue(e.target.value)} + onPressEnter={handleMemoryUpdate} + prefix={} + /> + + + + + Current Value: +
{formatValue(memoryValue)}
+
+
+ + + + {/* useSharedCache Testing */} + + + + useSharedCache Hook + + } + size="small" + style={{ + backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', + borderColor: isDarkTheme ? '#303030' : '#d9d9d9' + }} + > + + setSharedCacheKey(e.target.value)} + prefix={} + /> + + setSharedCacheDefault(e.target.value)} + prefix={} + /> + + setNewSharedValue(e.target.value)} + onPressEnter={handleSharedUpdate} + prefix={} + /> + + + + + Current Value: +
{formatValue(sharedValue)}
+
+
+
+ + + {/* usePersistCache Testing */} + + + + usePersistCache Hook + + } + size="small" + style={{ + backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', + borderColor: isDarkTheme ? '#303030' : '#d9d9d9' + }} + > + + + + setNewPersistValue(e.target.value)} + onPressEnter={handlePersistUpdate} + prefix={} + /> + + + + + Current Value: +
{formatValue(persistValue)}
+
+
+
+ + + + + + {/* Data Type Testing */} + + + + + Number Type Testing + + } + size="small" + style={{ + backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', + borderColor: isDarkTheme ? '#303030' : '#d9d9d9' + }} + > + + Key: {numberKey} + Current Value: {numberValue} + + + + + + + + + + + + + + + Object Type Testing + + } + size="small" + style={{ + backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', + borderColor: isDarkTheme ? '#303030' : '#d9d9d9' + }} + > + + Key: {objectKey} + + + handleObjectUpdate('name', e.target.value)} + style={{ width: 120 }} + /> + handleObjectUpdate('count', parseInt(e.target.value) || 0)} + style={{ width: 80 }} + /> + + + + +
{formatValue(objectValue)}
+
+
+
+ +
+ +
+ + 💡 提示: useCache 仅在当前窗口有效 • useSharedCache 跨窗口实时同步 • usePersistCache 类型安全的持久化存储 + +
+ +
+ ) +} + +const TestContainer = styled.div<{ $isDark: boolean }>` + color: ${props => props.$isDark ? '#fff' : '#000'}; +` + +const ResultDisplay = styled.div<{ $isDark: boolean }>` + background: ${props => props.$isDark ? '#0d1117' : '#f6f8fa'}; + border: 1px solid ${props => props.$isDark ? '#30363d' : '#d0d7de'}; + border-radius: 6px; + padding: 8px; + font-size: 11px; + max-height: 100px; + overflow-y: auto; + + pre { + margin: 0; + white-space: pre-wrap; + word-break: break-all; + color: ${props => props.$isDark ? '#e6edf3' : '#1f2328'}; + font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; + } +` + +export default CacheBasicTests \ No newline at end of file diff --git a/src/renderer/src/windows/dataRefactorTest/components/CacheServiceTests.tsx b/src/renderer/src/windows/dataRefactorTest/components/CacheServiceTests.tsx new file mode 100644 index 0000000000..d9e00a192a --- /dev/null +++ b/src/renderer/src/windows/dataRefactorTest/components/CacheServiceTests.tsx @@ -0,0 +1,443 @@ +import { cacheService } from '@renderer/data/CacheService' +import { loggerService } from '@renderer/services/LoggerService' +import type { PersistCacheKey, PersistCacheSchema } from '@shared/data/cache/cacheSchemas' +import { usePreference } from '@renderer/data/hooks/usePreference' +import { ThemeMode } from '@shared/data/preference/preferenceTypes' +import { Button, Input, message, Select, Space, Typography, Card, Row, Col, Divider } from 'antd' +import { Database, Clock, Trash2, Eye, Edit, Zap } from 'lucide-react' +import React, { useState, useEffect } from 'react' +import styled from 'styled-components' + +const { Text, Title } = Typography +const { Option } = Select +const { TextArea } = Input + +const logger = loggerService.withContext('CacheServiceTests') + +/** + * Direct CacheService API testing component + * Tests memory, shared, and persist cache operations + */ +const CacheServiceTests: React.FC = () => { + const [currentTheme] = usePreference('ui.theme_mode') + const isDarkTheme = currentTheme === ThemeMode.dark + + // State for test operations + const [memoryKey, setMemoryKey] = useState('test-memory-1') + const [memoryValue, setMemoryValue] = useState('{"type": "memory", "data": "test"}') + const [memoryTTL, setMemoryTTL] = useState('5000') + + const [sharedKey, setSharedKey] = useState('test-shared-1') + const [sharedValue, setSharedValue] = useState('{"type": "shared", "data": "cross-window"}') + const [sharedTTL, setSharedTTL] = useState('10000') + + const [persistKey, setPersistKey] = useState('example-1') + const [persistValue, setPersistValue] = useState('updated-example-value') + + // Display states + const [memoryResult, setMemoryResult] = useState(null) + const [sharedResult, setSharedResult] = useState(null) + const [persistResult, setPersistResult] = useState(null) + + const [updateCount, setUpdateCount] = useState(0) + + // Available persist keys from schema + const persistKeys: PersistCacheKey[] = ['example-1', 'example-2', 'example-3', 'example-4'] + + const parseValue = (value: string): any => { + if (!value) return undefined + try { + return JSON.parse(value) + } catch { + return value // Return as string if not valid JSON + } + } + + const formatValue = (value: any): string => { + if (value === undefined) return 'undefined' + if (value === null) return 'null' + if (typeof value === 'string') return `"${value}"` + return JSON.stringify(value, null, 2) + } + + // Memory Cache Operations + const handleMemorySet = () => { + try { + const parsed = parseValue(memoryValue) + const ttl = memoryTTL ? parseInt(memoryTTL) : undefined + cacheService.set(memoryKey, parsed, ttl) + message.success(`Memory cache set: ${memoryKey}`) + setUpdateCount(prev => prev + 1) + logger.info('Memory cache set', { key: memoryKey, value: parsed, ttl }) + } catch (error) { + message.error(`Memory cache set failed: ${(error as Error).message}`) + logger.error('Memory cache set failed', error as Error) + } + } + + const handleMemoryGet = () => { + try { + const result = cacheService.get(memoryKey) + setMemoryResult(result) + message.info(`Memory cache get: ${memoryKey}`) + logger.info('Memory cache get', { key: memoryKey, result }) + } catch (error) { + message.error(`Memory cache get failed: ${(error as Error).message}`) + logger.error('Memory cache get failed', error as Error) + } + } + + const handleMemoryHas = () => { + try { + const exists = cacheService.has(memoryKey) + message.info(`Memory cache has ${memoryKey}: ${exists}`) + logger.info('Memory cache has', { key: memoryKey, exists }) + } catch (error) { + message.error(`Memory cache has failed: ${(error as Error).message}`) + } + } + + const handleMemoryDelete = () => { + try { + const deleted = cacheService.delete(memoryKey) + message.info(`Memory cache delete ${memoryKey}: ${deleted}`) + setMemoryResult(undefined) + logger.info('Memory cache delete', { key: memoryKey, deleted }) + } catch (error) { + message.error(`Memory cache delete failed: ${(error as Error).message}`) + } + } + + // Shared Cache Operations + const handleSharedSet = () => { + try { + const parsed = parseValue(sharedValue) + const ttl = sharedTTL ? parseInt(sharedTTL) : undefined + cacheService.setShared(sharedKey, parsed, ttl) + message.success(`Shared cache set: ${sharedKey} (broadcasted to other windows)`) + setUpdateCount(prev => prev + 1) + logger.info('Shared cache set', { key: sharedKey, value: parsed, ttl }) + } catch (error) { + message.error(`Shared cache set failed: ${(error as Error).message}`) + logger.error('Shared cache set failed', error as Error) + } + } + + const handleSharedGet = () => { + try { + const result = cacheService.getShared(sharedKey) + setSharedResult(result) + message.info(`Shared cache get: ${sharedKey}`) + logger.info('Shared cache get', { key: sharedKey, result }) + } catch (error) { + message.error(`Shared cache get failed: ${(error as Error).message}`) + logger.error('Shared cache get failed', error as Error) + } + } + + const handleSharedHas = () => { + try { + const exists = cacheService.hasShared(sharedKey) + message.info(`Shared cache has ${sharedKey}: ${exists}`) + logger.info('Shared cache has', { key: sharedKey, exists }) + } catch (error) { + message.error(`Shared cache has failed: ${(error as Error).message}`) + } + } + + const handleSharedDelete = () => { + try { + const deleted = cacheService.deleteShared(sharedKey) + message.info(`Shared cache delete ${sharedKey}: ${deleted} (broadcasted to other windows)`) + setSharedResult(undefined) + logger.info('Shared cache delete', { key: sharedKey, deleted }) + } catch (error) { + message.error(`Shared cache delete failed: ${(error as Error).message}`) + } + } + + // Persist Cache Operations + const handlePersistSet = () => { + try { + let parsed: any + // Handle different types based on the schema + if (persistKey === 'example-1') { + parsed = persistValue // string + } else if (persistKey === 'example-2') { + parsed = parseInt(persistValue) || 0 // number + } else if (persistKey === 'example-3') { + parsed = persistValue === 'true' // boolean + } else if (persistKey === 'example-4') { + parsed = parseValue(persistValue) // object + } + + cacheService.setPersist(persistKey, parsed as PersistCacheSchema[typeof persistKey]) + message.success(`Persist cache set: ${persistKey} (saved to localStorage + broadcasted)`) + setUpdateCount(prev => prev + 1) + logger.info('Persist cache set', { key: persistKey, value: parsed }) + } catch (error) { + message.error(`Persist cache set failed: ${(error as Error).message}`) + logger.error('Persist cache set failed', error as Error) + } + } + + const handlePersistGet = () => { + try { + const result = cacheService.getPersist(persistKey) + setPersistResult(result) + message.info(`Persist cache get: ${persistKey}`) + logger.info('Persist cache get', { key: persistKey, result }) + } catch (error) { + message.error(`Persist cache get failed: ${(error as Error).message}`) + logger.error('Persist cache get failed', error as Error) + } + } + + const handlePersistHas = () => { + try { + const exists = cacheService.hasPersist(persistKey) + message.info(`Persist cache has ${persistKey}: ${exists}`) + logger.info('Persist cache has', { key: persistKey, exists }) + } catch (error) { + message.error(`Persist cache has failed: ${(error as Error).message}`) + } + } + + // Auto-refresh results + useEffect(() => { + const interval = setInterval(() => { + // Auto-get current values for display + try { + const memResult = cacheService.get(memoryKey) + const sharedResult = cacheService.getShared(sharedKey) + const persistResult = cacheService.getPersist(persistKey) + + setMemoryResult(memResult) + setSharedResult(sharedResult) + setPersistResult(persistResult) + } catch (error) { + logger.error('Auto-refresh failed', error as Error) + } + }, 1000) + + return () => clearInterval(interval) + }, [memoryKey, sharedKey, persistKey]) + + return ( + + +
+ + 直接测试 CacheService API • Updates: {updateCount} • Auto-refresh: 1s + +
+ + + {/* Memory Cache Section */} + + + + Memory Cache +
+ } + size="small" + style={{ + backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', + borderColor: isDarkTheme ? '#303030' : '#d9d9d9' + }} + > + + setMemoryKey(e.target.value)} + prefix={} + /> + +