package graph import ( "os" "strings" "testing" "git.k3nny.fr/glint/internal/model" ) // ── svgEsc ──────────────────────────────────────────────────────────────────── func TestSvgEsc(t *testing.T) { cases := []struct{ in, want string }{ {"hello", "hello"}, {"a&b", "a&b"}, {"", "<tag>"}, {"a&bd", "a&b<c>d"}, } for _, tc := range cases { if got := svgEsc(tc.in); got != tc.want { t.Errorf("svgEsc(%q)=%q want %q", tc.in, got, tc.want) } } } // ── svgTrunc ────────────────────────────────────────────────────────────────── func TestSvgTrunc(t *testing.T) { cases := []struct { s string maxLen int want string }{ {"short", 20, "short"}, {"exactly20charslong!", 20, "exactly20charslong!"}, {"this-is-a-very-long-job-name", 20, "this-is-a-very-long…"}, {"αβγδεζηθικλμνξο", 5, "αβγδ…"}, } for _, tc := range cases { got := svgTrunc(tc.s, tc.maxLen) if got != tc.want { t.Errorf("svgTrunc(%q, %d)=%q want %q", tc.s, tc.maxLen, got, tc.want) } } } // ── svgEmpty ────────────────────────────────────────────────────────────────── func TestSvgEmpty(t *testing.T) { out := svgEmpty() if !strings.Contains(out, " bezier curves if !strings.Contains(svg, " connector in DAG mode") } } func TestPipelineSVG_DAGMode_StraightConnector(t *testing.T) { // Two stages at same height → straight connector in classic mode p := &model.Pipeline{ Stages: []string{"a", "b"}, Jobs: map[string]model.Job{ "j1": {Name: "j1", Stage: "a"}, "j2": {Name: "j2", Stage: "b"}, }, } svg := pipelineSVG(p) if !strings.Contains(svg, "`), 0o644) // result is either true (converter found) or false (no converter) — just no panic convertToPNG(svgPath, pngPath) } func TestConvertToPNG_FakeConverter(t *testing.T) { // Install a fake rsvg-convert that just creates the output file and exits 0. // This exercises the "return true" branch (line 72) and os.Remove in RenderPipeline. binDir := t.TempDir() script := "#!/bin/sh\n# rsvg-convert --output \ncp \"$3\" \"$2\"\n" fakeBin := binDir + "/rsvg-convert" if err := os.WriteFile(fakeBin, []byte(script), 0o755); err != nil { t.Fatal(err) } origPath := os.Getenv("PATH") os.Setenv("PATH", binDir+":"+origPath) t.Cleanup(func() { os.Setenv("PATH", origPath) }) svgDir := t.TempDir() svgPath := svgDir + "/test.svg" pngPath := svgDir + "/test.png" if err := os.WriteFile(svgPath, []byte(``), 0o644); err != nil { t.Fatal(err) } ok := convertToPNG(svgPath, pngPath) if !ok { t.Error("expected convertToPNG to return true with fake rsvg-convert") } } func TestRenderPipeline_PNGConversion(t *testing.T) { // Verify the PNG path is returned (and SVG removed) when converter succeeds. binDir := t.TempDir() script := "#!/bin/sh\ncp \"$3\" \"$2\"\n" if err := os.WriteFile(binDir+"/rsvg-convert", []byte(script), 0o755); err != nil { t.Fatal(err) } origPath := os.Getenv("PATH") os.Setenv("PATH", binDir+":"+origPath) t.Cleanup(func() { os.Setenv("PATH", origPath) }) p := &model.Pipeline{ Stages: []string{"build"}, Jobs: map[string]model.Job{"j": {Name: "j", Stage: "build"}}, } dir := t.TempDir() path, err := RenderPipeline(p, dir) if err != nil { t.Fatalf("RenderPipeline: %v", err) } if !strings.HasSuffix(path, ".png") { t.Errorf("expected .png output, got %q", path) } } // ── pipelineSVG coverage gaps ───────────────────────────────────────────────── func TestPipelineSVG_LShapedElbow(t *testing.T) { // Classic mode with unequal stage sizes → different column heights → L-shaped elbow p := &model.Pipeline{ Stages: []string{"build", "test"}, Jobs: map[string]model.Job{ "j1": {Name: "j1", Stage: "build"}, "j2": {Name: "j2", Stage: "test"}, "j3": {Name: "j3", Stage: "test"}, }, } svg := pipelineSVG(p) if !strings.Contains(svg, " for L-shaped elbow connector") } } func TestPipelineSVG_DAGNeedNotInPositionMap(t *testing.T) { // DAG mode: job needs a template job (starts with .) → dep not in position maps → continue p := &model.Pipeline{ Stages: []string{"build"}, Jobs: map[string]model.Job{ "j": {Name: "j", Stage: "build", Needs: []any{".hidden-template"}}, ".hidden-template": {Name: ".hidden-template", Stage: "build"}, }, } svg := pipelineSVG(p) // Should render without panic; .hidden-template is not visible so no connector drawn. if !strings.Contains(svg, "