package commandkit import ( "bytes" "encoding/json" "errors" "fmt" "io" "reflect" "strings" "testing" "text/template" ) func TestRenderReportAutomaticShapes(t *testing.T) { for _, tc := range []struct { name string value any text string yaml bool }{ {"string", "hello", "hello\n", false}, {"number", json.Number("123456789012345678901234567890"), "123456789012345678901234567890\n", false}, {"boolean", false, "false\n", false}, {"strings", []string{"one", "two"}, "one\ntwo\n", false}, {"empty strings", []string{}, "", true}, {"empty map", map[string]any{}, "", true}, {"null", nil, "", true}, {"mixed", []any{"a", false}, "", true}, {"map", map[string]any{"n": json.Number("9007199254740993")}, "", true}, } { t.Run(tc.name, func(t *testing.T) { var out bytes.Buffer if err := RenderReport(&out, Report{Value: tc.value}, ""); err != nil { t.Fatal(err) } if !tc.yaml { if out.String() != tc.text { t.Fatalf("got %q want %q", out.String(), tc.text) } return } got, err := DecodeYAMLReport(&out) if err != nil { t.Fatal(err) } want, err := normalizeReport(tc.value) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(got, want) { t.Fatalf("got %#v want %#v", got, want) } }) } } func TestRenderReportTemplatesAndFormats(t *testing.T) { tmpl, err := ParseTextTemplate("report", `{{upper .Secret}}/{{.Name}}`, template.FuncMap{"upper": strings.ToUpper}) if err != nil { t.Fatal(err) } value := struct { Name string `json:"name"` Secret string `json:"-"` }{"public", "private"} var out bytes.Buffer if err := RenderReport(&out, Report{value, tmpl}, ""); err != nil { t.Fatal(err) } if out.String() != "PRIVATE/public" { t.Fatal(out.String()) } for _, format := range []Format{FormatYAML, FormatJSON} { out.Reset() if err := RenderReport(&out, Report{value, tmpl}, format); err != nil { t.Fatal(err) } var got any if format == FormatYAML { got, err = DecodeYAMLReport(&out) } else { d := json.NewDecoder(&out) d.UseNumber() err = d.Decode(&got) } if err != nil { t.Fatal(err) } if !reflect.DeepEqual(got, map[string]any{"name": "public"}) { t.Fatalf("got %#v", got) } } out.Reset() if err := RenderReport(&out, Report{Value: map[string]any{"n": json.Number("123456789012345678901234567890")}}, FormatText); err != nil { t.Fatal(err) } got, err := DecodeYAMLReport(&out) if err != nil || !reflect.DeepEqual(got, map[string]any{"n": json.Number("123456789012345678901234567890")}) { t.Fatalf("got %#v: %v", got, err) } } type reportShortWriter struct{} func (reportShortWriter) Write(p []byte) (int, error) { return len(p) / 2, nil } type reportErrorWriter struct{ err error } func (w reportErrorWriter) Write([]byte) (int, error) { return 0, w.err } func TestRenderReportErrors(t *testing.T) { if _, err := ParseTextTemplate("bad", `{{`, nil); err == nil { t.Fatal("invalid template accepted") } tmpl, err := ParseTextTemplate("bad-execution", `{{.Missing}}`, nil) if err != nil { t.Fatal(err) } if err := RenderReport(io.Discard, Report{Value: struct{}{}, TextTemplate: tmpl}, ""); err == nil { t.Fatal("template execution succeeded") } if err := RenderReport(io.Discard, Report{Value: make(chan int)}, FormatJSON); err == nil { t.Fatal("unsupported value accepted") } if err := RenderReport(io.Discard, Report{}, Format("bad")); err == nil { t.Fatal("unknown format accepted") } tmpl, err = ParseTextTemplate("skip-json", `{{if .}}ok{{end}}`, nil) if err != nil { t.Fatal(err) } var out bytes.Buffer if err := RenderReport(&out, Report{Value: make(chan int), TextTemplate: tmpl}, FormatText); err != nil || out.String() != "ok" { t.Fatalf("templated unsupported value: %q, %v", &out, err) } failure := errors.New("writer failure") for _, format := range []Format{FormatText, FormatYAML, FormatJSON} { t.Run(string(format), func(t *testing.T) { for _, writer := range []struct { w io.Writer want error }{{reportShortWriter{}, io.ErrShortWrite}, {reportErrorWriter{failure}, failure}} { err := RenderReport(writer.w, Report{Value: "content"}, format) if !errors.Is(err, writer.want) { t.Fatalf("%s: got %v, want %v", fmt.Sprint(writer.w), err, writer.want) } } }) } } func TestRenderReportExplicitFormatsSkipTemplateAndPreserveNumbers(t *testing.T) { tmpl, err := ParseTextTemplate("invalid-at-execution", `{{.Missing}}`, nil) if err != nil { t.Fatal(err) } value := struct { Number json.Number `json:"number"` }{json.Number("123456789012345678901234567890")} want := map[string]any{"number": json.Number("123456789012345678901234567890")} for _, format := range []Format{FormatYAML, FormatJSON} { t.Run(string(format), func(t *testing.T) { var out bytes.Buffer if err := RenderReport(&out, Report{Value: value, TextTemplate: tmpl}, format); err != nil { t.Fatal(err) } var got any if format == FormatYAML { got, err = DecodeYAMLReport(&out) } else { dec := json.NewDecoder(&out) dec.UseNumber() err = dec.Decode(&got) } if err != nil || !reflect.DeepEqual(got, want) { t.Fatalf("got %#v, %v", got, err) } }) } plain, err := ParseTextTemplate("plain", "content", nil) if err != nil { t.Fatal(err) } if err := RenderReport(reportShortWriter{}, Report{TextTemplate: plain}, FormatText); !errors.Is(err, io.ErrShortWrite) { t.Fatalf("got %v", err) } }