diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index a0680e1b..57c0c58c 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -364,10 +364,19 @@ vi.mock('./DataGrid', () => ({ })); vi.mock('./LogPanel', () => ({ - default: ({ variant, executionError }: { variant?: string; executionError?: string }) => ( + default: ({ + variant, + executionError, + onDiagnoseExecutionError, + }: { + variant?: string; + executionError?: string; + onDiagnoseExecutionError?: () => void; + }) => (
SQL 执行日志 - {executionError ? ` ${executionError}` : ''} + {executionError ? ` 执行失败 ${executionError}` : ''} + {onDiagnoseExecutionError ? : null}
), })); @@ -2514,6 +2523,7 @@ describe('QueryEditor external SQL save', () => { vi.useFakeTimers(); try { storeState.aiPanelVisible = true; + storeState.appearance.uiVersion = 'v2'; let renderer!: ReactTestRenderer; await act(async () => { @@ -2554,6 +2564,8 @@ describe('QueryEditor external SQL save', () => { await Promise.resolve(); }); + expect(textContent(renderer.toJSON())).toContain('SQL 执行日志'); + await act(async () => { findButton(renderer, 'AI diagnose').props.onClick(); vi.runAllTimers(); @@ -4573,6 +4585,7 @@ describe('QueryEditor external SQL save', () => { }); it('shows Chinese semantic meaning for SQL execution errors', async () => { + storeState.appearance.uiVersion = 'v2'; backendApp.DBQueryMulti.mockResolvedValueOnce({ success: false, message: 'pq: syntax error at or near "from"', @@ -4592,6 +4605,7 @@ describe('QueryEditor external SQL save', () => { }); const pageText = textContent(renderer!.root); + expect(pageText).toContain('SQL 执行日志'); expect(pageText).toContain('执行失败'); expect(pageText).toContain('中文语义:SQL 语法错误'); expect(pageText).toContain('处理建议:'); @@ -7014,7 +7028,7 @@ describe('QueryEditor external SQL save', () => { it('keeps query result tabs compact, centered, and readable in v2 UI', () => { const source = readFileSync(new URL('./QueryEditorResultsPanel.tsx', import.meta.url), 'utf8'); - const css = readFileSync(new URL('../v2-theme.css', import.meta.url), 'utf8'); + const css = readV2ThemeCss(); expect(source).toContain('.query-result-tabs .ant-tabs-tab {'); expect(source).toContain('width: auto !important;'); @@ -7037,7 +7051,7 @@ describe('QueryEditor external SQL save', () => { const resultsPanelSource = readFileSync(new URL('./QueryEditorResultsPanel.tsx', import.meta.url), 'utf8'); const transactionSettingsSource = readFileSync(new URL('./QueryEditorTransactionSettings.tsx', import.meta.url), 'utf8'); const transactionToolbarSource = readFileSync(new URL('./QueryEditorTransactionToolbar.tsx', import.meta.url), 'utf8'); - const css = readFileSync(new URL('../v2-theme.css', import.meta.url), 'utf8'); + const css = readV2ThemeCss(); expect(source).toContain('QueryEditorToolbar'); expect(toolbarSource).toContain('gn-v2-query-toolbar-selects'); diff --git a/frontend/src/components/QueryEditor.results-and-drop.test.tsx b/frontend/src/components/QueryEditor.results-and-drop.test.tsx index fadc7aa3..9d77890f 100644 --- a/frontend/src/components/QueryEditor.results-and-drop.test.tsx +++ b/frontend/src/components/QueryEditor.results-and-drop.test.tsx @@ -5,7 +5,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { readV2ThemeCss } from '../test/readV2ThemeCss'; import { setCurrentLanguage } from '../i18n'; +import { catalogs } from '../i18n/catalog'; import type { SavedQuery, TabData } from '../types'; +import { formatSqlExecutionError } from '../utils/sqlErrorSemantics'; import { ORACLE_ROWID_LOCATOR_COLUMN } from '../utils/rowLocator'; import { clearQueryTabDraft, clearSQLFileTabDraft, getQueryTabDraft, getSQLFileTabDraft } from '../utils/sqlFileTabDrafts'; import QueryEditor, { @@ -356,6 +358,15 @@ vi.mock('./DataGrid', () => ({ GONAVI_ROW_KEY: '__gonavi_row_key__', })); +vi.mock('./LogPanel', () => ({ + default: ({ variant, executionError }: { variant?: string; executionError?: string }) => ( +
+ SQL 执行日志 + {executionError ? ` ${executionError}` : ''} +
+ ), +})); + vi.mock('@ant-design/icons', () => { const Icon = () => ; return { @@ -460,10 +471,12 @@ vi.mock('antd', () => { }; }); -const textContent = (node: any): string => - (node.children || []) - .map((item: any) => (typeof item === 'string' ? item : textContent(item))) - .join(''); +const textContent = (node: any): string => { + if (node === null || node === undefined) return ''; + if (typeof node === 'string' || typeof node === 'number') return String(node); + if (Array.isArray(node)) return node.map((item) => textContent(item)).join(''); + return textContent(node.children || []); +}; const findButton = (renderer: ReactTestRenderer, text: string) => renderer.root.findAll((node) => node.type === 'button' && textContent(node).includes(text))[0]; @@ -1303,9 +1316,13 @@ describe('QueryEditor external SQL save', () => { renderer = create(); }); + await act(async () => { + findButton(renderer!, 'Show results panel').props.onClick(); + }); + const rendered = textContent(renderer!.toJSON()); - expect(rendered).toContain('Awaiting SQL execution'); - expect(rendered).toContain('Run a query to display results below in the new data grid.'); + expect(rendered).toContain(catalogs['en-US']['query_editor.empty_state.title']); + expect(rendered).toContain(catalogs['en-US']['query_editor.empty_state.description']); expect(rendered).not.toContain('等待执行 SQL'); expect(rendered).not.toContain('运行查询后,结果会在下方以新版数据网格展示。'); }); @@ -1401,7 +1418,7 @@ describe('QueryEditor external SQL save', () => { editorState.position = { lineNumber: 3, column: 1 }; await act(async () => { - const runButton = findButton(renderer!, 'Run'); + const runButton = findButton(renderer!, '运行'); runButton.props.onMouseDown?.({ preventDefault: vi.fn() }); await runButton.props.onClick(); }); @@ -1441,7 +1458,7 @@ describe('QueryEditor external SQL save', () => { }; await act(async () => { - const runButton = findButton(renderer!, 'Run'); + const runButton = findButton(renderer!, '运行'); runButton.props.onMouseDown?.({ preventDefault: vi.fn() }); await runButton.props.onClick(); }); @@ -1483,7 +1500,7 @@ describe('QueryEditor external SQL save', () => { }; await act(async () => { - const runButton = findButton(renderer!, '运行'); + const runButton = findButton(renderer!, 'Run'); runButton.props.onMouseDown?.({ preventDefault: vi.fn() }); await runButton.props.onClick(); }); @@ -1492,7 +1509,7 @@ describe('QueryEditor external SQL save', () => { await Promise.resolve(); }); - expect(textContent(renderer!.toJSON())).toContain('结果 1'); + expect(textContent(renderer!.toJSON())).toContain('Result 1'); backendApp.DBQueryMulti.mockClear(); messageApi.info.mockClear(); @@ -1510,7 +1527,7 @@ describe('QueryEditor external SQL save', () => { }); await act(async () => { - const runButton = findButton(renderer!, '运行'); + const runButton = findButton(renderer!, 'Run'); runButton.props.onMouseDown?.({ preventDefault: vi.fn() }); await runButton.props.onClick(); }); @@ -1646,7 +1663,8 @@ describe('QueryEditor external SQL save', () => { }); const rendered = textContent(renderer.toJSON()); - expect(rendered).toContain('Statement 2 failed: driver exploded'); + expect(rendered).toContain('Statement 2 failed:'); + expect(rendered).toContain('Raw error: driver exploded'); expect(rendered).not.toContain('第 2 条语句执行失败:driver exploded'); }); @@ -1718,11 +1736,16 @@ describe('QueryEditor external SQL save', () => { expect(messageApi.success).not.toHaveBeenCalledWith('已执行完成,生成 2 个结果集。'); }); - it('shows the non-Mongo zero-result success toast in English', async () => { + it('renders the non-Mongo zero-row transactional result in English', async () => { storeState.languagePreference = 'en-US'; setCurrentLanguage('en-US'); const query = 'update users set active = 1 where 1 = 0;'; - backendApp.DBQueryMulti.mockResolvedValueOnce({ success: true, data: [] }); + backendApp.DBQueryMultiTransactional.mockResolvedValueOnce({ + success: true, + transactionId: 'tx-zero-rows', + transactionPending: true, + data: [{ columns: ['affectedRows'], rows: [{ affectedRows: 0 }], statementIndex: 1 }], + }); let renderer!: ReactTestRenderer; await act(async () => { @@ -1747,11 +1770,16 @@ describe('QueryEditor external SQL save', () => { await Promise.resolve(); }); + const rendered = textContent(renderer.toJSON()); expect(backendApp.DBQueryWithCancel).not.toHaveBeenCalled(); - expect(backendApp.DBQueryMulti).toHaveBeenCalledTimes(1); - expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).toContain('update users set active = 1 where 1 = 0'); - expect(messageApi.success).toHaveBeenCalledWith('Execution succeeded.'); - expect(messageApi.success).not.toHaveBeenCalledWith('执行成功。'); + expect(backendApp.DBQueryMulti).not.toHaveBeenCalled(); + expect(backendApp.DBQueryMultiTransactional).toHaveBeenCalledTimes(1); + expect(String(backendApp.DBQueryMultiTransactional.mock.calls[0][2])).toContain('update users set active = 1 where 1 = 0'); + expect(rendered).toContain(catalogs['en-US']['query_editor.result.execution_success']); + expect(rendered).toContain(catalogs['en-US']['query_editor.result.affected_rows'].replace('{{count}}', '0')); + expect(rendered).not.toContain('执行成功'); + expect(rendered).not.toContain('影响行数:0'); + expect(messageApi.success).not.toHaveBeenCalledWith('Execution succeeded.'); }); it('shows the wrapped execution failure toast in English while preserving raw error detail', async () => { @@ -1786,7 +1814,7 @@ describe('QueryEditor external SQL save', () => { expect(backendApp.DBQueryWithCancel).not.toHaveBeenCalled(); expect(backendApp.DBQueryMulti).toHaveBeenCalledTimes(1); expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).toContain('select 1'); - expect(messageApi.error).toHaveBeenCalledWith('Query execution failed: driver exploded'); + expect(messageApi.error).toHaveBeenCalledWith(`Query execution failed: ${formatSqlExecutionError('driver exploded')}`); expect(messageApi.error).not.toHaveBeenCalledWith('Error executing query: driver exploded'); }); }); @@ -1841,7 +1869,7 @@ describe('QueryEditor external SQL save', () => { await Promise.resolve(); }); - expect(messageApi.error).toHaveBeenCalledWith('Refresh failed: network down'); + expect(messageApi.error).toHaveBeenCalledWith(`Refresh failed: ${formatSqlExecutionError('network down')}`); expect(messageApi.error).not.toHaveBeenCalledWith('刷新失败: network down'); }); @@ -1867,7 +1895,7 @@ describe('QueryEditor external SQL save', () => { await Promise.resolve(); }); - expect(messageApi.error).toHaveBeenCalledWith('Refresh failed: socket closed'); + expect(messageApi.error).toHaveBeenCalledWith(`Refresh failed: ${formatSqlExecutionError('socket closed')}`); expect(messageApi.error).not.toHaveBeenCalledWith('刷新失败: socket closed'); }); }); @@ -2432,7 +2460,6 @@ describe('QueryEditor external SQL save', () => { const modalSource = readFileSync(new URL('./SnippetSettingsModal.tsx', import.meta.url), 'utf8'); const source = readFileSync(new URL('./QueryEditor.tsx', import.meta.url), 'utf8'); - expect(modalSource).toContain('片段语法说明(可编辑)'); expect(modalSource).toContain('data-sql-snippet-syntax-help-editor="true"'); expect(modalSource).toContain("defaultActiveKey={['snippet-help']}"); expect(modalSource).toContain('footer={null}'); @@ -2441,7 +2468,7 @@ describe('QueryEditor external SQL save', () => { expect(modalSource).toContain("size=\"large\""); expect(modalSource).toContain('minWidth: 96'); expect(modalSource).toContain('syntaxHelp'); - expect(modalSource).toContain('占位符语法参考'); + expect(modalSource).toContain("t('snippet_settings.syntax_reference.label')"); expect(source).toContain('s.syntaxHelp || s.description || s.body'); }); diff --git a/internal/app/methods_driver.go b/internal/app/methods_driver.go index d0504f37..d2539d1c 100644 --- a/internal/app/methods_driver.go +++ b/internal/app/methods_driver.go @@ -4846,1354 +4846,3 @@ func prepareDuckDBWindowsDynamicLibraryForBuild() (string, func(), error) { return workDir, cleanup, nil } - -func optionalDriverPublicTypeName(driverType string) string { - switch normalizeDriverType(driverType) { - case "diros": - return "doris" - default: - return normalizeDriverType(driverType) - } -} - -func optionalDriverExecutableBaseNameForType(typeName string) string { - base := strings.TrimSpace(typeName) - if base == "" { - base = "unknown" - } - name := fmt.Sprintf("%s-driver-agent", base) - if stdRuntime.GOOS == "windows" { - return name + ".exe" - } - return name -} - -func optionalDriverReleaseAssetNameForType(typeName string, goos string, goarch string) string { - base := strings.TrimSpace(typeName) - if base == "" { - base = "unknown" - } - name := fmt.Sprintf("%s-driver-agent-%s-%s", base, goos, goarch) - if strings.EqualFold(goos, "windows") { - return name + ".exe" - } - return name -} - -func optionalDriverNameStemCandidates(driverType string, selectedVersion string) []string { - candidates := make([]string, 0, 3) - seen := make(map[string]struct{}, 3) - appendStem := func(stem string) { - trimmed := strings.TrimSpace(stem) - if trimmed == "" { - return - } - if _, ok := seen[trimmed]; ok { - return - } - seen[trimmed] = struct{}{} - candidates = append(candidates, trimmed) - } - - base := fmt.Sprintf("%s-driver-agent", optionalDriverPublicTypeName(driverType)) - if normalizeDriverType(driverType) == "mongodb" { - switch resolveMongoDriverMajorFromVersion(selectedVersion) { - case 1: - appendStem(base + "-v1") - case 2: - appendStem(base + "-v2") - appendStem(base) - default: - appendStem(base) - } - return candidates - } - - appendStem(base) - return candidates -} - -func optionalDriverExecutableBaseNamesForVersion(driverType string, selectedVersion string) []string { - names := make([]string, 0, 2) - seen := make(map[string]struct{}, 2) - appendName := func(stem string) { - name := strings.TrimSpace(stem) - if strings.TrimSpace(name) == "" { - return - } - if stdRuntime.GOOS == "windows" { - name += ".exe" - } - if _, ok := seen[name]; ok { - return - } - seen[name] = struct{}{} - names = append(names, name) - } - - for _, stem := range optionalDriverNameStemCandidates(driverType, selectedVersion) { - appendName(stem) - } - return names -} - -func optionalDriverExecutableBaseNames(driverType string) []string { - return optionalDriverExecutableBaseNamesForVersion(driverType, "") -} - -func optionalDriverReleaseAssetNamesForVersion(driverType string, selectedVersion string) []string { - names := make([]string, 0, 2) - seen := make(map[string]struct{}, 2) - appendName := func(stem string) { - trimmedStem := strings.TrimSpace(stem) - if trimmedStem == "" { - return - } - name := fmt.Sprintf("%s-%s-%s", trimmedStem, stdRuntime.GOOS, stdRuntime.GOARCH) - if strings.EqualFold(stdRuntime.GOOS, "windows") { - name += ".exe" - } - if strings.TrimSpace(name) == "" { - return - } - if _, ok := seen[name]; ok { - return - } - seen[name] = struct{}{} - names = append(names, name) - } - - for _, stem := range optionalDriverNameStemCandidates(driverType, selectedVersion) { - appendName(stem) - } - return names -} - -func optionalDriverReleaseAssetNames(driverType string) []string { - return optionalDriverReleaseAssetNamesForVersion(driverType, "") -} - -func optionalDriverExecutableBaseName(driverType string) string { - names := optionalDriverExecutableBaseNames(driverType) - if len(names) == 0 { - return optionalDriverExecutableBaseNameForType("") - } - return names[0] -} - -func optionalDriverReleaseAssetName(driverType string) string { - names := optionalDriverReleaseAssetNames(driverType) - if len(names) == 0 { - return optionalDriverReleaseAssetNameForType("", stdRuntime.GOOS, stdRuntime.GOARCH) - } - return names[0] -} - -func optionalDriverReleaseAssetNameForVersion(driverType string, selectedVersion string) string { - names := optionalDriverReleaseAssetNamesForVersion(driverType, selectedVersion) - if len(names) == 0 { - return optionalDriverReleaseAssetNameForType("", stdRuntime.GOOS, stdRuntime.GOARCH) - } - return names[0] -} - -func currentDriverReleaseTag() string { - currentVersion := normalizeVersion(getCurrentVersion()) - if currentVersion == "" || currentVersion == "0.0.0" { - return "" - } - if isDevelopmentDriverReleaseVersion(currentVersion) { - return driverReleaseDevTag - } - return "v" + currentVersion -} - -func isDevelopmentDriverReleaseVersion(version string) bool { - normalized := strings.ToLower(strings.TrimSpace(normalizeVersion(version))) - if normalized == "" || normalized == "0.0.0" { - return false - } - if strings.HasPrefix(normalized, "dev-") { - return true - } - for _, marker := range []string{"-dev", "-test", "-local", "-snapshot"} { - if strings.Contains(normalized, marker) { - return true - } - } - return false -} - -func driverReleaseDownloadURL(tag string, assetName string) string { - tagName := strings.TrimSpace(tag) - asset := strings.TrimSpace(assetName) - if tagName == "" || asset == "" { - return "" - } - return fmt.Sprintf("https://github.com/%s/releases/download/%s/%s", driverReleaseRepo, url.PathEscape(tagName), url.PathEscape(asset)) -} - -func driverReleaseLatestDownloadURL(assetName string) string { - asset := strings.TrimSpace(assetName) - if asset == "" { - return "" - } - return fmt.Sprintf("https://github.com/%s/releases/latest/download/%s", driverReleaseRepo, url.PathEscape(asset)) -} - -func findReleaseAssetByName(release *githubRelease, assetNames []string) (githubAsset, bool) { - if release == nil || len(release.Assets) == 0 || len(assetNames) == 0 { - return githubAsset{}, false - } - for _, expected := range assetNames { - trimmed := strings.TrimSpace(expected) - if trimmed == "" { - continue - } - for _, asset := range release.Assets { - if strings.EqualFold(strings.TrimSpace(asset.Name), trimmed) { - return asset, true - } - } - } - return githubAsset{}, false -} - -func driverReleaseAssetAPIURL(asset githubAsset) string { - urlText := strings.TrimSpace(asset.URL) - if urlText != "" { - name := strings.TrimSpace(asset.Name) - if name == "" { - return urlText - } - parsed, err := url.Parse(urlText) - if err != nil { - return urlText - } - parsed.Fragment = name - return parsed.String() - } - urlText = strings.TrimSpace(asset.BrowserDownloadURL) - if urlText == "" { - return "" - } - return urlText -} - -func optionalDriverBundlePlatformDir(goos string) string { - switch strings.ToLower(strings.TrimSpace(goos)) { - case "windows": - return "Windows" - case "darwin": - return "MacOS" - case "linux": - return "Linux" - default: - return "Unknown" - } -} - -func optionalDriverBundleEntryPathsForVersion(driverType string, selectedVersion string) []string { - platformDir := optionalDriverBundlePlatformDir(stdRuntime.GOOS) - assetNames := optionalDriverReleaseAssetNamesForVersion(driverType, selectedVersion) - result := make([]string, 0, len(assetNames)) - seen := make(map[string]struct{}, len(assetNames)) - for _, assetName := range assetNames { - entry := filepath.ToSlash(filepath.Join(platformDir, assetName)) - if _, ok := seen[entry]; ok { - continue - } - seen[entry] = struct{}{} - result = append(result, entry) - } - return result -} - -func optionalDriverBundleEntryPaths(driverType string) []string { - return optionalDriverBundleEntryPathsForVersion(driverType, "") -} - -func optionalDriverBundleEntryPathForVersion(driverType string, selectedVersion string) string { - paths := optionalDriverBundleEntryPathsForVersion(driverType, selectedVersion) - if len(paths) == 0 { - return filepath.ToSlash(filepath.Join(optionalDriverBundlePlatformDir(stdRuntime.GOOS), optionalDriverReleaseAssetNameForVersion(driverType, selectedVersion))) - } - return paths[0] -} - -func optionalDriverBundleEntryPath(driverType string) string { - return optionalDriverBundleEntryPathForVersion(driverType, "") -} - -func resolveOptionalDriverAssetSize(sizeByAsset map[string]int64, driverType string) int64 { - if len(sizeByAsset) == 0 { - return 0 - } - for _, assetName := range optionalDriverReleaseAssetNames(driverType) { - sizeBytes := sizeByAsset[assetName] - if sizeBytes > 0 { - return sizeBytes - } - } - return 0 -} - -func resolveOptionalDriverAssetSizeForVersion(sizeByAsset map[string]int64, driverType string, version string) int64 { - if len(sizeByAsset) == 0 { - return 0 - } - for _, assetName := range optionalDriverReleaseAssetNamesForVersion(driverType, version) { - sizeBytes := sizeByAsset[assetName] - if sizeBytes > 0 { - return sizeBytes - } - } - return 0 -} - -func resolveOptionalDriverBundleDownloadURLs() []string { - candidates := make([]string, 0, 2) - seen := make(map[string]struct{}, 2) - appendURL := func(value string) { - trimmed := strings.TrimSpace(value) - if trimmed == "" { - return - } - if _, ok := seen[trimmed]; ok { - return - } - seen[trimmed] = struct{}{} - candidates = append(candidates, trimmed) - } - - if tag := currentDriverReleaseTag(); tag != "" { - if release, err := fetchReleaseByTag(tag); err == nil { - if asset, ok := findReleaseAssetByName(release, []string{optionalDriverBundleAssetName}); ok { - appendURL(driverReleaseAssetAPIURL(asset)) - } - } - appendURL(driverReleaseDownloadURL(tag, optionalDriverBundleAssetName)) - } - if release, err := fetchLatestReleaseForDriverAssets(); err == nil { - if asset, ok := findReleaseAssetByName(release, []string{optionalDriverBundleAssetName}); ok { - appendURL(driverReleaseAssetAPIURL(asset)) - } - } - appendURL(driverReleaseLatestDownloadURL(optionalDriverBundleAssetName)) - return candidates -} - -func optionalDriverBundleCacheDir() (string, error) { - cacheDir := filepath.Join(os.TempDir(), "gonavi-driver-bundle-cache") - if err := os.MkdirAll(cacheDir, 0o755); err != nil { - return "", err - } - return cacheDir, nil -} - -func optionalDriverBundleCachePath(bundleURL string) (string, error) { - cacheDir, err := optionalDriverBundleCacheDir() - if err != nil { - return "", err - } - sum := sha256.Sum256([]byte(strings.TrimSpace(bundleURL))) - return filepath.Join(cacheDir, hex.EncodeToString(sum[:])+".zip"), nil -} - -func cleanupOptionalDriverBundleCache(keepPaths ...string) { - cacheDir, err := optionalDriverBundleCacheDir() - if err != nil { - return - } - - keep := make(map[string]struct{}, len(keepPaths)+4) - for _, path := range keepPaths { - if strings.TrimSpace(path) != "" { - keep[filepath.Clean(path)] = struct{}{} - } - } - optionalDriverBundleDownloadMu.Lock() - for _, state := range optionalDriverBundleDownloads { - if state != nil && strings.TrimSpace(state.path) != "" { - keep[filepath.Clean(state.path)] = struct{}{} - } - } - optionalDriverBundleDownloadMu.Unlock() - - type cacheFile struct { - path string - modTime time.Time - } - cacheFiles := make([]cacheFile, 0) - now := time.Now() - entries, err := os.ReadDir(cacheDir) - if err != nil { - return - } - for _, entry := range entries { - if entry.IsDir() { - continue - } - path := filepath.Join(cacheDir, entry.Name()) - cleanPath := filepath.Clean(path) - if _, ok := keep[cleanPath]; ok { - continue - } - info, statErr := entry.Info() - if statErr != nil { - continue - } - name := strings.ToLower(strings.TrimSpace(entry.Name())) - if strings.HasSuffix(name, ".tmp") { - if now.Sub(info.ModTime()) > 24*time.Hour { - _ = os.Remove(path) - } - continue - } - if !strings.HasSuffix(name, ".zip") { - continue - } - if now.Sub(info.ModTime()) > optionalDriverBundleCacheMaxAge { - _ = os.Remove(path) - continue - } - cacheFiles = append(cacheFiles, cacheFile{path: path, modTime: info.ModTime()}) - } - if len(cacheFiles) <= optionalDriverBundleCacheMaxFiles { - return - } - sort.Slice(cacheFiles, func(i, j int) bool { - return cacheFiles[i].modTime.After(cacheFiles[j].modTime) - }) - for _, item := range cacheFiles[optionalDriverBundleCacheMaxFiles:] { - _ = os.Remove(item.path) - } -} - -func downloadOptionalDriverBundleToCache(bundleURL string, onProgress func(downloaded, total int64)) (string, error) { - cachePath, err := optionalDriverBundleCachePath(bundleURL) - if err != nil { - return "", err - } - tempPath := cachePath + fmt.Sprintf(".%d.tmp", time.Now().UnixNano()) - _ = os.Remove(tempPath) - if _, err := downloadFileWithHashWithTimeout(bundleURL, tempPath, onProgress, optionalDriverBundleDownloadTimeout); err != nil { - _ = os.Remove(tempPath) - return "", err - } - if err := os.Remove(cachePath); err != nil && !os.IsNotExist(err) { - _ = os.Remove(tempPath) - return "", err - } - if err := os.Rename(tempPath, cachePath); err != nil { - _ = os.Remove(tempPath) - return "", err - } - reader, err := zip.OpenReader(cachePath) - if err != nil { - _ = os.Remove(cachePath) - return "", fmt.Errorf("open driver bundle failed: %w", err) - } - if err := reader.Close(); err != nil { - _ = os.Remove(cachePath) - return "", fmt.Errorf("close driver bundle failed: %w", err) - } - cleanupOptionalDriverBundleCache(cachePath) - return cachePath, nil -} - -func acquireOptionalDriverBundlePath(bundleURL string, onProgress func(downloaded, total int64), onWaiting func()) (string, error) { - trimmedURL := strings.TrimSpace(bundleURL) - if trimmedURL == "" { - return "", newLocalizedDriverBackendError("driver_manager.backend.error.bundle_url_empty", nil, nil) - } - - for { - optionalDriverBundleDownloadMu.Lock() - state, ok := optionalDriverBundleDownloads[trimmedURL] - if ok { - if state.finished { - path := strings.TrimSpace(state.path) - err := state.err - if err == nil && path != "" && fileExists(path) { - optionalDriverBundleDownloadMu.Unlock() - return path, nil - } - delete(optionalDriverBundleDownloads, trimmedURL) - optionalDriverBundleDownloadMu.Unlock() - continue - } - done := state.done - optionalDriverBundleDownloadMu.Unlock() - if onWaiting != nil { - onWaiting() - } - <-done - optionalDriverBundleDownloadMu.Lock() - path := strings.TrimSpace(state.path) - err := state.err - if err == nil && path != "" && fileExists(path) { - optionalDriverBundleDownloadMu.Unlock() - return path, nil - } - if current, exists := optionalDriverBundleDownloads[trimmedURL]; exists && current == state { - delete(optionalDriverBundleDownloads, trimmedURL) - } - optionalDriverBundleDownloadMu.Unlock() - if err == nil { - err = fmt.Errorf("driver bundle cache file is unavailable") - } - return "", err - } - - state = &optionalDriverBundleDownloadState{done: make(chan struct{})} - optionalDriverBundleDownloads[trimmedURL] = state - optionalDriverBundleDownloadMu.Unlock() - - path, err := downloadOptionalDriverBundleToCache(trimmedURL, onProgress) - optionalDriverBundleDownloadMu.Lock() - state.path = path - state.err = err - state.finished = true - if err != nil { - delete(optionalDriverBundleDownloads, trimmedURL) - } - close(state.done) - optionalDriverBundleDownloadMu.Unlock() - - if err != nil { - return "", err - } - return path, nil - } -} - -func resolveOptionalDriverAgentDownloadURLs(definition driverDefinition, rawURL string, selectedVersion string) []string { - candidates := make([]string, 0, 3) - seen := make(map[string]struct{}, 3) - driverType := normalizeDriverType(definition.Type) - appendURL := func(value string) { - trimmed := strings.TrimSpace(value) - if trimmed == "" { - return - } - if _, ok := seen[trimmed]; ok { - return - } - seen[trimmed] = struct{}{} - candidates = append(candidates, trimmed) - } - - restrictToExplicitArtifact := shouldRestrictToExplicitVersionArtifact(definition, selectedVersion) - appendPublishedURLs := func() { - if tag := currentDriverReleaseTag(); tag != "" { - if publishedURL, ok := resolvePublishedDriverDownloadURLForTag(definition, selectedVersion, tag); ok { - appendURL(publishedURL) - } - } - if publishedURL, ok := resolveLatestPublishedDriverDownloadURLForVersion(definition, selectedVersion); ok { - appendURL(publishedURL) - } - } - - if !restrictToExplicitArtifact && shouldPreferPublishedOptionalDriverDownloads(driverType) { - appendPublishedURLs() - } - - if parsed, err := url.Parse(strings.TrimSpace(rawURL)); err == nil { - switch strings.ToLower(strings.TrimSpace(parsed.Scheme)) { - case "http", "https": - appendURL(parsed.String()) - } - } - if restrictToExplicitArtifact { - return candidates - } - - if !shouldPreferPublishedOptionalDriverDownloads(driverType) { - appendPublishedURLs() - } - return candidates -} - -func findExistingOptionalDriverAgentCandidate(definition driverDefinition, targetPath string) (string, bool) { - driverType := normalizeDriverType(definition.Type) - targetAbs, _ := filepath.Abs(targetPath) - candidates := resolveOptionalDriverAgentCandidatePaths(definition) - for _, candidate := range candidates { - candidate = strings.TrimSpace(candidate) - if candidate == "" { - continue - } - absPath, err := filepath.Abs(candidate) - if err != nil || absPath == "" { - continue - } - if targetAbs != "" && absPath == targetAbs { - continue - } - info, statErr := os.Stat(absPath) - if statErr != nil || info.IsDir() { - continue - } - if validateErr := validateOptionalDriverAgentExecutableFunc(driverType, absPath); validateErr != nil { - continue - } - if !isReusableOptionalDriverAgentCandidateRevisionAcceptable(driverType, absPath) { - continue - } - return absPath, true - } - return "", false -} - -func isReusableOptionalDriverAgentCandidateRevisionAcceptable(driverType string, executablePath string) bool { - expected := strings.TrimSpace(db.OptionalDriverAgentRevision(driverType)) - if expected == "" { - return true - } - actual, current, err := optionalDriverAgentRevisionCurrent(driverType, executablePath) - displayName := resolveDriverDisplayName(driverDefinition{Type: driverType}) - if err != nil { - logger.Warnf("可复用 %s 驱动代理候选版本元数据不可用,仍允许安装:path=%s err=%v;建议在驱动管理中重装", displayName, executablePath, err) - return true - } - if !current { - actualLabel := strings.TrimSpace(actual) - if actualLabel == "" { - actualLabel = "空" - } - logger.Warnf("可复用 %s 驱动代理候选 revision 不匹配,仍允许安装:path=%s actual=%s expected=%s;建议在驱动管理中重装", displayName, executablePath, actualLabel, expected) - return true - } - return true -} - -func resolveOptionalDriverAgentCandidatePaths(definition driverDefinition) []string { - driverType := normalizeDriverType(definition.Type) - names := optionalDriverExecutableBaseNames(driverType) - assetNames := optionalDriverReleaseAssetNames(driverType) - pathTypeNames := make([]string, 0, 2) - seenPathType := make(map[string]struct{}, 2) - appendPathType := func(typeName string) { - trimmed := strings.TrimSpace(typeName) - if trimmed == "" { - return - } - if _, ok := seenPathType[trimmed]; ok { - return - } - seenPathType[trimmed] = struct{}{} - pathTypeNames = append(pathTypeNames, trimmed) - } - appendPathType(optionalDriverPublicTypeName(driverType)) - - candidates := make([]string, 0, 12) - appendPath := func(pathText string) { - trimmed := strings.TrimSpace(pathText) - if trimmed != "" { - candidates = append(candidates, trimmed) - } - } - - if exePath, err := os.Executable(); err == nil && strings.TrimSpace(exePath) != "" { - resolved := exePath - if evalPath, evalErr := filepath.EvalSymlinks(exePath); evalErr == nil && strings.TrimSpace(evalPath) != "" { - resolved = evalPath - } - exeDir := filepath.Dir(resolved) - for _, name := range names { - appendPath(filepath.Join(exeDir, name)) - } - for _, assetName := range assetNames { - appendPath(filepath.Join(exeDir, assetName)) - } - for _, typeName := range pathTypeNames { - for _, name := range names { - appendPath(filepath.Join(exeDir, "drivers", typeName, name)) - } - for _, assetName := range assetNames { - appendPath(filepath.Join(exeDir, "drivers", typeName, assetName)) - } - } - - resourcesDir := filepath.Clean(filepath.Join(exeDir, "..", "Resources")) - for _, typeName := range pathTypeNames { - for _, name := range names { - appendPath(filepath.Join(resourcesDir, "drivers", typeName, name)) - } - for _, assetName := range assetNames { - appendPath(filepath.Join(resourcesDir, "drivers", typeName, assetName)) - } - } - } - if wd, err := os.Getwd(); err == nil && strings.TrimSpace(wd) != "" { - for _, assetName := range assetNames { - appendPath(filepath.Join(wd, "dist", assetName)) - appendPath(filepath.Join(wd, assetName)) - } - } - - unique := make([]string, 0, len(candidates)) - seen := make(map[string]struct{}, len(candidates)) - for _, item := range candidates { - if _, ok := seen[item]; ok { - continue - } - seen[item] = struct{}{} - unique = append(unique, item) - } - return unique -} - -func resolveDriverDisplayName(definition driverDefinition) string { - if strings.TrimSpace(definition.Name) != "" { - return strings.TrimSpace(definition.Name) - } - if strings.TrimSpace(definition.Type) != "" { - return strings.TrimSpace(definition.Type) - } - return defaultAppText("driver_manager.backend.driver_fallback_name", nil) -} - -func activateOptionalDriverAgentBinary(driverType string, installPath string, runtimePath string) error { - source := strings.TrimSpace(installPath) - target := strings.TrimSpace(runtimePath) - if source == "" || target == "" { - return fmt.Errorf("agent path is empty") - } - if source == target { - return nil - } - - absSource := source - absTarget := target - if value, err := filepath.Abs(source); err == nil && strings.TrimSpace(value) != "" { - absSource = value - } - if value, err := filepath.Abs(target); err == nil && strings.TrimSpace(value) != "" { - absTarget = value - } - if strings.EqualFold(absSource, absTarget) { - return nil - } - if err := copyAgentBinary(source, target); err != nil { - return err - } - return copyOptionalDriverSupportFilesFromDirectory(driverType, filepath.Dir(source), filepath.Dir(target)) -} - -func copyAgentBinary(sourcePath, targetPath string) error { - src, err := os.Open(sourcePath) - if err != nil { - return err - } - defer src.Close() - - tempPath := targetPath + ".tmp" - _ = os.Remove(tempPath) - dst, err := os.Create(tempPath) - if err != nil { - return err - } - if _, err := io.Copy(dst, src); err != nil { - dst.Close() - _ = os.Remove(tempPath) - return err - } - if err := dst.Sync(); err != nil { - dst.Close() - _ = os.Remove(tempPath) - return err - } - if err := dst.Close(); err != nil { - _ = os.Remove(tempPath) - return err - } - if chmodErr := os.Chmod(tempPath, 0o755); chmodErr != nil && stdRuntime.GOOS != "windows" { - _ = os.Remove(tempPath) - return chmodErr - } - if err := renameTempFileOverTarget(tempPath, targetPath); err != nil { - _ = os.Remove(tempPath) - return err - } - if chmodErr := os.Chmod(targetPath, 0o755); chmodErr != nil && stdRuntime.GOOS != "windows" { - return chmodErr - } - return nil -} - -func extractZipFileToPath(file *zip.File, targetPath string) error { - if file == nil { - return newLocalizedDriverBackendError("driver_manager.backend.error.zip_entry_empty", nil, nil) - } - src, err := file.Open() - if err != nil { - return err - } - defer src.Close() - tempPath := targetPath + ".tmp" - _ = os.Remove(tempPath) - if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil { - return err - } - dst, err := os.Create(tempPath) - if err != nil { - return err - } - if _, err := io.Copy(dst, src); err != nil { - dst.Close() - _ = os.Remove(tempPath) - return err - } - if err := dst.Sync(); err != nil { - dst.Close() - _ = os.Remove(tempPath) - return err - } - if err := dst.Close(); err != nil { - _ = os.Remove(tempPath) - return err - } - if err := renameTempFileOverTarget(tempPath, targetPath); err != nil { - _ = os.Remove(tempPath) - return err - } - return nil -} - -func copyOptionalDriverSupportFile(sourcePath, targetPath string) error { - src, err := os.Open(sourcePath) - if err != nil { - return err - } - defer src.Close() - - tempPath := targetPath + ".tmp" - _ = os.Remove(tempPath) - if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil { - return err - } - dst, err := os.Create(tempPath) - if err != nil { - return err - } - if _, err := io.Copy(dst, src); err != nil { - dst.Close() - _ = os.Remove(tempPath) - return err - } - if err := dst.Sync(); err != nil { - dst.Close() - _ = os.Remove(tempPath) - return err - } - if err := dst.Close(); err != nil { - _ = os.Remove(tempPath) - return err - } - if err := renameTempFileOverTarget(tempPath, targetPath); err != nil { - _ = os.Remove(tempPath) - return err - } - return nil -} - -func renameTempFileOverTarget(tempPath, targetPath string) error { - if err := os.Rename(tempPath, targetPath); err == nil { - return nil - } else { - firstErr := err - if removeErr := os.Remove(targetPath); removeErr != nil && !os.IsNotExist(removeErr) { - return firstErr - } - if retryErr := os.Rename(tempPath, targetPath); retryErr != nil { - return retryErr - } - return nil - } -} - -func copyOptionalDriverSupportFilesFromDirectory(driverType string, sourceDir string, targetDir string) error { - names := optionalDriverSupportFileNames(driverType) - if len(names) == 0 { - return nil - } - sourceRoot := strings.TrimSpace(sourceDir) - targetRoot := strings.TrimSpace(targetDir) - if sourceRoot == "" || targetRoot == "" { - return newLocalizedDriverBackendError("driver_manager.backend.error.runtime_dependency_directory_empty", nil, nil) - } - for _, name := range names { - sourcePath := filepath.Join(sourceRoot, name) - targetPath := filepath.Join(targetRoot, name) - if err := copyOptionalDriverSupportFile(sourcePath, targetPath); err != nil { - return newLocalizedDriverBackendError("driver_manager.backend.error.copy_runtime_dependency_entry_failed", map[string]any{"name": name}, err) - } - } - return nil -} - -func findOptionalDriverSupportFileInZip(files []*zip.File, agentEntryName string, supportName string) *zip.File { - normalizedAgent := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(agentEntryName), "./")) - agentDir := filepath.ToSlash(filepath.Dir(normalizedAgent)) - if agentDir == "." { - agentDir = "" - } - candidatePaths := []string{} - if agentDir != "" { - candidatePaths = append(candidatePaths, filepath.ToSlash(filepath.Join(agentDir, supportName))) - } - candidatePaths = append(candidatePaths, supportName) - - for _, candidate := range candidatePaths { - for _, file := range files { - name := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(file.Name), "./")) - if name == candidate { - return file - } - } - for _, file := range files { - name := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(file.Name), "./")) - if strings.EqualFold(name, candidate) { - return file - } - } - } - for _, file := range files { - name := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(file.Name), "./")) - if strings.EqualFold(filepath.Base(name), supportName) { - return file - } - } - return nil -} - -func extractOptionalDriverSupportFilesFromZip(files []*zip.File, driverType string, agentEntryName string, targetDir string) error { - names := optionalDriverSupportFileNames(driverType) - if len(names) == 0 { - return nil - } - targetRoot := strings.TrimSpace(targetDir) - if targetRoot == "" { - return newLocalizedDriverBackendError("driver_manager.backend.error.runtime_dependency_target_directory_empty", nil, nil) - } - for _, name := range names { - entry := findOptionalDriverSupportFileInZip(files, agentEntryName, name) - if entry == nil { - return newLocalizedDriverBackendError("driver_manager.backend.error.runtime_dependency_entry_missing", map[string]any{"name": name}, nil) - } - if err := extractZipFileToPath(entry, filepath.Join(targetRoot, name)); err != nil { - return newLocalizedDriverBackendError("driver_manager.backend.error.extract_runtime_dependency_failed", map[string]any{"name": name}, err) - } - } - return nil -} - -func scaleProgress(downloaded, total, start, end int64) (int64, int64) { - if end <= start { - return end, 100 - } - if total <= 0 { - return start, 100 - } - if downloaded < 0 { - downloaded = 0 - } - if downloaded > total { - downloaded = total - } - span := end - start - return start + ((downloaded * span) / total), 100 -} - -func preloadOptionalDriverPackageSizes(definitions []driverDefinition) map[string]int64 { - result := make(map[string]int64) - if len(definitions) == 0 { - return result - } - - needed := make([]string, 0, len(definitions)) - for _, definition := range definitions { - normalizedType := normalizeDriverType(definition.Type) - if normalizedType == "" || definition.BuiltIn { - continue - } - if !db.IsOptionalGoDriver(normalizedType) { - continue - } - if !db.IsOptionalGoDriverBuildIncluded(normalizedType) { - continue - } - needed = append(needed, normalizedType) - } - if len(needed) == 0 { - return result - } - - tag := currentDriverReleaseTag() - - fillFromSizes := func(sizeByAsset map[string]int64, driverTypes []string) []string { - missing := make([]string, 0, len(driverTypes)) - for _, driverType := range driverTypes { - sizeBytes := resolveOptionalDriverAssetSize(sizeByAsset, driverType) - if sizeBytes > 0 { - result[driverType] = sizeBytes - continue - } - missing = append(missing, driverType) - } - return missing - } - - pending := needed - if tag != "" { - if sizeByAsset, _, err := loadReleaseAssetSizesCached("tag:"+tag, func() (*githubRelease, error) { - return fetchReleaseByTag(tag) - }); err == nil { - pending = fillFromSizes(sizeByAsset, pending) - } - } - if len(pending) == 0 { - return result - } - if sizeByAsset, _, err := loadReleaseAssetSizesCached("latest", fetchLatestReleaseForDriverAssets); err == nil { - _ = fillFromSizes(sizeByAsset, pending) - } - return result -} - -func loadReleaseAssetSizesCached(cacheKey string, fetch func() (*githubRelease, error)) (map[string]int64, map[string]bool, error) { - key := strings.TrimSpace(cacheKey) - if key == "" { - return nil, nil, newLocalizedDriverBackendError("driver_manager.backend.error.cache_key_empty", nil, nil) - } - - driverReleaseSizeMu.RLock() - cached, ok := driverReleaseSizeMap[key] - driverReleaseSizeMu.RUnlock() - if ok { - ttl := driverReleaseAssetSizeCacheTTL - if strings.TrimSpace(cached.Err) != "" { - ttl = driverReleaseAssetSizeErrorCacheTTL - } - if time.Since(cached.LoadedAt) < ttl { - if strings.TrimSpace(cached.Err) != "" { - return nil, nil, errors.New(strings.TrimSpace(cached.Err)) - } - return cached.SizeByKey, cached.PublishedAssets, nil - } - } - - release, err := fetch() - entry := driverReleaseAssetSizeCacheEntry{ - LoadedAt: time.Now(), - SizeByKey: map[string]int64{}, - PublishedAssets: map[string]bool{}, - } - if err != nil { - entry.Err = err.Error() - } else { - entry.SizeByKey = buildReleaseAssetSizeMap(release) - entry.PublishedAssets = buildReleaseAssetNameMap(release) - if indexSizes, indexErr := fetchDriverBundleAssetSizeIndex(release); indexErr == nil { - for name, size := range indexSizes { - trimmedName := strings.TrimSpace(name) - if trimmedName == "" || size <= 0 { - continue - } - entry.SizeByKey[trimmedName] = size - } - } - } - - driverReleaseSizeMu.Lock() - driverReleaseSizeMap[key] = entry - driverReleaseSizeMu.Unlock() - - if err != nil { - return nil, nil, err - } - return entry.SizeByKey, entry.PublishedAssets, nil -} - -func readReleaseAssetSizesFromCache(cacheKey string) (map[string]int64, map[string]bool, bool) { - key := strings.TrimSpace(cacheKey) - if key == "" { - return nil, nil, false - } - - driverReleaseSizeMu.RLock() - cached, ok := driverReleaseSizeMap[key] - driverReleaseSizeMu.RUnlock() - if !ok { - return nil, nil, false - } - - ttl := driverReleaseAssetSizeCacheTTL - if strings.TrimSpace(cached.Err) != "" { - ttl = driverReleaseAssetSizeErrorCacheTTL - } - if time.Since(cached.LoadedAt) >= ttl { - return nil, nil, false - } - if strings.TrimSpace(cached.Err) != "" { - return nil, nil, false - } - return cached.SizeByKey, cached.PublishedAssets, true -} - -func buildReleaseAssetSizeMap(release *githubRelease) map[string]int64 { - sizes := make(map[string]int64) - if release == nil { - return sizes - } - for _, asset := range release.Assets { - name := strings.TrimSpace(asset.Name) - if name == "" || asset.Size <= 0 { - continue - } - sizes[name] = asset.Size - } - return sizes -} - -func buildReleaseAssetNameMap(release *githubRelease) map[string]bool { - names := make(map[string]bool) - if release == nil { - return names - } - for _, asset := range release.Assets { - name := strings.TrimSpace(asset.Name) - if name == "" { - continue - } - names[name] = true - } - return names -} - -func fetchDriverBundleAssetSizeIndex(release *githubRelease) (map[string]int64, error) { - if release == nil { - return nil, newLocalizedDriverBackendError("driver_manager.backend.error.release_empty", nil, nil) - } - indexURL := "" - for _, asset := range release.Assets { - if strings.EqualFold(strings.TrimSpace(asset.Name), optionalDriverBundleIndexAssetName) { - indexURL = strings.TrimSpace(asset.BrowserDownloadURL) - break - } - } - if indexURL == "" { - return nil, newLocalizedDriverBackendError("driver_manager.backend.error.bundle_index_asset_missing", nil, nil) - } - - client := newHTTPClientWithGlobalProxy(driverReleaseAssetSizeProbeTimeout) - req, err := http.NewRequest(http.MethodGet, indexURL, nil) - if err != nil { - return nil, err - } - req.Header.Set("User-Agent", "GoNavi-DriverManager") - req.Header.Set("Accept", "application/json") - - resp, err := client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, newLocalizedDriverBackendError( - "driver_manager.backend.error.bundle_index_fetch_failed", - nil, - fmt.Errorf("HTTP %d", resp.StatusCode), - ) - } - - limited := io.LimitReader(resp.Body, driverBundleIndexMaxSize) - decoder := json.NewDecoder(limited) - var index driverBundleAssetIndex - if err := decoder.Decode(&index); err != nil { - return nil, newLocalizedDriverBackendError("driver_manager.backend.error.bundle_index_parse_failed", nil, err) - } - if len(index.Assets) == 0 { - return nil, newLocalizedDriverBackendError("driver_manager.backend.error.bundle_index_empty", nil, nil) - } - return index.Assets, nil -} - -func fetchLatestReleaseForDriverAssets() (*githubRelease, error) { - return fetchDriverReleaseByURL(driverReleaseLatestAPIURL) -} - -func resolveLatestPublishedDriverDownloadURL(definition driverDefinition) (string, bool) { - return resolveLatestPublishedDriverDownloadURLForVersion(definition, "") -} - -func resolveLatestPublishedDriverDownloadURLForVersion(definition driverDefinition, selectedVersion string) (string, bool) { - driverType := normalizeDriverType(definition.Type) - if driverType == "" { - return "", false - } - if shouldUseDuckDBWindowsDynamicLibrary(driverType) { - if sizeByAsset, publishedAssets, ok := readReleaseAssetSizesFromCache("latest"); ok { - if publishedAssets[duckDBWindowsDriverZipAssetName] && sizeByAsset[duckDBWindowsDriverZipAssetName] > 0 { - if release, err := fetchLatestReleaseForDriverAssets(); err == nil { - if asset, found := findReleaseAssetByName(release, []string{duckDBWindowsDriverZipAssetName}); found { - return driverReleaseAssetAPIURL(asset), true - } - } - return driverReleaseLatestDownloadURL(duckDBWindowsDriverZipAssetName), true - } - return "", false - } - - sizeByAsset, publishedAssets, err := loadReleaseAssetSizesCached("latest", fetchLatestReleaseForDriverAssets) - if err != nil { - return "", false - } - if publishedAssets[duckDBWindowsDriverZipAssetName] && sizeByAsset[duckDBWindowsDriverZipAssetName] > 0 { - if release, relErr := fetchLatestReleaseForDriverAssets(); relErr == nil { - if asset, found := findReleaseAssetByName(release, []string{duckDBWindowsDriverZipAssetName}); found { - return driverReleaseAssetAPIURL(asset), true - } - } - return driverReleaseLatestDownloadURL(duckDBWindowsDriverZipAssetName), true - } - return "", false - } - - assetNames := optionalDriverReleaseAssetNamesForVersion(driverType, selectedVersion) - if len(assetNames) == 0 { - return "", false - } - - if sizeByAsset, publishedAssets, ok := readReleaseAssetSizesFromCache("latest"); ok { - for _, assetName := range assetNames { - if publishedAssets[assetName] && sizeByAsset[assetName] > 0 { - if release, err := fetchLatestReleaseForDriverAssets(); err == nil { - if asset, found := findReleaseAssetByName(release, []string{assetName}); found { - return driverReleaseAssetAPIURL(asset), true - } - } - return driverReleaseLatestDownloadURL(assetName), true - } - } - return "", false - } - - sizeByAsset, publishedAssets, err := loadReleaseAssetSizesCached("latest", fetchLatestReleaseForDriverAssets) - if err != nil { - return "", false - } - for _, assetName := range assetNames { - if publishedAssets[assetName] && sizeByAsset[assetName] > 0 { - if release, relErr := fetchLatestReleaseForDriverAssets(); relErr == nil { - if asset, found := findReleaseAssetByName(release, []string{assetName}); found { - return driverReleaseAssetAPIURL(asset), true - } - } - return driverReleaseLatestDownloadURL(assetName), true - } - } - return "", false -} - -func fetchReleaseByTag(tag string) (*githubRelease, error) { - tagName := strings.TrimSpace(tag) - if tagName == "" { - return nil, newLocalizedDriverBackendError("driver_manager.backend.error.tag_empty", nil, nil) - } - apiURL := fmt.Sprintf("https://api.github.com/repos/%s/releases/tags/%s", driverReleaseRepo, url.PathEscape(tagName)) - return fetchDriverReleaseByURL(apiURL) -} - -func fetchDriverReleaseByURL(apiURL string) (*githubRelease, error) { - urlText := strings.TrimSpace(apiURL) - if urlText == "" { - return nil, newLocalizedDriverBackendError("driver_manager.backend.error.api_url_empty", nil, nil) - } - - client := newHTTPClientWithGlobalProxy(driverReleaseAssetSizeProbeTimeout) - req, err := http.NewRequest(http.MethodGet, urlText, nil) - if err != nil { - return nil, err - } - req.Header.Set("User-Agent", "GoNavi-DriverManager") - req.Header.Set("Accept", "application/vnd.github+json") - - resp, err := client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, newLocalizedDriverBackendError( - "driver_manager.backend.error.release_info_fetch_failed", - nil, - fmt.Errorf("HTTP %d", resp.StatusCode), - ) - } - - var release githubRelease - if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { - return nil, err - } - return &release, nil -} - -func resolveDriverPackageSizeText(definition driverDefinition, pkg installedDriverPackage, packageMetaExists bool, packageSizeBytesMap map[string]int64, text func(string, map[string]any) string) string { - if definition.BuiltIn { - return driverManagerLocalizedText(text, "driver_manager.package_size.built_in", nil, "Built-in") - } - - normalizedType := normalizeDriverType(definition.Type) - if packageMetaExists { - sizeBytes := readInstalledPackageSizeBytes(pkg) - if sizeBytes > 0 { - return formatSizeMB(sizeBytes) - } - } - if sizeBytes, ok := packageSizeBytesMap[normalizedType]; ok && sizeBytes > 0 { - return formatSizeMB(sizeBytes) - } - - if !db.IsOptionalGoDriverBuildIncluded(normalizedType) { - return driverManagerLocalizedText(text, "driver_manager.package_size.pending_release", nil, "Pending release") - } - return "-" -} - -func driverManagerLocalizedText(text func(string, map[string]any) string, key string, params map[string]any, fallback string) string { - if text == nil { - return fallback - } - localized := text(key, params) - if localized == "" { - return fallback - } - return localized -} - -func readInstalledPackageSizeBytes(pkg installedDriverPackage) int64 { - pathText := strings.TrimSpace(pkg.ExecutablePath) - if pathText == "" { - pathText = strings.TrimSpace(pkg.FilePath) - } - if pathText == "" { - return 0 - } - info, err := os.Stat(pathText) - if err != nil || info.IsDir() { - return 0 - } - return info.Size() -} - -func formatSizeMB(sizeBytes int64) string { - if sizeBytes <= 0 { - return "-" - } - sizeMB := float64(sizeBytes) / (1024 * 1024) - return fmt.Sprintf("%.2f MB", sizeMB) -} diff --git a/internal/app/methods_driver_assets.go b/internal/app/methods_driver_assets.go index 37a9ff10..9627e46b 100644 --- a/internal/app/methods_driver_assets.go +++ b/internal/app/methods_driver_assets.go @@ -466,11 +466,11 @@ func downloadOptionalDriverBundleToCache(bundleURL string, onProgress func(downl reader, err := zip.OpenReader(cachePath) if err != nil { _ = os.Remove(cachePath) - return "", fmt.Errorf("打开驱动总包失败:%w", err) + return "", fmt.Errorf("open driver bundle failed: %w", err) } if err := reader.Close(); err != nil { _ = os.Remove(cachePath) - return "", fmt.Errorf("关闭驱动总包失败:%w", err) + return "", fmt.Errorf("close driver bundle failed: %w", err) } cleanupOptionalDriverBundleCache(cachePath) return cachePath, nil @@ -479,7 +479,7 @@ func downloadOptionalDriverBundleToCache(bundleURL string, onProgress func(downl func acquireOptionalDriverBundlePath(bundleURL string, onProgress func(downloaded, total int64), onWaiting func()) (string, error) { trimmedURL := strings.TrimSpace(bundleURL) if trimmedURL == "" { - return "", fmt.Errorf("驱动总包下载地址为空") + return "", newLocalizedDriverBackendError("driver_manager.backend.error.bundle_url_empty", nil, nil) } for { @@ -515,7 +515,7 @@ func acquireOptionalDriverBundlePath(bundleURL string, onProgress func(downloade } optionalDriverBundleDownloadMu.Unlock() if err == nil { - err = fmt.Errorf("驱动总包缓存文件不可用") + err = fmt.Errorf("driver bundle cache file is unavailable") } return "", err } @@ -727,7 +727,7 @@ func resolveDriverDisplayName(definition driverDefinition) string { if strings.TrimSpace(definition.Type) != "" { return strings.TrimSpace(definition.Type) } - return "未知" + return defaultAppText("driver_manager.backend.driver_fallback_name", nil) } func activateOptionalDriverAgentBinary(driverType string, installPath string, runtimePath string) error { @@ -800,7 +800,7 @@ func copyAgentBinary(sourcePath, targetPath string) error { func extractZipFileToPath(file *zip.File, targetPath string) error { if file == nil { - return fmt.Errorf("zip 条目为空") + return newLocalizedDriverBackendError("driver_manager.backend.error.zip_entry_empty", nil, nil) } src, err := file.Open() if err != nil { @@ -897,13 +897,13 @@ func copyOptionalDriverSupportFilesFromDirectory(driverType string, sourceDir st sourceRoot := strings.TrimSpace(sourceDir) targetRoot := strings.TrimSpace(targetDir) if sourceRoot == "" || targetRoot == "" { - return fmt.Errorf("运行时依赖目录为空") + return newLocalizedDriverBackendError("driver_manager.backend.error.runtime_dependency_directory_empty", nil, nil) } for _, name := range names { sourcePath := filepath.Join(sourceRoot, name) targetPath := filepath.Join(targetRoot, name) if err := copyOptionalDriverSupportFile(sourcePath, targetPath); err != nil { - return fmt.Errorf("复制 %s 失败:%w", name, err) + return newLocalizedDriverBackendError("driver_manager.backend.error.copy_runtime_dependency_entry_failed", map[string]any{"name": name}, err) } } return nil @@ -951,15 +951,15 @@ func extractOptionalDriverSupportFilesFromZip(files []*zip.File, driverType stri } targetRoot := strings.TrimSpace(targetDir) if targetRoot == "" { - return fmt.Errorf("运行时依赖目标目录为空") + return newLocalizedDriverBackendError("driver_manager.backend.error.runtime_dependency_target_directory_empty", nil, nil) } for _, name := range names { entry := findOptionalDriverSupportFileInZip(files, agentEntryName, name) if entry == nil { - return fmt.Errorf("驱动包缺少运行时依赖:%s", name) + return newLocalizedDriverBackendError("driver_manager.backend.error.runtime_dependency_entry_missing", map[string]any{"name": name}, nil) } if err := extractZipFileToPath(entry, filepath.Join(targetRoot, name)); err != nil { - return fmt.Errorf("解压运行时依赖 %s 失败:%w", name, err) + return newLocalizedDriverBackendError("driver_manager.backend.error.extract_runtime_dependency_failed", map[string]any{"name": name}, err) } } return nil @@ -1041,7 +1041,7 @@ func preloadOptionalDriverPackageSizes(definitions []driverDefinition) map[strin func loadReleaseAssetSizesCached(cacheKey string, fetch func() (*githubRelease, error)) (map[string]int64, map[string]bool, error) { key := strings.TrimSpace(cacheKey) if key == "" { - return nil, nil, fmt.Errorf("缓存 key 为空") + return nil, nil, newLocalizedDriverBackendError("driver_manager.backend.error.cache_key_empty", nil, nil) } driverReleaseSizeMu.RLock() @@ -1150,7 +1150,7 @@ func buildReleaseAssetNameMap(release *githubRelease) map[string]bool { func fetchDriverBundleAssetSizeIndex(release *githubRelease) (map[string]int64, error) { if release == nil { - return nil, fmt.Errorf("release 为空") + return nil, newLocalizedDriverBackendError("driver_manager.backend.error.release_empty", nil, nil) } indexURL := "" for _, asset := range release.Assets { @@ -1160,7 +1160,7 @@ func fetchDriverBundleAssetSizeIndex(release *githubRelease) (map[string]int64, } } if indexURL == "" { - return nil, fmt.Errorf("未找到驱动总包索引资产") + return nil, newLocalizedDriverBackendError("driver_manager.backend.error.bundle_index_asset_missing", nil, nil) } client := newHTTPClientWithGlobalProxy(driverReleaseAssetSizeProbeTimeout) @@ -1177,17 +1177,21 @@ func fetchDriverBundleAssetSizeIndex(release *githubRelease) (map[string]int64, } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("拉取驱动总包索引失败:HTTP %d", resp.StatusCode) + return nil, newLocalizedDriverBackendError( + "driver_manager.backend.error.bundle_index_fetch_failed", + nil, + fmt.Errorf("HTTP %d", resp.StatusCode), + ) } limited := io.LimitReader(resp.Body, driverBundleIndexMaxSize) decoder := json.NewDecoder(limited) var index driverBundleAssetIndex if err := decoder.Decode(&index); err != nil { - return nil, fmt.Errorf("解析驱动总包索引失败:%w", err) + return nil, newLocalizedDriverBackendError("driver_manager.backend.error.bundle_index_parse_failed", nil, err) } if len(index.Assets) == 0 { - return nil, fmt.Errorf("驱动总包索引为空") + return nil, newLocalizedDriverBackendError("driver_manager.backend.error.bundle_index_empty", nil, nil) } return index.Assets, nil } @@ -1272,7 +1276,7 @@ func resolveLatestPublishedDriverDownloadURLForVersion(definition driverDefiniti func fetchReleaseByTag(tag string) (*githubRelease, error) { tagName := strings.TrimSpace(tag) if tagName == "" { - return nil, fmt.Errorf("Tag 为空") + return nil, newLocalizedDriverBackendError("driver_manager.backend.error.tag_empty", nil, nil) } apiURL := fmt.Sprintf("https://api.github.com/repos/%s/releases/tags/%s", driverReleaseRepo, url.PathEscape(tagName)) return fetchDriverReleaseByURL(apiURL) @@ -1281,7 +1285,7 @@ func fetchReleaseByTag(tag string) (*githubRelease, error) { func fetchDriverReleaseByURL(apiURL string) (*githubRelease, error) { urlText := strings.TrimSpace(apiURL) if urlText == "" { - return nil, fmt.Errorf("API 地址为空") + return nil, newLocalizedDriverBackendError("driver_manager.backend.error.api_url_empty", nil, nil) } client := newHTTPClientWithGlobalProxy(driverReleaseAssetSizeProbeTimeout) @@ -1298,7 +1302,11 @@ func fetchDriverReleaseByURL(apiURL string) (*githubRelease, error) { } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("拉取 Release 信息失败:HTTP %d", resp.StatusCode) + return nil, newLocalizedDriverBackendError( + "driver_manager.backend.error.release_info_fetch_failed", + nil, + fmt.Errorf("HTTP %d", resp.StatusCode), + ) } var release githubRelease @@ -1308,9 +1316,9 @@ func fetchDriverReleaseByURL(apiURL string) (*githubRelease, error) { return &release, nil } -func resolveDriverPackageSizeText(definition driverDefinition, pkg installedDriverPackage, packageMetaExists bool, packageSizeBytesMap map[string]int64) string { +func resolveDriverPackageSizeText(definition driverDefinition, pkg installedDriverPackage, packageMetaExists bool, packageSizeBytesMap map[string]int64, text func(string, map[string]any) string) string { if definition.BuiltIn { - return "内置" + return driverManagerLocalizedText(text, "driver_manager.package_size.built_in", nil, "Built-in") } normalizedType := normalizeDriverType(definition.Type) @@ -1325,11 +1333,22 @@ func resolveDriverPackageSizeText(definition driverDefinition, pkg installedDriv } if !db.IsOptionalGoDriverBuildIncluded(normalizedType) { - return "待发布" + return driverManagerLocalizedText(text, "driver_manager.package_size.pending_release", nil, "Pending release") } return "-" } +func driverManagerLocalizedText(text func(string, map[string]any) string, key string, params map[string]any, fallback string) string { + if text == nil { + return fallback + } + localized := text(key, params) + if localized == "" { + return fallback + } + return localized +} + func readInstalledPackageSizeBytes(pkg installedDriverPackage) int64 { pathText := strings.TrimSpace(pkg.ExecutablePath) if pathText == "" { diff --git a/internal/app/methods_driver_i18n_test.go b/internal/app/methods_driver_i18n_test.go index ea9a3fe6..8d868d29 100644 --- a/internal/app/methods_driver_i18n_test.go +++ b/internal/app/methods_driver_i18n_test.go @@ -20,7 +20,7 @@ func methodsDriverFunctionSource(t *testing.T, source string, signature string) t.Helper() start := strings.Index(source, signature) if start < 0 { - t.Fatalf("methods_driver.go missing function signature %q", signature) + t.Fatalf("methods driver source missing function signature %q", signature) } rest := source[start+len(signature):] end := strings.Index(rest, "\nfunc ") @@ -39,11 +39,7 @@ func (timeoutDriverNetworkError) Temporary() bool { return true } var _ net.Error = timeoutDriverNetworkError{} func TestMethodsDriverNetworkBackendMessagesUseLocalizedText(t *testing.T) { - sourceBytes, err := os.ReadFile("methods_driver.go") - if err != nil { - t.Fatalf("read methods_driver.go: %v", err) - } - source := string(sourceBytes) + source := methodsDriverSource(t) checks := map[string]struct { rawMessages []string @@ -116,11 +112,7 @@ func TestMethodsDriverNetworkBackendCatalogKeysExist(t *testing.T) { } func TestMethodsDriverReleaseHelpersUseLocalizedText(t *testing.T) { - sourceBytes, err := os.ReadFile("methods_driver.go") - if err != nil { - t.Fatalf("read methods_driver.go: %v", err) - } - source := string(sourceBytes) + source := methodsDriverSource(t) checks := map[string]struct { rawMessages []string @@ -188,11 +180,7 @@ func TestMethodsDriverReleaseHelpersUseLocalizedText(t *testing.T) { } func TestMethodsDriverInstallActionDetailsUseEnglishInternalWrappers(t *testing.T) { - sourceBytes, err := os.ReadFile("methods_driver.go") - if err != nil { - t.Fatalf("read methods_driver.go: %v", err) - } - source := string(sourceBytes) + source := methodsDriverSource(t) checks := map[string]struct { rawMessages []string @@ -388,6 +376,7 @@ func TestLoadReleaseAssetSizesCachedUsesCurrentLanguageForEmptyCacheKey(t *testi func TestFetchDriverBundleAssetSizeIndexUsesCurrentLanguageForStructuredErrors(t *testing.T) { app := NewApp() app.SetLanguage(string(i18n.LanguageEnUS)) + disableGlobalProxyForTest(t) makeRelease := func(downloadURL string) *githubRelease { return &githubRelease{ @@ -486,6 +475,7 @@ func TestFetchReleaseByTagUsesCurrentLanguageForEmptyTag(t *testing.T) { func TestFetchDriverReleaseByURLUsesCurrentLanguageForStructuredErrors(t *testing.T) { app := NewApp() app.SetLanguage(string(i18n.LanguageEnUS)) + disableGlobalProxyForTest(t) _, err := fetchDriverReleaseByURL(" ") if err == nil { @@ -517,11 +507,7 @@ func TestFetchDriverReleaseByURLUsesCurrentLanguageForStructuredErrors(t *testin } func TestMethodsDriverVersionOptionErrorsUseLocalizedText(t *testing.T) { - sourceBytes, err := os.ReadFile("methods_driver.go") - if err != nil { - t.Fatalf("read methods_driver.go: %v", err) - } - source := string(sourceBytes) + source := methodsDriverSource(t) functionSource := methodsDriverFunctionSource(t, source, "func resolveDriverVersionOptions") for _, rawMessage := range []string{ @@ -628,11 +614,7 @@ func TestResolveDriverVersionOptionsUsesCurrentLanguageForStructuredErrors(t *te } func TestMethodsDriverModuleVersionFetchErrorsUseLocalizedText(t *testing.T) { - sourceBytes, err := os.ReadFile("methods_driver.go") - if err != nil { - t.Fatalf("read methods_driver.go: %v", err) - } - source := string(sourceBytes) + source := methodsDriverSource(t) functionSource := methodsDriverFunctionSource(t, source, "func fetchGoModuleVersionMetas(modulePath string)") for _, rawMessage := range []string{ @@ -699,11 +681,7 @@ func TestFetchGoModuleVersionMetasUsesCurrentLanguageForEmptyModulePath(t *testi } func TestMethodsDriverBundleAcquireErrorsUseLocalizedText(t *testing.T) { - sourceBytes, err := os.ReadFile("methods_driver.go") - if err != nil { - t.Fatalf("read methods_driver.go: %v", err) - } - source := string(sourceBytes) + source := methodsDriverSource(t) functionSource := methodsDriverFunctionSource(t, source, "func acquireOptionalDriverBundlePath(bundleURL string, onProgress func(downloaded, total int64), onWaiting func()) (string, error)") if strings.Contains(functionSource, `fmt.Errorf("驱动总包下载地址为空")`) { @@ -734,11 +712,7 @@ func TestAcquireOptionalDriverBundlePathUsesCurrentLanguageForEmptyURL(t *testin } func TestMethodsDriverManifestErrorsUseLocalizedText(t *testing.T) { - sourceBytes, err := os.ReadFile("methods_driver.go") - if err != nil { - t.Fatalf("read methods_driver.go: %v", err) - } - source := string(sourceBytes) + source := methodsDriverSource(t) checks := map[string]struct { rawMessages []string @@ -852,6 +826,7 @@ func TestResolveDriverRepositoryURLUsesCurrentLanguageForStructuredErrors(t *tes func TestGetDriverStatusListUsesCurrentLanguageForManifestErrors(t *testing.T) { app := NewApp() app.SetLanguage(string(i18n.LanguageEnUS)) + disableGlobalProxyForTest(t) t.Cleanup(func() { app.SetLanguage(string(i18n.LanguageZhCN)) }) @@ -948,11 +923,7 @@ func TestGetDriverStatusListUsesCurrentLanguageForManifestErrors(t *testing.T) { } func TestMethodsDriverUnsupportedVersionErrorsUseLocalizedText(t *testing.T) { - sourceBytes, err := os.ReadFile("methods_driver.go") - if err != nil { - t.Fatalf("read methods_driver.go: %v", err) - } - source := string(sourceBytes) + source := methodsDriverSource(t) functionSource := methodsDriverFunctionSource(t, source, "func (a *App) localizeDriverSelectionError") for _, key := range []string{ @@ -966,11 +937,7 @@ func TestMethodsDriverUnsupportedVersionErrorsUseLocalizedText(t *testing.T) { } func TestMethodsDriverUnsupportedVersionErrorTypesUseLocalizedText(t *testing.T) { - sourceBytes, err := os.ReadFile("methods_driver.go") - if err != nil { - t.Fatalf("read methods_driver.go: %v", err) - } - source := string(sourceBytes) + source := methodsDriverSource(t) checks := map[string]struct { rawMessages []string @@ -1136,11 +1103,7 @@ func TestLocalizeDriverSelectionErrorUsesCurrentLanguageForUnsupportedVersions(t } func TestMethodsDriverRuntimeReasonCompatibilityUsesLocalizedText(t *testing.T) { - sourceBytes, err := os.ReadFile("methods_driver.go") - if err != nil { - t.Fatalf("read methods_driver.go: %v", err) - } - source := string(sourceBytes) + source := methodsDriverSource(t) checks := map[string]struct { rawMessages []string @@ -1335,11 +1298,7 @@ func TestResolveDriverDisplayNameUsesCurrentLanguageFallbackName(t *testing.T) { } func TestMethodsDriverUpdateStatusUsesLocalizedText(t *testing.T) { - sourceBytes, err := os.ReadFile("methods_driver.go") - if err != nil { - t.Fatalf("read methods_driver.go: %v", err) - } - source := string(sourceBytes) + source := methodsDriverSource(t) checks := map[string]struct { rawMessages []string @@ -1460,11 +1419,7 @@ func TestOptionalDriverPackageUpdateStatusUsesCurrentLanguageForMongoCompatibili } func TestMethodsDriverSourceBuildHelperErrorsUseLocalizedText(t *testing.T) { - sourceBytes, err := os.ReadFile("methods_driver.go") - if err != nil { - t.Fatalf("read methods_driver.go: %v", err) - } - source := string(sourceBytes) + source := methodsDriverSource(t) checks := map[string]struct { rawMessages []string diff --git a/internal/app/methods_driver_local_import_test.go b/internal/app/methods_driver_local_import_test.go index 53e938b1..dea64b65 100644 --- a/internal/app/methods_driver_local_import_test.go +++ b/internal/app/methods_driver_local_import_test.go @@ -75,11 +75,7 @@ func TestInstallLocalDriverPackageUsesCurrentLanguageForEmptyLocalPath(t *testin } func TestLocalDriverImportErrorsUseI18nWrappers(t *testing.T) { - sourceBytes, err := os.ReadFile("methods_driver.go") - if err != nil { - t.Fatalf("read methods_driver.go: %v", err) - } - source := string(sourceBytes) + source := methodsDriverSource(t) functionNames := []string{ "(*App) InstallLocalDriverPackage", @@ -243,11 +239,7 @@ func TestDriverOperationFailureDetailUsesCurrentLanguageForRuntimeDependencyWrap } func TestInstallLocalDriverPackageProgressUsesLocalizedText(t *testing.T) { - sourceBytes, err := os.ReadFile("methods_driver.go") - if err != nil { - t.Fatalf("read methods_driver.go: %v", err) - } - source := string(sourceBytes) + source := methodsDriverSource(t) start := strings.Index(source, "func (a *App) InstallLocalDriverPackage") if start < 0 { t.Fatal("methods_driver.go missing InstallLocalDriverPackage") diff --git a/internal/app/methods_driver_test_helpers_test.go b/internal/app/methods_driver_test_helpers_test.go new file mode 100644 index 00000000..bbfdb9e6 --- /dev/null +++ b/internal/app/methods_driver_test_helpers_test.go @@ -0,0 +1,37 @@ +package app + +import ( + "os" + "strings" + "testing" +) + +func methodsDriverSource(t *testing.T) string { + t.Helper() + + paths := []string{ + "methods_driver.go", + "methods_driver_assets.go", + } + parts := make([]string, 0, len(paths)) + for _, path := range paths { + content, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + parts = append(parts, string(content)) + } + return strings.Join(parts, "\n\n") +} + +func disableGlobalProxyForTest(t *testing.T) { + t.Helper() + + proxySnapshot := currentGlobalProxyConfig() + if _, err := setGlobalProxyConfig(false, proxySnapshot.Proxy); err != nil { + t.Fatalf("disable global proxy failed: %v", err) + } + t.Cleanup(func() { + _, _ = setGlobalProxyConfig(proxySnapshot.Enabled, proxySnapshot.Proxy) + }) +} diff --git a/internal/app/methods_driver_version_test.go b/internal/app/methods_driver_version_test.go index 15510c37..1280a0e4 100644 --- a/internal/app/methods_driver_version_test.go +++ b/internal/app/methods_driver_version_test.go @@ -180,11 +180,7 @@ func TestOptionalDriverDownloadZipURLAcceptsAssetAPIFragment(t *testing.T) { } func TestOptionalDriverAgentDownloadAndBuildErrorsUseI18nWrappers(t *testing.T) { - sourceBytes, err := os.ReadFile("methods_driver.go") - if err != nil { - t.Fatalf("read methods_driver.go: %v", err) - } - source := string(sourceBytes) + source := methodsDriverSource(t) functionNames := []string{ "ensureOptionalDriverAgentBinary", @@ -257,11 +253,7 @@ func TestOptionalDriverAgentDownloadAndBuildErrorsUseI18nWrappers(t *testing.T) } func TestInstallOptionalDriverAgentPackageUsesLocalizedNamedHashFailure(t *testing.T) { - sourceBytes, err := os.ReadFile("methods_driver.go") - if err != nil { - t.Fatalf("read methods_driver.go: %v", err) - } - source := string(sourceBytes) + source := methodsDriverSource(t) start := strings.Index(source, "func installOptionalDriverAgentPackage") if start < 0 { @@ -627,11 +619,7 @@ func TestVerifyInstalledOptionalDriverAgentRevisionLocalizesRevisionMismatch(t * } func TestVerifyInstalledOptionalDriverAgentRevisionUsesI18nWrappers(t *testing.T) { - sourceBytes, err := os.ReadFile("methods_driver.go") - if err != nil { - t.Fatalf("read methods_driver.go: %v", err) - } - source := string(sourceBytes) + source := methodsDriverSource(t) start := strings.Index(source, "func verifyInstalledOptionalDriverAgentRevision") if start < 0 { t.Fatal("methods_driver.go missing verifyInstalledOptionalDriverAgentRevision") @@ -999,11 +987,7 @@ func zhCNDriverProgressText(t *testing.T) func(string, map[string]any) string { } func TestOptionalDriverAgentProgressMessagesUseLocalizedText(t *testing.T) { - sourceBytes, err := os.ReadFile("methods_driver.go") - if err != nil { - t.Fatalf("read methods_driver.go: %v", err) - } - source := string(sourceBytes) + source := methodsDriverSource(t) rawMessages := []string{ `"准备安装 %s 驱动代理`, @@ -1317,11 +1301,7 @@ func TestResolveDriverPackageSizeTextUsesLocalizedStatusText(t *testing.T) { } func TestDriverVersionDisplayLabelsAndPackageSizeStatusesUseI18nKeys(t *testing.T) { - sourceBytes, err := os.ReadFile("methods_driver.go") - if err != nil { - t.Fatalf("read methods_driver.go: %v", err) - } - source := string(sourceBytes) + source := methodsDriverSource(t) if !strings.Contains(source, "resolveDriverVersionOptions(definition, repositoryURL, a.appText)") { t.Fatal("expected GetDriverVersionList to pass app localizer into resolveDriverVersionOptions") @@ -2068,11 +2048,7 @@ func resetOptionalDriverBundleDownloadCacheForTest(t *testing.T) { } func TestOptionalDriverBundleCacheHelpersDoNotContainLegacyChineseWrappers(t *testing.T) { - sourceBytes, err := os.ReadFile("methods_driver.go") - if err != nil { - t.Fatalf("read methods_driver.go: %v", err) - } - source := string(sourceBytes) + source := methodsDriverSource(t) functionNames := []string{ "downloadOptionalDriverBundleToCache",