Files
MyGoNavi/internal/app/methods_driver_version_test.go
Syngnat 36a80951a0 🐛 fix(ci/driver): 修复驱动发布错配与 dev 驱动下载命中旧资产
- 发布链路新增 driver release 资产自检,校验已发布二进制与 manifest revision/sha256 一致
- dev-build 与 release 工作流在复用已发布驱动前先校验实际资产,发现旧二进制混入时强制全量重建
- driver manifest 新增 sha256 字段,避免仅凭 source commit 与 manifest 一致就误判发布有效
- 驱动变更检测与全量重建判定纳入 release asset 校验脚本变更,确保链路修复提交会触发自愈重建
- 驱动下载优先使用 GitHub release asset API 地址,并对资产接口按 octet-stream 下载,降低 dev-latest 同名资产命中旧缓存的风险
- 保持 DuckDB 无主键编辑修复不回退,并通过 internal/db 与前端相关回归测试
2026-06-05 20:00:27 +08:00

1203 lines
40 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 at least tagged and latest bundle URLs, got %v", urls)
}
foundTagged := false
foundLatest := false
for _, candidate := range urls {
if candidate == wantTagged {
foundTagged = true
}
if candidate == wantLatest {
foundLatest = true
}
}
if !foundTagged || !foundLatest {
t.Fatalf("expected bundle URLs to include tagged=%q and latest=%q, got %v", wantTagged, wantLatest, urls)
}
}
func TestDriverReleaseAssetAPIURLUsesReleaseAssetEndpoint(t *testing.T) {
asset := githubAsset{
Name: "kingbase-driver-agent-darwin-arm64",
BrowserDownloadURL: "https://github.com/Syngnat/GoNavi-DriverAgents/releases/download/dev-latest/kingbase-driver-agent-darwin-arm64",
URL: "https://api.github.com/repos/Syngnat/GoNavi-DriverAgents/releases/assets/123456",
Size: 18 << 20,
}
if got := driverReleaseAssetAPIURL(asset); got != "https://api.github.com/repos/Syngnat/GoNavi-DriverAgents/releases/assets/123456#kingbase-driver-agent-darwin-arm64" {
t.Fatalf("expected release asset API URL, got %q", got)
}
}
func TestOptionalDriverDownloadZipURLAcceptsAssetAPIFragment(t *testing.T) {
urlText := "https://api.github.com/repos/Syngnat/GoNavi-DriverAgents/releases/assets/123456#duckdb-driver.zip"
if !isOptionalDriverDownloadZipURL(urlText) {
t.Fatalf("expected asset API URL with zip fragment to be treated as zip download: %q", urlText)
}
}
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")
}
seedReleaseAssetCacheEntry(t, "latest", map[string]int64{
duckDBWindowsDriverZipAssetName: 19 << 20,
}, map[string]int64{
duckDBWindowsDriverZipAssetName: 19 << 20,
})
legacyDirectURL := "https://example.com/duckdb-driver-agent-windows-amd64.exe"
urls := resolveOptionalDriverAgentDownloadURLs(driverDefinition{Type: "duckdb"}, legacyDirectURL, "")
if len(urls) < 2 {
t.Fatalf("expected DuckDB Windows install to keep dedicated zip ahead of legacy direct candidate, got %v", urls)
}
if urls[0] != driverReleaseLatestDownloadURL(duckDBWindowsDriverZipAssetName) {
t.Fatalf("expected DuckDB Windows dedicated zip candidate first, got %v", urls)
}
if urls[1] != legacyDirectURL {
t.Fatalf("expected DuckDB Windows to keep legacy direct candidate after dedicated zip, 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 TestDownloadOptionalDriverAgentBinaryInstallsDuckDBDedicatedZip(t *testing.T) {
if runtime.GOOS != "windows" || runtime.GOARCH != "amd64" {
t.Skip("DuckDB dedicated zip flow only applies on windows/amd64")
}
originalValidateFunc := validateOptionalDriverAgentExecutableFunc
validateOptionalDriverAgentExecutableFunc = func(driverType string, executablePath string) error {
return nil
}
t.Cleanup(func() {
validateOptionalDriverAgentExecutableFunc = originalValidateFunc
})
tmpDir := t.TempDir()
zipPath := filepath.Join(tmpDir, duckDBWindowsDriverZipAssetName)
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)
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, zipPath)
}))
defer server.Close()
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)
}
hash, err := downloadOptionalDriverAgentBinary(nil, driverDefinition{Type: "duckdb", Name: "DuckDB"}, server.URL+"/"+duckDBWindowsDriverZipAssetName+"?source=release", target)
if err != nil {
t.Fatalf("download dedicated zip failed: %v", err)
}
if strings.TrimSpace(hash) == "" {
t.Fatal("expected hash for installed duckdb agent")
}
if _, err := os.Stat(target); err != nil {
t.Fatalf("expected duckdb agent to be installed: %v", err)
}
dllBytes, err := os.ReadFile(filepath.Join(filepath.Dir(target), "duckdb.dll"))
if err != nil {
t.Fatalf("expected duckdb.dll to be installed: %v", err)
}
if string(dllBytes) != "dll" {
t.Fatalf("unexpected duckdb.dll content: %q", string(dllBytes))
}
}
func TestOptionalDriverDownloadZipURLAcceptsQueryString(t *testing.T) {
if !isOptionalDriverDownloadZipURL("https://example.com/duckdb-driver.zip?token=abc") {
t.Fatal("expected signed zip URL to be treated as zip download")
}
if isOptionalDriverDownloadZipURL("https://example.com/duckdb-driver-agent.exe?token=abc") {
t.Fatal("expected exe URL with query to remain non-zip download")
}
}
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 release build to prefer published MariaDB driver-agent before source fallback")
}
if shouldPreferSourceBuildBeforeDownloadForBuildType("development", "clickhouse", "2.43.1") && !shouldUseDuckDBWindowsDynamicLibrary("clickhouse") {
t.Fatal("expected development build alias not to prefer source build for ClickHouse")
}
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 shouldUseDuckDBWindowsDynamicLibrary("duckdb") {
if !shouldPreferSourceBuildBeforeDownloadForBuildType("dev", "duckdb", "2.5.6") {
t.Fatal("expected DuckDB Windows dynamic-library install to prefer local source build before bundle fallback")
}
} else if shouldPreferSourceBuildBeforeDownloadForBuildType("dev", "duckdb", "2.5.6") {
t.Fatal("expected development build not to prefer DuckDB source build on non-Windows dynamic-library platforms")
}
if shouldRequireSourceBuildBeforeDownloadForBuildType("development", "mariadb", "1.9.3") {
t.Fatal("expected development build alias to allow published MariaDB driver-agent fallback")
}
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 TestOptionalDriverInstallTimeoutsStayBounded(t *testing.T) {
if optionalDriverBundleDownloadTimeout > 15*time.Minute {
t.Fatalf("driver bundle download timeout should stay bounded, got %s", optionalDriverBundleDownloadTimeout)
}
if optionalDriverSourceBuildTimeout > 8*time.Minute {
t.Fatalf("driver source build timeout should stay bounded, got %s", optionalDriverSourceBuildTimeout)
}
}
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)
}