mopac-pdf v0: markdown to beautiful PDFs via digest-pinned typst
Go CLI that turns the markdown currency of this stack (Redmine notes, Discourse posts, briefing output) into typeset PDFs: two embedded typst templates (report with title page/TOC/headers, dense brief), front-matter (title/subtitle/author/date/classification/template), GFM tables, and bar charts rendered in pure Go (go-chart) from fenced chart data blocks. Engine is a prebuilt typst 0.15.1 container pinned by digest and run --network none; PDF bytes to stdout or -o. Exit codes 0/1/2. All dev in docker (dev.sh/Makefile); unit tests + golden typst fixtures plus a host-side smoke against the real engine container. Generated with Crush Assisted-by: Crush:glm-5.2
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// stubDocker prints a fake PDF; real engine behavior is covered by engine
|
||||
// tests + smoke against the actual container.
|
||||
func stubDocker(t *testing.T, fail bool) string {
|
||||
t.Helper()
|
||||
script := filepath.Join(t.TempDir(), "docker-stub")
|
||||
body := "#!/bin/sh\n"
|
||||
if fail {
|
||||
body += "echo 'typst: compile error' >&2\nexit 1\n"
|
||||
} else {
|
||||
body += "printf '%%PDF-1.7 stub\\n'\n"
|
||||
}
|
||||
if err := os.WriteFile(script, []byte(body), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("MOPAC_PDF_DOCKER", script)
|
||||
return script
|
||||
}
|
||||
|
||||
func fixture(t *testing.T) string {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(filepath.Join("..", "..", "testdata", "sample-brief.md"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := filepath.Join(t.TempDir(), "in.md")
|
||||
if err := os.WriteFile(path, data, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func TestRunStdoutPDF(t *testing.T) {
|
||||
stubDocker(t, false)
|
||||
var stdout, stderr bytes.Buffer
|
||||
code := Run([]string{fixture(t)}, &stdout, &stderr)
|
||||
if code != 0 {
|
||||
t.Fatalf("code=%d stderr=%s", code, stderr.String())
|
||||
}
|
||||
if !bytes.HasPrefix(stdout.Bytes(), []byte("%PDF-")) {
|
||||
t.Fatalf("stdout not PDF: %q", stdout.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunOutputFile(t *testing.T) {
|
||||
stubDocker(t, false)
|
||||
out := filepath.Join(t.TempDir(), "out.pdf")
|
||||
var stdout, stderr bytes.Buffer
|
||||
if code := Run([]string{"-o", out, fixture(t)}, &stdout, &stderr); code != 0 {
|
||||
t.Fatalf("code=%d stderr=%s", code, stderr.String())
|
||||
}
|
||||
data, err := os.ReadFile(out)
|
||||
if err != nil || !bytes.HasPrefix(data, []byte("%PDF-")) {
|
||||
t.Fatalf("out.pdf: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunTemplateOverrides(t *testing.T) {
|
||||
stubDocker(t, false)
|
||||
// -t wins over front-matter template key.
|
||||
var stdout, stderr bytes.Buffer
|
||||
if code := Run([]string{"-t", "report", fixture(t)}, &stdout, &stderr); code != 0 {
|
||||
t.Fatalf("code=%d stderr=%s", code, stderr.String())
|
||||
}
|
||||
// Front-matter default (brief) also resolves.
|
||||
if code := Run([]string{fixture(t)}, &stdout, &stderr); code != 0 {
|
||||
t.Fatalf("code=%d stderr=%s", code, stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunFlagNeedsValue(t *testing.T) {
|
||||
stubDocker(t, false)
|
||||
var stdout, stderr bytes.Buffer
|
||||
for _, args := range [][]string{{"-o"}, {"-t"}, {"-T"}} {
|
||||
if code := Run(args, &stdout, &stderr); code != 1 {
|
||||
t.Fatalf("args=%v code=%d", args, code)
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "needs a value") {
|
||||
t.Fatalf("args=%v stderr: %s", args, stderr.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunTooManyInputs(t *testing.T) {
|
||||
stubDocker(t, false)
|
||||
var stdout, stderr bytes.Buffer
|
||||
if code := Run([]string{"a.md", "b.md"}, &stdout, &stderr); code != 1 {
|
||||
t.Fatalf("code=%d", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunUnknownFlag(t *testing.T) {
|
||||
stubDocker(t, false)
|
||||
var stdout, stderr bytes.Buffer
|
||||
if code := Run([]string{"--bogus"}, &stdout, &stderr); code != 1 {
|
||||
t.Fatalf("code=%d", code)
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "unknown flag") {
|
||||
t.Fatalf("stderr: %s", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunUnknownTemplate(t *testing.T) {
|
||||
stubDocker(t, false)
|
||||
var stdout, stderr bytes.Buffer
|
||||
if code := Run([]string{"-t", "nope", fixture(t)}, &stdout, &stderr); code != 1 {
|
||||
t.Fatalf("code=%d", code)
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "not found") {
|
||||
t.Fatalf("stderr: %s", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunEngineFailure(t *testing.T) {
|
||||
stubDocker(t, true)
|
||||
var stdout, stderr bytes.Buffer
|
||||
if code := Run([]string{fixture(t)}, &stdout, &stderr); code != 2 {
|
||||
t.Fatalf("engine failure must exit 2, got %d", code)
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "typst: compile error") {
|
||||
t.Fatalf("engine stderr must surface: %s", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunMissingInput(t *testing.T) {
|
||||
stubDocker(t, false)
|
||||
var stdout, stderr bytes.Buffer
|
||||
if code := Run([]string{"/does/not/exist.md"}, &stdout, &stderr); code != 1 {
|
||||
t.Fatalf("code=%d", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunVersion(t *testing.T) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
if code := Run([]string{"-V"}, &stdout, &stderr); code != 0 {
|
||||
t.Fatalf("code=%d", code)
|
||||
}
|
||||
if !strings.Contains(stdout.String(), Version) {
|
||||
t.Fatalf("stdout: %s", stdout.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunHelp(t *testing.T) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
if code := Run([]string{"--help"}, &stdout, &stderr); code != 0 {
|
||||
t.Fatalf("code=%d", code)
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "Usage:") {
|
||||
t.Fatalf("stdout: %s", stdout.String())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user