package scrape import ( "errors" "strings" "testing" ) const vllmSample = ` # HELP vllm:num_requests_running Number of requests currently running on GPU. # TYPE vllm:num_requests_running gauge vllm:num_requests_running{model_name="llama"} 7 # TYPE vllm:num_requests_waiting gauge vllm:num_requests_waiting{model_name="llama"} 3 # TYPE vllm:kv_cache_usage_perc gauge vllm:kv_cache_usage_perc{model_name="llama"} 0.42 # TYPE vllm:cache_config_info gauge vllm:cache_config_info{block_size="16",num_gpu_blocks="4096"} 1 # TYPE vllm:generation_tokens_total counter vllm:generation_tokens_total{model_name="llama"} 12345 # TYPE vllm:request_success_total counter vllm:request_success_total{model_name="llama",finished_reason="length"} 100 vllm:request_success_total{model_name="llama",finished_reason="stop"} 50 ` func TestParseContractFields(t *testing.T) { s, err := Parse(strings.NewReader(vllmSample), VLLMMapping()) if err != nil { t.Fatal(err) } if s.Running != 7 { t.Errorf("Running=%d", s.Running) } if s.Waiting != 3 { t.Errorf("Waiting=%d", s.Waiting) } if s.KVCacheUsage != 0.42 { t.Errorf("KVCacheUsage=%v", s.KVCacheUsage) } if s.BlockSize != 16 || s.NumGPUBlocks != 4096 { t.Errorf("cache_config_info not read: block=%d blocks=%d", s.BlockSize, s.NumGPUBlocks) } // A counter split across labels must be summed. if s.SuccessTotal != 150 { t.Errorf("SuccessTotal=%v, expected 150 (labels must be summed)", s.SuccessTotal) } if s.GenerationTokens != 12345 { t.Errorf("GenerationTokens=%v", s.GenerationTokens) } } func TestParseRejectsMissingMandatoryMetric(t *testing.T) { // kv_cache_usage is absent: routing would be blind, so this must error. input := ` # TYPE vllm:num_requests_running gauge vllm:num_requests_running 1 # TYPE vllm:num_requests_waiting gauge vllm:num_requests_waiting 0 ` _, err := Parse(strings.NewReader(input), VLLMMapping()) var me *MissingError if !errors.As(err, &me) { t.Fatalf("expected MissingError, got %v", err) } if len(me.Missing) != 1 || me.Missing[0] != "vllm:kv_cache_usage_perc" { t.Errorf("missing list is wrong: %v", me.Missing) } } func TestParseToleratesMissingOptionals(t *testing.T) { input := ` # TYPE vllm:num_requests_running gauge vllm:num_requests_running 2 # TYPE vllm:num_requests_waiting gauge vllm:num_requests_waiting 1 # TYPE vllm:kv_cache_usage_perc gauge vllm:kv_cache_usage_perc 0.1 ` s, err := Parse(strings.NewReader(input), VLLMMapping()) if err != nil { t.Fatalf("errored although only optional metrics were missing: %v", err) } if s.BlockSize != 0 || s.SuccessTotal != 0 { t.Error("missing optional fields should have stayed zero") } } func TestParseWithCustomMapping(t *testing.T) { // A different engine with different names (plan ยง14: names drift across versions). input := ` # TYPE sglang:running gauge sglang:running 4 # TYPE sglang:queued gauge sglang:queued 2 # TYPE sglang:kv_usage gauge sglang:kv_usage 0.8 ` s, err := Parse(strings.NewReader(input), Mapping{ Running: "sglang:running", Waiting: "sglang:queued", KVCacheUsage: "sglang:kv_usage", }) if err != nil { t.Fatal(err) } if s.Running != 4 || s.Waiting != 2 || s.KVCacheUsage != 0.8 { t.Errorf("custom mapping did not work: %+v", s) } } func TestParseRejectsGarbage(t *testing.T) { if _, err := Parse(strings.NewReader("this is not metrics {{{"), VLLMMapping()); err == nil { t.Error("expected an error for malformed input") } }