Files
MyGoNavi/internal/app/methods_driver_version_test.go
Syngnat a664f1a869 🐛 fix(driver): 精简驱动安装失败日志
驱动代理 revision 校验失败时不再在内外层重复拼接同一下载来源。

统一安装尝试错误格式,并折叠完全相同的失败条目。

补充 URL 前缀去重和安装尝试错误去重回归测试。
2026-06-04 17:02:46 +08:00

1072 lines
35 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package app
import (
"archive/zip"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"GoNavi-Wails/internal/db"
)
func TestResolveVersionedDriverOptionUsesPublishedMongoV1Release(t *testing.T) {
definition, ok := resolveDriverDefinition("mongodb")
if !ok {
t.Fatal("expected mongodb driver definition")
}
version := "1.17.4"
assetName := mongoVersionedReleaseAssetName(1)
seedReleaseAssetSizeCache(t, "tag:v"+version, map[string]int64{
assetName: 24 << 20,
})
chdirTemp(t)
gotVersion, gotURL, ok := resolveVersionedDriverOption(definition, version, "history")
if !ok {
t.Fatal("expected published mongodb v1 option to remain available")
}
if gotVersion != version {
t.Fatalf("expected version %q, got %q", version, gotVersion)
}
wantURL := fmt.Sprintf("https://github.com/%s/releases/download/v%s/%s", driverReleaseRepo, version, assetName)
if gotURL != wantURL {
t.Fatalf("expected published release URL %q, got %q", wantURL, gotURL)
}
}
func TestCurrentDriverReleaseTagUsesDevLatestForDevBuild(t *testing.T) {
originalVersion := AppVersion
AppVersion = "dev-abc1234"
t.Cleanup(func() {
AppVersion = originalVersion
})
if got := currentDriverReleaseTag(); got != driverReleaseDevTag {
t.Fatalf("expected dev driver release tag %q, got %q", driverReleaseDevTag, got)
}
}
func TestCurrentDriverReleaseTagUsesDevLatestForLocalTestBuild(t *testing.T) {
originalVersion := AppVersion
t.Cleanup(func() {
AppVersion = originalVersion
})
for _, version := range []string{"0.0.1-test", "0.7.9-dev", "0.7.9-local", "0.7.9-SNAPSHOT"} {
AppVersion = version
if got := currentDriverReleaseTag(); got != driverReleaseDevTag {
t.Fatalf("expected %s to use dev driver release tag %q, got %q", version, driverReleaseDevTag, got)
}
}
}
func TestCurrentDriverReleaseTagUsesVersionedReleaseForStableBuild(t *testing.T) {
originalVersion := AppVersion
AppVersion = "0.7.9"
t.Cleanup(func() {
AppVersion = originalVersion
})
if got := currentDriverReleaseTag(); got != "v0.7.9" {
t.Fatalf("expected stable driver release tag v0.7.9, got %q", got)
}
}
func TestResolveOptionalDriverBundleDownloadURLsUsesDriverReleaseRepo(t *testing.T) {
originalVersion := AppVersion
AppVersion = "0.7.4"
t.Cleanup(func() {
AppVersion = originalVersion
})
urls := resolveOptionalDriverBundleDownloadURLs()
wantTagged := driverReleaseDownloadURL("v0.7.4", optionalDriverBundleAssetName)
wantLatest := driverReleaseLatestDownloadURL(optionalDriverBundleAssetName)
if len(urls) != 2 {
t.Fatalf("expected tagged and latest bundle URLs, got %v", urls)
}
if urls[0] != wantTagged || urls[1] != wantLatest {
t.Fatalf("unexpected driver bundle URLs: got %v want [%q %q]", urls, wantTagged, wantLatest)
}
}
func TestDriverVersionSupportRangeForMongoDB(t *testing.T) {
definition, ok := resolveDriverDefinition("mongodb")
if !ok {
t.Fatal("expected mongodb driver definition")
}
if err := validateDriverSelectedVersion(definition, "1.17.4"); err != nil {
t.Fatalf("expected 1.17.4 to stay supported, got %v", err)
}
if err := validateDriverSelectedVersion(definition, "2.5.0"); err != nil {
t.Fatalf("expected 2.5.0 to stay supported, got %v", err)
}
if err := validateDriverSelectedVersion(definition, "1.16.1"); err == nil {
t.Fatal("expected 1.16.1 to be rejected by MongoDB support range")
}
}
func TestResolveVersionedDriverOptionSkipsMongoV1WithoutPublishedReleaseOrSourceBuild(t *testing.T) {
definition, ok := resolveDriverDefinition("mongodb")
if !ok {
t.Fatal("expected mongodb driver definition")
}
version := "1.17.4"
seedReleaseAssetSizeCache(t, "tag:v"+version, map[string]int64{})
chdirTemp(t)
_, _, ok = resolveVersionedDriverOption(definition, version, "history")
if ok {
t.Fatal("expected unpublished mongodb v1 option to be filtered out when source build is unavailable")
}
}
func TestResolveVersionedDriverOptionRejectsUnsupportedMongoV1Range(t *testing.T) {
definition, ok := resolveDriverDefinition("mongodb")
if !ok {
t.Fatal("expected mongodb driver definition")
}
seedReleaseAssetSizeCache(t, "tag:v1.16.1", map[string]int64{
mongoVersionedReleaseAssetName(1): 24 << 20,
})
_, _, ok = resolveVersionedDriverOption(definition, "1.16.1", "history")
if ok {
t.Fatal("expected MongoDB 1.16.1 to be hidden from the selectable version list")
}
}
func TestResolveDriverVersionPackageSizeBytesReadsMongoV1VersionedAsset(t *testing.T) {
definition, ok := resolveDriverDefinition("mongodb")
if !ok {
t.Fatal("expected mongodb driver definition")
}
version := "1.17.4"
assetName := mongoVersionedReleaseAssetName(1)
const wantSize int64 = 31 << 20
seedReleaseAssetSizeCache(t, "tag:v"+version, map[string]int64{
assetName: wantSize,
})
got := resolveDriverVersionPackageSizeBytes(definition, driverVersionOptionItem{
Version: version,
Source: "history",
})
if got != wantSize {
t.Fatalf("expected size %d, got %d", wantSize, got)
}
}
func TestResolveOptionalDriverAgentDownloadURLsDoesNotFallbackForHistoricalVersion(t *testing.T) {
definition, ok := resolveDriverDefinition("mongodb")
if !ok {
t.Fatal("expected mongodb driver definition")
}
explicitURL := driverReleaseDownloadURL("v1.17.4", mongoVersionedReleaseAssetName(1))
urls := resolveOptionalDriverAgentDownloadURLs(
definition,
explicitURL,
"1.17.4",
)
if len(urls) != 1 {
t.Fatalf("expected only explicit historical URL, got %d candidates: %v", len(urls), urls)
}
if urls[0] != explicitURL {
t.Fatalf("unexpected historical URL candidate: %v", urls)
}
}
func TestResolveOptionalDriverAgentDownloadURLsSkipsBundleOnlyDamengAsset(t *testing.T) {
definition, ok := resolveDriverDefinition("dameng")
if !ok {
t.Fatal("expected dameng driver definition")
}
version := normalizeVersion(definition.PinnedVersion)
assetName := optionalDriverReleaseAssetNameForVersion("dameng", version)
seedReleaseAssetCacheEntry(t, "tag:v"+version, map[string]int64{
assetName: 23 << 20,
}, nil)
seedReleaseAssetCacheEntry(t, "latest", map[string]int64{
assetName: 23 << 20,
}, nil)
urls := resolveOptionalDriverAgentDownloadURLs(definition, "builtin://activate/dameng", version)
if len(urls) != 0 {
t.Fatalf("expected bundle-only dameng install to skip direct asset URLs, got %v", urls)
}
}
func TestShouldUseOptionalDriverBundleFallbackSkipsWhenDirectAssetExists(t *testing.T) {
if shouldUseOptionalDriverBundleFallback("sqlserver", false, 1) {
t.Fatal("expected published single-file driver asset to avoid 497MB bundle fallback")
}
}
func TestShouldUseOptionalDriverBundleFallbackKeepsBundleWhenDirectAssetMissing(t *testing.T) {
if !shouldUseOptionalDriverBundleFallback("dameng", false, 0) {
t.Fatal("expected missing single-file driver asset to keep bundle fallback")
}
if shouldUseOptionalDriverBundleFallback("dameng", true, 0) {
t.Fatal("expected explicit version artifact installs to skip bundle fallback")
}
}
func TestFormatOptionalDriverAttemptErrorRemovesDuplicatedSourcePrefix(t *testing.T) {
source := "https://github.com/Syngnat/GoNavi-DriverAgents/releases/download/dev-latest/kingbase-driver-agent-darwin-arm64"
err := fmt.Errorf("%s: kingbase 驱动代理 revision 不匹配已安装src-old当前需要src-new请安装当前版本对应的 driver-agent", source)
got := formatOptionalDriverAttemptError(source, err)
if strings.Count(got, source) != 1 {
t.Fatalf("expected source to appear once, got %q", got)
}
if !strings.Contains(got, "kingbase 驱动代理 revision 不匹配") {
t.Fatalf("expected revision mismatch detail, got %q", got)
}
}
func TestAppendOptionalDriverAttemptErrorDeduplicatesIdenticalEntries(t *testing.T) {
source := "https://github.com/Syngnat/GoNavi-DriverAgents/releases/latest/download/GoNavi-DriverAgents.zip#MacOS/kingbase-driver-agent-darwin-arm64"
err := fmt.Errorf("kingbase 驱动代理 revision 不匹配已安装src-old当前需要src-new请安装当前版本对应的 driver-agent")
entries := appendOptionalDriverAttemptError(nil, source, err)
entries = appendOptionalDriverAttemptError(entries, source, err)
if len(entries) != 1 {
t.Fatalf("expected duplicate driver attempt error to be collapsed, got %d entries: %v", len(entries), entries)
}
}
func TestResolveDriverInstallVersionUsesPinnedVersionForBuiltinActivateURL(t *testing.T) {
definition, ok := resolveDriverDefinition("sqlserver")
if !ok {
t.Fatal("expected sqlserver driver definition")
}
if normalizeVersion(definition.PinnedVersion) == "" {
t.Fatal("expected sqlserver default definition to include builtin manifest pinned version")
}
got := resolveDriverInstallVersion("", "builtin://activate/sqlserver", definition)
want := normalizeVersion(definition.PinnedVersion)
if got != want {
t.Fatalf("expected builtin activate URL to fall back to pinned version %q, got %q", want, got)
}
}
func TestBuiltinActivatePinnedVersionDoesNotRestrictBundleFallback(t *testing.T) {
definition, ok := resolveDriverDefinition("sqlserver")
if !ok {
t.Fatal("expected sqlserver driver definition")
}
selectedVersion := resolveDriverInstallVersion("", "builtin://activate/sqlserver", definition)
if shouldRestrictToExplicitVersionArtifact(definition, selectedVersion) {
t.Fatalf("expected builtin activate default version %q not to restrict bundle fallback", selectedVersion)
}
}
func TestIRISDriverDefinitionUsesOptionalAgent(t *testing.T) {
definition, ok := resolveDriverDefinition("iris")
if !ok {
t.Fatal("expected iris driver definition")
}
if definition.Name != "InterSystems IRIS" {
t.Fatalf("unexpected iris driver name: %q", definition.Name)
}
if driverGoModulePathMap["iris"] != "github.com/caretdev/go-irisnative" {
t.Fatalf("unexpected iris go module path: %q", driverGoModulePathMap["iris"])
}
if definition.PinnedVersion != "0.2.1" {
t.Fatalf("unexpected iris definition pinned version: %q", definition.PinnedVersion)
}
if definition.DefaultDownloadURL != "builtin://activate/iris" {
t.Fatalf("unexpected iris default download URL: %q", definition.DefaultDownloadURL)
}
if latestDriverVersionMap["iris"] != "0.2.1" {
t.Fatalf("unexpected iris pinned version: %q", latestDriverVersionMap["iris"])
}
tags, err := optionalDriverBuildTags("iris", "")
if err != nil {
t.Fatalf("resolve iris build tags failed: %v", err)
}
if tags != "gonavi_iris_driver" {
t.Fatalf("unexpected iris build tag: %q", tags)
}
}
func TestElasticsearchDriverDefinitionUsesOptionalAgent(t *testing.T) {
definition, ok := resolveDriverDefinition("elasticsearch")
if !ok {
t.Fatal("expected elasticsearch driver definition")
}
if definition.Name != "Elasticsearch" {
t.Fatalf("unexpected elasticsearch driver name: %q", definition.Name)
}
if definition.BuiltIn {
t.Fatal("expected elasticsearch to be an optional driver agent")
}
if driverGoModulePathMap["elasticsearch"] != "github.com/elastic/go-elasticsearch/v8" {
t.Fatalf("unexpected elasticsearch go module path: %q", driverGoModulePathMap["elasticsearch"])
}
if definition.PinnedVersion != "8.19.6" {
t.Fatalf("unexpected elasticsearch definition pinned version: %q", definition.PinnedVersion)
}
if definition.DefaultDownloadURL != "builtin://activate/elasticsearch" {
t.Fatalf("unexpected elasticsearch default download URL: %q", definition.DefaultDownloadURL)
}
if latestDriverVersionMap["elasticsearch"] != "8.19.6" {
t.Fatalf("unexpected elasticsearch pinned version: %q", latestDriverVersionMap["elasticsearch"])
}
tags, err := optionalDriverBuildTags("elasticsearch", "")
if err != nil {
t.Fatalf("resolve elasticsearch build tags failed: %v", err)
}
if tags != "gonavi_elasticsearch_driver" {
t.Fatalf("unexpected elasticsearch build tag: %q", tags)
}
}
func TestBuildOptionalDriverInstallPlanMessagePrefersDirectThenBundle(t *testing.T) {
message := buildOptionalDriverInstallPlanMessage("SQL Server", "1.9.6", false, false, false, false, 1, 2)
if !strings.Contains(message, "先尝试 1 个预编译直链") {
t.Fatalf("expected direct-download hint, got %q", message)
}
if !strings.Contains(message, "失败后转入 2 个驱动总包源") {
t.Fatalf("expected bundle fallback hint, got %q", message)
}
}
func TestBuildOptionalDriverFallbackProgressMessageReportsBundleFallback(t *testing.T) {
message := buildOptionalDriverFallbackProgressMessage("SQL Server", 1, 2, false)
if !strings.Contains(message, "预编译直链未命中") {
t.Fatalf("expected direct miss hint, got %q", message)
}
if !strings.Contains(message, "转入驱动总包兜底") {
t.Fatalf("expected bundle fallback hint, got %q", message)
}
}
func TestDuckDBWindowsBuildUsesDynamicLibraryTag(t *testing.T) {
if runtime.GOOS != "windows" || runtime.GOARCH != "amd64" {
t.Skip("DuckDB Windows dynamic library flow only applies on windows/amd64")
}
tags, err := optionalDriverBuildTags("duckdb", "")
if err != nil {
t.Fatalf("resolve DuckDB build tags failed: %v", err)
}
if !strings.Contains(tags, "gonavi_duckdb_driver") || !strings.Contains(tags, "duckdb_use_lib") {
t.Fatalf("expected DuckDB Windows build tags to include dynamic library tag, got %q", tags)
}
if !shouldPreferSourceBuildBeforeDownload("duckdb", "") {
t.Fatal("expected DuckDB Windows install to try local dynamic-library build before downloads")
}
if !shouldSkipReusableAgentCandidate("duckdb", "") {
t.Fatal("expected DuckDB Windows install to skip reusable static agent candidates")
}
urls := resolveOptionalDriverAgentDownloadURLs(driverDefinition{Type: "duckdb"}, "https://example.com/duckdb-driver-agent-windows-amd64.exe", "")
if len(urls) != 0 {
t.Fatalf("expected DuckDB Windows install to skip single-file direct downloads, got %v", urls)
}
}
func TestInstallOptionalDriverAgentFromLocalZipExtractsDuckDBDLL(t *testing.T) {
if runtime.GOOS != "windows" || runtime.GOARCH != "amd64" {
t.Skip("DuckDB DLL support file is only required on windows/amd64")
}
tmpDir := t.TempDir()
zipPath := filepath.Join(tmpDir, "duckdb-driver.zip")
zipFile, err := os.Create(zipPath)
if err != nil {
t.Fatalf("create zip failed: %v", err)
}
zw := zip.NewWriter(zipFile)
for name, content := range map[string]string{
"Windows/duckdb-driver-agent-windows-amd64.exe": "agent",
"Windows/duckdb.dll": "dll",
} {
w, err := zw.Create(name)
if err != nil {
t.Fatalf("create zip entry %s failed: %v", name, err)
}
if _, err := w.Write([]byte(content)); err != nil {
t.Fatalf("write zip entry %s failed: %v", name, err)
}
}
if err := zw.Close(); err != nil {
t.Fatalf("close zip writer failed: %v", err)
}
if err := zipFile.Close(); err != nil {
t.Fatalf("close zip file failed: %v", err)
}
target := filepath.Join(tmpDir, "install", "duckdb-driver-agent.exe")
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
t.Fatalf("create install dir failed: %v", err)
}
entryName, err := installOptionalDriverAgentFromLocalZip(zipPath, driverDefinition{Type: "duckdb", Name: "DuckDB"}, target, "")
if err != nil {
t.Fatalf("install local DuckDB zip failed: %v", err)
}
if entryName != "Windows/duckdb-driver-agent-windows-amd64.exe" {
t.Fatalf("unexpected extracted agent entry: %q", entryName)
}
dllBytes, err := os.ReadFile(filepath.Join(filepath.Dir(target), "duckdb.dll"))
if err != nil {
t.Fatalf("expected duckdb.dll to be extracted: %v", err)
}
if string(dllBytes) != "dll" {
t.Fatalf("unexpected duckdb.dll content: %q", string(dllBytes))
}
}
func TestDownloadDriverPackageRejectsUnsupportedMongoVersion(t *testing.T) {
app := &App{}
result := app.DownloadDriverPackage("mongodb", "1.16.1", "builtin://activate/mongodb?channel=history&version=1.16.1", t.TempDir())
if result.Success {
t.Fatal("expected unsupported MongoDB 1.16.1 install to be rejected")
}
if !strings.Contains(result.Message, "仅支持 1.17.x 和 2.x") {
t.Fatalf("expected support-range error, got %q", result.Message)
}
}
func TestResolveRecentDriverVersionMetasIncludesHistoricalTDengineVersionsFromCache(t *testing.T) {
seedGoModuleVersionCache(t, "github.com/taosdata/driver-go/v3", []string{
"3.8.0",
"3.7.8",
"3.7.7",
"3.7.6",
"3.7.5",
"3.7.4",
"3.7.3",
"3.7.2",
"3.7.1",
"3.7.0",
"3.6.0",
"3.5.8",
"3.5.7",
"3.5.6",
"3.5.5",
"3.5.4",
"3.5.3",
"3.5.2",
"3.5.1",
"3.5.0",
"3.3.1",
"3.1.0",
"3.0.4",
"3.0.3",
"3.0.2",
"3.0.1",
"3.0.0",
})
metas := resolveRecentDriverVersionMetas("tdengine", driverRecentVersionLimit)
versions := make([]string, 0, len(metas))
for _, meta := range metas {
versions = append(versions, meta.Version)
}
if !containsVersion(versions, "3.5.8") {
t.Fatalf("expected tdengine historical version 3.5.8 to remain selectable, got %v", versions)
}
if !containsVersion(versions, "3.3.1") {
t.Fatalf("expected tdengine historical version 3.3.1 to remain selectable, got %v", versions)
}
}
func TestResolveRecentDriverVersionMetasFallsBackToHistoricalTDengineMatrix(t *testing.T) {
driverModuleVersionMu.Lock()
original := driverModuleVersionMap
driverModuleVersionMap = map[string]goModuleVersionListCacheEntry{}
driverModuleVersionMu.Unlock()
t.Cleanup(func() {
driverModuleVersionMu.Lock()
driverModuleVersionMap = original
driverModuleVersionMu.Unlock()
})
metas := resolveRecentDriverVersionMetas("tdengine", driverRecentVersionLimit)
versions := make([]string, 0, len(metas))
for _, meta := range metas {
versions = append(versions, meta.Version)
}
if !containsVersion(versions, "3.5.8") {
t.Fatalf("expected tdengine fallback list to include 3.5.8, got %v", versions)
}
if !containsVersion(versions, "3.3.1") {
t.Fatalf("expected tdengine fallback list to include 3.3.1, got %v", versions)
}
}
func TestShouldForceSourceBuildForResolvedDownload(t *testing.T) {
if !shouldForceSourceBuildForResolvedDownload("mongodb", "1.17.4", "builtin://activate/mongodb?channel=history&version=1.17.4") {
t.Fatal("expected mongodb v1 builtin install to keep source build mode")
}
explicitURL := driverReleaseDownloadURL("v1.17.4", mongoVersionedReleaseAssetName(1))
if shouldForceSourceBuildForResolvedDownload("mongodb", "1.17.4", explicitURL) {
t.Fatal("expected mongodb v1 published asset install to skip forced source build")
}
if shouldForceSourceBuildForResolvedDownload("mongodb", "2.5.0", "builtin://activate/mongodb?channel=latest&version=2.5.0") {
t.Fatal("expected mongodb v2 install not to force source build")
}
}
func TestShouldPreferSourceBuildBeforeDownloadDoesNotPreferKingbase(t *testing.T) {
if shouldPreferSourceBuildBeforeDownload("kingbase", "0.0.0-20201021123113-29bd62a876c3") {
t.Fatal("expected kingbase release install not to prefer source build before download")
}
}
func TestShouldPreferSourceBuildBeforeDownloadForDevelopmentBuild(t *testing.T) {
if !shouldPreferSourceBuildBeforeDownloadForBuildType("dev", "mariadb", "1.9.3") {
t.Fatal("expected development build to prefer local driver-agent source build")
}
if !shouldPreferSourceBuildBeforeDownloadForBuildType("development", "clickhouse", "2.43.1") {
t.Fatal("expected development build alias to prefer local driver-agent source build")
}
if shouldPreferSourceBuildBeforeDownloadForBuildType("production", "mariadb", "1.9.3") {
t.Fatal("expected production build not to prefer source build for MariaDB")
}
if shouldPreferSourceBuildBeforeDownloadForBuildType("dev", "mysql", "") {
t.Fatal("expected built-in drivers not to prefer optional driver-agent source build")
}
}
func TestShouldRequireSourceBuildBeforeDownloadForDevelopmentBuild(t *testing.T) {
if shouldRequireSourceBuildBeforeDownloadForBuildType("dev", "duckdb", "2.5.6") {
t.Fatal("expected development build to allow DuckDB release bundle fallback after local build failure")
}
if !shouldPreferSourceBuildBeforeDownloadForBuildType("dev", "duckdb", "2.5.6") {
t.Fatal("expected development build to still prefer local DuckDB driver-agent source build before bundle fallback")
}
if !shouldRequireSourceBuildBeforeDownloadForBuildType("development", "mariadb", "1.9.3") {
t.Fatal("expected development build alias to require local driver-agent source build")
}
if shouldRequireSourceBuildBeforeDownloadForBuildType("production", "duckdb", "2.5.6") {
t.Fatal("expected production build to allow DuckDB release bundle fallback")
}
if shouldRequireSourceBuildBeforeDownloadForBuildType("dev", "mysql", "") {
t.Fatal("expected built-in drivers not to require optional driver-agent source build")
}
}
func TestResolveDuckDBWindowsCGOToolchainBinFromCandidates(t *testing.T) {
binDir := t.TempDir()
writeSelfExecutable(t, filepath.Join(binDir, "gcc.exe"))
writeSelfExecutable(t, filepath.Join(binDir, "g++.exe"))
got, err := resolveDuckDBWindowsCGOToolchainBinFromCandidates([]string{
filepath.Join(t.TempDir(), "missing"),
binDir,
})
if err != nil {
t.Fatalf("expected toolchain bin to resolve: %v", err)
}
if got != filepath.Clean(binDir) {
t.Fatalf("expected %q, got %q", filepath.Clean(binDir), got)
}
}
func TestPrependPathEnvUsesCurrentEnvPath(t *testing.T) {
basePath := "base-path"
firstPath := "first-path"
secondPath := "second-path"
env := []string{"PATH=" + basePath}
env = prependPathEnv(env, firstPath)
env = prependPathEnv(env, secondPath)
got := envValue(env, "PATH")
want := strings.Join([]string{secondPath, firstPath, basePath}, string(os.PathListSeparator))
if got != want {
t.Fatalf("expected PATH %q, got %q", want, got)
}
}
func TestResolveOptionalDriverAgentDownloadURLsIncludesPublishedKingbaseAsset(t *testing.T) {
definition, ok := resolveDriverDefinition("kingbase")
if !ok {
t.Fatal("expected kingbase driver definition")
}
version := normalizeVersion(definition.PinnedVersion)
assetName := optionalDriverReleaseAssetNameForVersion("kingbase", version)
publishedAssets := map[string]int64{
assetName: 18 << 20,
}
seedReleaseAssetCacheEntry(t, "tag:v"+version, publishedAssets, publishedAssets)
seedReleaseAssetCacheEntry(t, "latest", publishedAssets, publishedAssets)
urls := resolveOptionalDriverAgentDownloadURLs(definition, "builtin://activate/kingbase", version)
if len(urls) == 0 {
t.Fatal("expected kingbase pinned install to include published download candidates")
}
if !strings.Contains(urls[0], assetName) {
t.Fatalf("expected first kingbase download URL to contain %q, got %q", assetName, urls[0])
}
}
func TestInstallOptionalDriverAgentFromLocalPathSupportsMongoV1DirectoryImport(t *testing.T) {
definition, ok := resolveDriverDefinition("mongodb")
if !ok {
t.Fatal("expected mongodb driver definition")
}
packageRoot := t.TempDir()
platformDir := filepath.Join(packageRoot, optionalDriverBundlePlatformDir(runtime.GOOS))
if err := os.MkdirAll(platformDir, 0o755); err != nil {
t.Fatalf("mkdir package dir failed: %v", err)
}
assetName := mongoVersionedReleaseAssetName(1)
writeSelfExecutable(t, filepath.Join(platformDir, assetName))
installRoot := filepath.Join(t.TempDir(), "drivers")
meta, err := installOptionalDriverAgentFromLocalPath(definition, packageRoot, installRoot, "1.17.4")
if err != nil {
t.Fatalf("expected mongodb v1 directory import to succeed, got %v", err)
}
if meta.Version != "1.17.4" {
t.Fatalf("expected imported version to stay 1.17.4, got %q", meta.Version)
}
if filepath.Base(meta.FilePath) != assetName {
t.Fatalf("expected source file %q, got %q", assetName, meta.FilePath)
}
if !strings.Contains(meta.DownloadURL, assetName) {
t.Fatalf("expected download source to reference %q, got %q", assetName, meta.DownloadURL)
}
if _, err := os.Stat(meta.ExecutablePath); err != nil {
t.Fatalf("expected imported executable to exist, got %v", err)
}
}
func TestInstallOptionalDriverAgentFromLocalPathSupportsMongoV1ZipImport(t *testing.T) {
definition, ok := resolveDriverDefinition("mongodb")
if !ok {
t.Fatal("expected mongodb driver definition")
}
assetName := mongoVersionedReleaseAssetName(1)
zipPath := filepath.Join(t.TempDir(), "mongodb-v1.zip")
writeZipWithSelfExecutable(t, zipPath, filepath.ToSlash(filepath.Join(optionalDriverBundlePlatformDir(runtime.GOOS), assetName)))
installRoot := filepath.Join(t.TempDir(), "drivers")
meta, err := installOptionalDriverAgentFromLocalPath(definition, zipPath, installRoot, "1.17.4")
if err != nil {
t.Fatalf("expected mongodb v1 zip import to succeed, got %v", err)
}
if meta.Version != "1.17.4" {
t.Fatalf("expected imported version to stay 1.17.4, got %q", meta.Version)
}
if !strings.Contains(meta.DownloadURL, assetName) {
t.Fatalf("expected zip download source to reference %q, got %q", assetName, meta.DownloadURL)
}
if _, err := os.Stat(meta.ExecutablePath); err != nil {
t.Fatalf("expected imported executable to exist, got %v", err)
}
}
func TestDownloadOptionalDriverAgentFromBundleSharesConcurrentDownload(t *testing.T) {
resetOptionalDriverBundleDownloadCacheForTest(t)
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)
})
bundlePath := filepath.Join(t.TempDir(), "GoNavi-DriverAgents.zip")
writeZipWithSelfExecutableEntries(t, bundlePath, []string{
optionalDriverBundleEntryPath("clickhouse"),
optionalDriverBundleEntryPath("mongodb"),
})
var requestCount int32
releaseDownload := make(chan struct{})
var releaseOnce sync.Once
release := func() {
releaseOnce.Do(func() {
close(releaseDownload)
})
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt32(&requestCount, 1)
<-releaseDownload
http.ServeFile(w, r, bundlePath)
}))
defer server.Close()
defer release()
errCh := make(chan error, 2)
clickhouseTarget := filepath.Join(t.TempDir(), optionalDriverExecutableBaseName("clickhouse"))
mongodbTarget := filepath.Join(t.TempDir(), optionalDriverExecutableBaseName("mongodb"))
go func() {
_, _, err := downloadOptionalDriverAgentFromBundle(
nil,
driverDefinition{Type: "clickhouse", Name: "ClickHouse"},
server.URL,
clickhouseTarget,
)
errCh <- err
}()
deadline := time.Now().Add(2 * time.Second)
for atomic.LoadInt32(&requestCount) == 0 && time.Now().Before(deadline) {
time.Sleep(10 * time.Millisecond)
}
if atomic.LoadInt32(&requestCount) != 1 {
t.Fatalf("expected first bundle request to start, got %d", atomic.LoadInt32(&requestCount))
}
go func() {
_, _, err := downloadOptionalDriverAgentFromBundle(
nil,
driverDefinition{Type: "mongodb", Name: "MongoDB"},
server.URL,
mongodbTarget,
)
errCh <- err
}()
time.Sleep(100 * time.Millisecond)
if got := atomic.LoadInt32(&requestCount); got != 1 {
t.Fatalf("expected concurrent bundle install to wait for first download, got %d requests", got)
}
release()
for i := 0; i < 2; i++ {
if err := <-errCh; err != nil {
t.Fatalf("bundle install failed: %v", err)
}
}
if got := atomic.LoadInt32(&requestCount); got != 1 {
t.Fatalf("expected one shared bundle download, got %d requests", got)
}
}
func TestEnsureOptionalDriverAgentBinaryFallsBackAfterStaleDownloadRevision(t *testing.T) {
originalProbe := optionalDriverAgentMetadataProbe
originalGoBinaryLookPath := goBinaryLookPath
t.Cleanup(func() {
optionalDriverAgentMetadataProbe = originalProbe
goBinaryLookPath = originalGoBinaryLookPath
})
tmpDir := t.TempDir()
staleAgent := filepath.Join(tmpDir, "stale-driver-agent")
if runtime.GOOS == "windows" {
staleAgent += ".exe"
}
writeSelfExecutable(t, staleAgent)
staleServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, staleAgent)
}))
defer staleServer.Close()
projectRoot := filepath.Join(tmpDir, "project")
if err := os.MkdirAll(filepath.Join(projectRoot, "cmd", "optional-driver-agent"), 0o755); err != nil {
t.Fatalf("create project root failed: %v", err)
}
if err := os.WriteFile(filepath.Join(projectRoot, "go.mod"), []byte("module GoNavi-Wails\n"), 0o644); err != nil {
t.Fatalf("write go.mod failed: %v", err)
}
if err := os.WriteFile(filepath.Join(projectRoot, "cmd", "optional-driver-agent", "main.go"), []byte("package main\n"), 0o644); err != nil {
t.Fatalf("write optional agent main failed: %v", err)
}
wd, err := os.Getwd()
if err != nil {
t.Fatalf("getwd failed: %v", err)
}
if err := os.Chdir(projectRoot); err != nil {
t.Fatalf("chdir project root failed: %v", err)
}
t.Cleanup(func() {
if err := os.Chdir(wd); err != nil {
t.Fatalf("restore cwd failed: %v", err)
}
})
goScript := filepath.Join(tmpDir, "fake-go")
if runtime.GOOS == "windows" {
goScript += ".bat"
}
if runtime.GOOS == "windows" {
if err := os.WriteFile(goScript, []byte("@echo off\r\nset out=\r\n:loop\r\nif \"%1\"==\"\" goto done\r\nif \"%1\"==\"-o\" (set out=%2& shift& shift& goto loop)\r\nshift\r\ngoto loop\r\n:done\r\ncopy /Y \"%GONAVI_TEST_BUILT_AGENT%\" \"%out%\" >nul\r\n"), 0o755); err != nil {
t.Fatalf("write fake go script failed: %v", err)
}
} else {
if err := os.WriteFile(goScript, []byte("#!/usr/bin/env sh\nout=\"\"\nwhile [ \"$#\" -gt 0 ]; do\n if [ \"$1\" = \"-o\" ]; then out=\"$2\"; shift 2; continue; fi\n shift\ndone\ncp \"$GONAVI_TEST_BUILT_AGENT\" \"$out\"\n"), 0o755); err != nil {
t.Fatalf("write fake go script failed: %v", err)
}
}
goBinaryLookPath = func(file string) (string, error) {
return goScript, nil
}
t.Setenv("GONAVI_TEST_BUILT_AGENT", staleAgent)
probeCount := 0
optionalDriverAgentMetadataProbe = func(driverType string, executablePath string) (db.OptionalDriverAgentMetadata, error) {
probeCount++
revision := "src-stale-agent"
if probeCount > 1 {
revision = db.OptionalDriverAgentRevision(driverType)
}
return db.OptionalDriverAgentMetadata{
DriverType: driverType,
AgentRevision: revision,
}, nil
}
targetPath := filepath.Join(tmpDir, optionalDriverExecutableBaseName("sqlserver"))
source, _, err := ensureOptionalDriverAgentBinary(
nil,
driverDefinition{Type: "sqlserver", Name: "SQL Server"},
targetPath,
staleServer.URL,
"1.9.6",
)
if err != nil {
t.Fatalf("expected stale direct download to fall back to source build, got %v", err)
}
if source != "local://go-build/sqlserver-driver-agent" {
t.Fatalf("expected source build fallback, got %q", source)
}
}
func seedReleaseAssetSizeCache(t *testing.T, cacheKey string, sizeByKey map[string]int64) {
t.Helper()
seedReleaseAssetCacheEntry(t, cacheKey, sizeByKey, sizeByKey)
}
func seedReleaseAssetCacheEntry(t *testing.T, cacheKey string, sizeByKey map[string]int64, publishedAssets map[string]int64) {
t.Helper()
driverReleaseSizeMu.Lock()
original := cloneReleaseAssetSizeCache(driverReleaseSizeMap)
driverReleaseSizeMap[cacheKey] = driverReleaseAssetSizeCacheEntry{
LoadedAt: time.Now(),
SizeByKey: cloneInt64Map(sizeByKey),
PublishedAssets: cloneBoolMapFromSizes(publishedAssets),
}
driverReleaseSizeMu.Unlock()
t.Cleanup(func() {
driverReleaseSizeMu.Lock()
driverReleaseSizeMap = original
driverReleaseSizeMu.Unlock()
})
}
func cloneReleaseAssetSizeCache(src map[string]driverReleaseAssetSizeCacheEntry) map[string]driverReleaseAssetSizeCacheEntry {
cloned := make(map[string]driverReleaseAssetSizeCacheEntry, len(src))
for key, value := range src {
cloned[key] = driverReleaseAssetSizeCacheEntry{
LoadedAt: value.LoadedAt,
SizeByKey: cloneInt64Map(value.SizeByKey),
PublishedAssets: cloneBoolMap(value.PublishedAssets),
Err: value.Err,
}
}
return cloned
}
func cloneBoolMap(src map[string]bool) map[string]bool {
if len(src) == 0 {
return map[string]bool{}
}
cloned := make(map[string]bool, len(src))
for key, value := range src {
cloned[key] = value
}
return cloned
}
func cloneBoolMapFromSizes(src map[string]int64) map[string]bool {
if len(src) == 0 {
return map[string]bool{}
}
cloned := make(map[string]bool, len(src))
for key := range src {
cloned[key] = true
}
return cloned
}
func cloneInt64Map(src map[string]int64) map[string]int64 {
if len(src) == 0 {
return map[string]int64{}
}
cloned := make(map[string]int64, len(src))
for key, value := range src {
cloned[key] = value
}
return cloned
}
func seedGoModuleVersionCache(t *testing.T, modulePath string, versions []string) {
t.Helper()
driverModuleVersionMu.Lock()
original := make(map[string]goModuleVersionListCacheEntry, len(driverModuleVersionMap))
for key, value := range driverModuleVersionMap {
original[key] = goModuleVersionListCacheEntry{
LoadedAt: value.LoadedAt,
Versions: append([]goModuleVersionMeta(nil), value.Versions...),
Err: value.Err,
}
}
driverModuleVersionMap[modulePath] = goModuleVersionListCacheEntry{
LoadedAt: time.Now(),
Versions: mapVersionsToMetas(versions),
}
driverModuleVersionMu.Unlock()
t.Cleanup(func() {
driverModuleVersionMu.Lock()
driverModuleVersionMap = original
driverModuleVersionMu.Unlock()
})
}
func mapVersionsToMetas(versions []string) []goModuleVersionMeta {
result := make([]goModuleVersionMeta, 0, len(versions))
for _, version := range versions {
result = append(result, goModuleVersionMeta{Version: version})
}
return result
}
func containsVersion(versions []string, target string) bool {
for _, version := range versions {
if version == target {
return true
}
}
return false
}
func chdirTemp(t *testing.T) {
t.Helper()
wd, err := os.Getwd()
if err != nil {
t.Fatalf("getwd failed: %v", err)
}
tempDir := t.TempDir()
if err := os.Chdir(tempDir); err != nil {
t.Fatalf("chdir temp failed: %v", err)
}
t.Cleanup(func() {
if err := os.Chdir(wd); err != nil {
t.Fatalf("restore cwd failed: %v", err)
}
})
}
func mongoVersionedReleaseAssetName(major int) string {
name := fmt.Sprintf("mongodb-driver-agent-v%d-%s-%s", major, runtime.GOOS, runtime.GOARCH)
if runtime.GOOS == "windows" {
return name + ".exe"
}
return name
}
func writeSelfExecutable(t *testing.T, targetPath string) {
t.Helper()
selfPath, err := os.Executable()
if err != nil {
t.Fatalf("executable path failed: %v", err)
}
content, err := os.ReadFile(selfPath)
if err != nil {
t.Fatalf("read self executable failed: %v", err)
}
if err := os.WriteFile(targetPath, content, 0o755); err != nil {
t.Fatalf("write executable failed: %v", err)
}
}
func writeZipWithSelfExecutable(t *testing.T, zipPath string, entryName string) {
t.Helper()
writeZipWithSelfExecutableEntries(t, zipPath, []string{entryName})
}
func writeZipWithSelfExecutableEntries(t *testing.T, zipPath string, entryNames []string) {
t.Helper()
selfPath, err := os.Executable()
if err != nil {
t.Fatalf("executable path failed: %v", err)
}
content, err := os.ReadFile(selfPath)
if err != nil {
t.Fatalf("read self executable failed: %v", err)
}
file, err := os.Create(zipPath)
if err != nil {
t.Fatalf("create zip failed: %v", err)
}
defer file.Close()
writer := zip.NewWriter(file)
for _, entryName := range entryNames {
entry, err := writer.Create(entryName)
if err != nil {
t.Fatalf("create zip entry failed: %v", err)
}
if _, err := entry.Write(content); err != nil {
t.Fatalf("write zip entry failed: %v", err)
}
}
if err := writer.Close(); err != nil {
t.Fatalf("close zip writer failed: %v", err)
}
}
func resetOptionalDriverBundleDownloadCacheForTest(t *testing.T) {
t.Helper()
reset := func() {
optionalDriverBundleDownloadMu.Lock()
paths := make([]string, 0, len(optionalDriverBundleDownloads))
for _, state := range optionalDriverBundleDownloads {
if state != nil && strings.TrimSpace(state.path) != "" {
paths = append(paths, state.path)
}
}
optionalDriverBundleDownloads = make(map[string]*optionalDriverBundleDownloadState)
optionalDriverBundleDownloadMu.Unlock()
for _, path := range paths {
_ = os.Remove(path)
}
}
reset()
t.Cleanup(reset)
}