Move unit tests from tests/unit/ to internal/ following Go conventions: - tests/unit/queue/* -> internal/queue/* (dedup, filesystem_fallback, queue_permissions, queue_spec, queue, sqlite_queue tests) - tests/unit/gpu/* -> internal/resources/* (gpu_detector, gpu_golden tests) - tests/unit/resources/* -> internal/resources/* (manager_test.go) Update import paths in test files to reflect new locations. Note: GPU tests consolidated into resources package since GPU detection is part of resource management. Manager tests show significant new test coverage (166 lines).
288 lines
9.2 KiB
Go
288 lines
9.2 KiB
Go
package resources_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/jfraeys/fetch_ml/internal/worker"
|
|
)
|
|
|
|
// GoldenGPUStatus represents the expected GPU status output for golden file testing
|
|
type GoldenGPUStatus struct {
|
|
GPUCount int `json:"gpu_count"`
|
|
GPUType string `json:"gpu_type"`
|
|
ConfiguredVendor string `json:"configured_vendor"`
|
|
DetectionMethod string `json:"detection_method"`
|
|
EnvOverrideType string `json:"env_override_type,omitempty"`
|
|
EnvOverrideCount int `json:"env_override_count,omitempty"`
|
|
BuildTags map[string]bool `json:"build_tags"`
|
|
NativeAvailable bool `json:"native_available"`
|
|
Extra map[string]any `json:"extra,omitempty"`
|
|
}
|
|
|
|
// detectBuildTags returns which build tags are active
|
|
func detectBuildTags() map[string]bool {
|
|
tags := map[string]bool{
|
|
"cgo": false,
|
|
"native_libs": false,
|
|
"darwin": false,
|
|
"linux": false,
|
|
}
|
|
|
|
// CGO is determined at compile time - we can detect by trying to use native
|
|
// If native functions return "disabled", we know native_libs is not set
|
|
simdName := worker.GetSIMDImplName()
|
|
tags["native_libs"] = simdName != "disabled" && simdName != "disabled (no CGO)"
|
|
tags["cgo"] = simdName != "disabled (no CGO)"
|
|
|
|
// OS detection
|
|
if worker.IsMacOS() {
|
|
tags["darwin"] = true
|
|
} else {
|
|
tags["linux"] = true
|
|
}
|
|
|
|
return tags
|
|
}
|
|
|
|
// TestGoldenGPUStatusNVML validates GPU status against golden file for NVML path
|
|
// This test runs under all build configurations but expectations differ:
|
|
// - cgo+native_libs: Real GPU count and NVML detection
|
|
// - cgo without native_libs: Returns 0, nil (stub behavior)
|
|
// - !cgo: Returns 0, nil (stub behavior)
|
|
func TestGoldenGPUStatusNVML(t *testing.T) {
|
|
// Setup: Configure for NVIDIA detection
|
|
cfg := &worker.Config{
|
|
GPUVendor: "nvidia",
|
|
}
|
|
|
|
factory := &worker.GPUDetectorFactory{}
|
|
result := factory.CreateDetectorWithInfo(cfg)
|
|
|
|
buildTags := detectBuildTags()
|
|
|
|
// Build the golden status object
|
|
got := GoldenGPUStatus{
|
|
DetectionMethod: string(result.Info.DetectionMethod),
|
|
ConfiguredVendor: result.Info.ConfiguredVendor,
|
|
}
|
|
|
|
// Validate against build-specific expectations
|
|
if buildTags["native_libs"] && buildTags["cgo"] {
|
|
// Real NVML build: Should detect actual GPUs or get real NVML error
|
|
// GPU count may be 0 if no NVIDIA hardware, but detection method should be config
|
|
if got.DetectionMethod != "config" {
|
|
t.Errorf("cgo+native_libs: DetectionMethod = %v, want 'config'", got.DetectionMethod)
|
|
}
|
|
} else if buildTags["cgo"] {
|
|
// CGO without native_libs: Stub returns 0
|
|
if got.GPUCount != 0 {
|
|
t.Logf("cgo-only build: GPUCount = %d (expected 0 from stub)", got.GPUCount)
|
|
}
|
|
if got.NativeAvailable {
|
|
t.Error("cgo-only build: NativeAvailable should be false")
|
|
}
|
|
} else {
|
|
// No CGO: Stub returns 0
|
|
if got.GPUCount != 0 {
|
|
t.Logf("nocgo build: GPUCount = %d (expected 0 from stub)", got.GPUCount)
|
|
}
|
|
if got.NativeAvailable {
|
|
t.Error("nocgo build: NativeAvailable should be false")
|
|
}
|
|
}
|
|
|
|
// Common validations
|
|
if got.ConfiguredVendor != "nvidia" {
|
|
t.Errorf("ConfiguredVendor = %v, want 'nvidia'", got.ConfiguredVendor)
|
|
}
|
|
}
|
|
|
|
// TestGoldenGPUStatusAMDVendorAlias validates AMD aliasing is visible in output
|
|
// Build tags: all three configurations
|
|
// Runtime scenarios: amd config
|
|
func TestGoldenGPUStatusAMDVendorAlias(t *testing.T) {
|
|
cfg := &worker.Config{
|
|
GPUVendor: "amd",
|
|
}
|
|
|
|
factory := &worker.GPUDetectorFactory{}
|
|
result := factory.CreateDetectorWithInfo(cfg)
|
|
|
|
got := GoldenGPUStatus{
|
|
ConfiguredVendor: result.Info.ConfiguredVendor,
|
|
GPUType: string(result.Info.GPUType),
|
|
}
|
|
|
|
// The key assertion: configured_vendor should be "amd"
|
|
if got.ConfiguredVendor != "amd" {
|
|
t.Errorf("AMD config: ConfiguredVendor = %v, want 'amd'", got.ConfiguredVendor)
|
|
}
|
|
if got.GPUType != "amd" {
|
|
t.Errorf("AMD config: GPUType = %v, want 'amd'", got.GPUType)
|
|
}
|
|
}
|
|
|
|
// TestGoldenGPUStatusEnvOverride validates env override behavior across build configs
|
|
// Build tags: all three
|
|
// Runtime scenarios: env override set
|
|
func TestGoldenGPUStatusEnvOverride(t *testing.T) {
|
|
// Set env override
|
|
os.Setenv("FETCH_ML_GPU_TYPE", "nvidia")
|
|
os.Setenv("FETCH_ML_GPU_COUNT", "4")
|
|
defer os.Unsetenv("FETCH_ML_GPU_TYPE")
|
|
defer os.Unsetenv("FETCH_ML_GPU_COUNT")
|
|
|
|
factory := &worker.GPUDetectorFactory{}
|
|
result := factory.CreateDetectorWithInfo(&worker.Config{GPUVendor: "apple"})
|
|
|
|
got := GoldenGPUStatus{
|
|
DetectionMethod: string(result.Info.DetectionMethod),
|
|
GPUType: string(result.Info.GPUType),
|
|
EnvOverrideType: result.Info.EnvOverrideType,
|
|
EnvOverrideCount: result.Info.EnvOverrideCount,
|
|
}
|
|
|
|
// Env should take precedence over config
|
|
if got.DetectionMethod != "env_override_both" {
|
|
t.Errorf("Env override: DetectionMethod = %v, want 'env_override_both'", got.DetectionMethod)
|
|
}
|
|
if got.GPUType != "nvidia" {
|
|
t.Errorf("Env override: GPUType = %v, want 'nvidia'", got.GPUType)
|
|
}
|
|
if got.EnvOverrideType != "nvidia" {
|
|
t.Errorf("Env override: EnvOverrideType = %v, want 'nvidia'", got.EnvOverrideType)
|
|
}
|
|
if got.EnvOverrideCount != 4 {
|
|
t.Errorf("Env override: EnvOverrideCount = %v, want 4", got.EnvOverrideCount)
|
|
}
|
|
}
|
|
|
|
// TestGoldenGPUStatusMacOS validates macOS detection when running on Darwin
|
|
// Build tags: cgo+native_libs on Darwin
|
|
// Runtime scenarios: darwin
|
|
func TestGoldenGPUStatusMacOS(t *testing.T) {
|
|
if !worker.IsMacOS() {
|
|
t.Skip("Skipping macOS-specific test on non-Darwin platform")
|
|
}
|
|
|
|
cfg := &worker.Config{
|
|
GPUVendor: "apple",
|
|
AppleGPU: worker.AppleGPUConfig{Enabled: true},
|
|
}
|
|
|
|
factory := &worker.GPUDetectorFactory{}
|
|
result := factory.CreateDetectorWithInfo(cfg)
|
|
|
|
buildTags := detectBuildTags()
|
|
|
|
got := GoldenGPUStatus{
|
|
ConfiguredVendor: result.Info.ConfiguredVendor,
|
|
GPUType: string(result.Info.GPUType),
|
|
}
|
|
|
|
if got.ConfiguredVendor != "apple" {
|
|
t.Errorf("macOS: ConfiguredVendor = %v, want 'apple'", got.ConfiguredVendor)
|
|
}
|
|
if got.GPUType != "apple" {
|
|
t.Errorf("macOS: GPUType = %v, want 'apple'", got.GPUType)
|
|
}
|
|
if !buildTags["darwin"] {
|
|
t.Error("macOS: darwin build tag should be true")
|
|
}
|
|
}
|
|
|
|
// TestGoldenGPUStatusNone validates no-GPU configuration
|
|
// Build tags: all three
|
|
// Runtime scenarios: none
|
|
func TestGoldenGPUStatusNone(t *testing.T) {
|
|
cfg := &worker.Config{
|
|
GPUVendor: "none",
|
|
}
|
|
|
|
factory := &worker.GPUDetectorFactory{}
|
|
result := factory.CreateDetectorWithInfo(cfg)
|
|
|
|
if result.Detector.DetectGPUCount() != 0 {
|
|
t.Errorf("none config: GPUCount = %v, want 0", result.Detector.DetectGPUCount())
|
|
}
|
|
if result.Info.ConfiguredVendor != "none" {
|
|
t.Errorf("none config: ConfiguredVendor = %v, want 'none'", result.Info.ConfiguredVendor)
|
|
}
|
|
}
|
|
|
|
// TestGoldenJSONSerialization validates the GPU status serializes to JSON correctly
|
|
func TestGoldenJSONSerialization(t *testing.T) {
|
|
os.Setenv("FETCH_ML_GPU_TYPE", "nvidia")
|
|
os.Setenv("FETCH_ML_GPU_COUNT", "2")
|
|
defer os.Unsetenv("FETCH_ML_GPU_TYPE")
|
|
defer os.Unsetenv("FETCH_ML_GPU_COUNT")
|
|
|
|
factory := &worker.GPUDetectorFactory{}
|
|
result := factory.CreateDetectorWithInfo(nil)
|
|
|
|
status := GoldenGPUStatus{
|
|
GPUCount: result.Detector.DetectGPUCount(),
|
|
GPUType: string(result.Info.GPUType),
|
|
ConfiguredVendor: result.Info.ConfiguredVendor,
|
|
DetectionMethod: string(result.Info.DetectionMethod),
|
|
EnvOverrideType: result.Info.EnvOverrideType,
|
|
EnvOverrideCount: result.Info.EnvOverrideCount,
|
|
BuildTags: detectBuildTags(),
|
|
}
|
|
|
|
// Serialize to JSON (this mimics what ml status --json would output)
|
|
jsonData, err := json.MarshalIndent(status, "", " ")
|
|
if err != nil {
|
|
t.Fatalf("JSON serialization failed: %v", err)
|
|
}
|
|
|
|
// Verify JSON can be parsed back
|
|
var parsed GoldenGPUStatus
|
|
if err := json.Unmarshal(jsonData, &parsed); err != nil {
|
|
t.Fatalf("JSON deserialization failed: %v", err)
|
|
}
|
|
|
|
if parsed.ConfiguredVendor != status.ConfiguredVendor {
|
|
t.Errorf("JSON roundtrip: ConfiguredVendor mismatch")
|
|
}
|
|
if parsed.DetectionMethod != status.DetectionMethod {
|
|
t.Errorf("JSON roundtrip: DetectionMethod mismatch")
|
|
}
|
|
}
|
|
|
|
// TestBuildTagMatrix validates that all expected build tag combinations are testable
|
|
// This test documents the three build configurations:
|
|
// 1. cgo + native_libs: Real native library implementations
|
|
// 2. cgo without native_libs: Stubs that return errors
|
|
// 3. !cgo: Stubs that return "disabled (no CGO)"
|
|
func TestBuildTagMatrix(t *testing.T) {
|
|
tags := detectBuildTags()
|
|
|
|
// Log the current build configuration for CI visibility
|
|
t.Logf("Build configuration: cgo=%v native_libs=%v darwin=%v linux=%v",
|
|
tags["cgo"], tags["native_libs"], tags["darwin"], tags["linux"])
|
|
|
|
// Validate SIMD implementation name matches build tags
|
|
simdName := worker.GetSIMDImplName()
|
|
t.Logf("SIMD implementation: %s", simdName)
|
|
|
|
switch {
|
|
case tags["native_libs"]:
|
|
// Should have real implementation name (avx2, sha_ni, armv8_crypto, or generic)
|
|
if simdName == "disabled" || simdName == "disabled (no CGO)" {
|
|
t.Errorf("native_libs build: SIMD impl should be active, got %q", simdName)
|
|
}
|
|
case tags["cgo"]:
|
|
// Should be disabled without native_libs
|
|
if simdName != "disabled" {
|
|
t.Errorf("cgo-only build: SIMD impl should be 'disabled', got %q", simdName)
|
|
}
|
|
default:
|
|
// No CGO
|
|
if simdName != "disabled (no CGO)" {
|
|
t.Errorf("nocgo build: SIMD impl should be 'disabled (no CGO)', got %q", simdName)
|
|
}
|
|
}
|
|
}
|