package services import ( "strings" "testing" lokimodels "lokistack/models" ) // YAML 双模式的核心保证:**表单 → YAML → 表单 必须无损往返**。 // 如果往返丢字段(比如 multiline.maxLines 或 rateLimit),用户切一下模式 // 配置就被悄悄改掉了,这是最难排查的一类问题。 func TestRenderParseJobsYAMLRoundTrip(t *testing.T) { original := []lokimodels.ScrapeJob{ { Name: "app", Paths: []string{"/var/log/app/*.log", "/data/logs/*.log"}, Labels: map[string]string{ "tier": "backend", }, Encoding: "utf-8", JSON: true, Multiline: &lokimodels.JobMultiline{ FirstLine: `^\d{4}-\d{2}-\d{2}`, MaxLines: 256, }, DropIfMatch: []string{`/healthz`, `/metrics`}, RateLimit: &lokimodels.JobRateLimit{Rate: 2000, Burst: 4000}, Timestamp: &lokimodels.JobTimestamp{ Source: "ts", Format: "RFC3339Nano", Location: "Local", }, }, { Name: "system", Paths: []string{"/var/log/messages"}, System: true, }, } text, err := RenderJobsYAML(original) if err != nil { t.Fatalf("渲染失败: %v", err) } // 头部注释必须说明"这不是完整 promtail 配置",避免用户以为能改 clients if !strings.Contains(text, "clients") || !strings.Contains(text, "不在此处修改") { t.Error("渲染结果缺少边界说明注释") } parsed, err := ParseJobsYAML(text) if err != nil { t.Fatalf("解析失败: %v\n--- YAML ---\n%s", err, text) } if len(parsed) != len(original) { t.Fatalf("采集项数量不符: got %d want %d", len(parsed), len(original)) } first := parsed[0] if first.Name != "app" || len(first.Paths) != 2 { t.Errorf("基础字段丢失: %+v", first) } if first.Multiline == nil || first.Multiline.MaxLines != 256 { t.Errorf("multiline 丢失: %+v", first.Multiline) } if first.RateLimit == nil || first.RateLimit.Rate != 2000 || first.RateLimit.Burst != 4000 { t.Errorf("rateLimit 丢失: %+v", first.RateLimit) } if len(first.DropIfMatch) != 2 { t.Errorf("dropIfMatch 丢失: %+v", first.DropIfMatch) } if !first.JSON || first.Encoding != "utf-8" { t.Errorf("json/encoding 丢失: json=%v enc=%q", first.JSON, first.Encoding) } if first.Timestamp == nil || first.Timestamp.Source != "ts" || first.Timestamp.Location != "Local" { t.Errorf("timestamp 丢失: %+v", first.Timestamp) } if first.Labels["tier"] != "backend" { t.Errorf("labels 丢失: %+v", first.Labels) } if !parsed[1].System { t.Error("system 标记丢失") } } // 报错必须具体:语法错要能定位,语义错要指出是哪个采集项,不能笼统"格式错误"。 func TestParseJobsYAMLErrors(t *testing.T) { cases := []struct { name string in string want string }{ {"空内容", " ", "YAML 为空"}, {"语法错误", "- name: app\n paths: [/a\n", "YAML 语法错误"}, {"空列表", "[]", "没有采集项"}, {"缺名称", "- paths: [/var/log/x.log]", "缺少名称"}, {"缺路径", "- name: app", "至少需要填写一个日志路径"}, {"名称非法", "- name: 'bad name!'\n paths: [/a]", "不合法"}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { _, err := ParseJobsYAML(c.in) if err == nil { t.Fatalf("期望报错,实际通过") } if !strings.Contains(err.Error(), c.want) { t.Errorf("错误信息不够具体: got %q, want contains %q", err.Error(), c.want) } }) } }