294 lines
7.0 KiB
Go
294 lines
7.0 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func svgOnly(files []string) []string {
|
|
var out []string
|
|
for _, f := range files {
|
|
if filepath.Ext(f) == ".svg" {
|
|
out = append(out, f)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestCubeOnOnePageSmall(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cube := cubeFaces(25)
|
|
cfg := FaceTemplateConfig{NumFaces: 6, LongestSide: 25}
|
|
files, err := generateFaceTemplateSVGsWithShapes(cfg, dir, cube)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// 25mm cube, 6 faces on letter paper — should pack onto 1 SVG page
|
|
svgs := svgOnly(files)
|
|
if len(svgs) != 1 {
|
|
t.Errorf("25mm cube should fit on 1 page, got %d", len(svgs))
|
|
}
|
|
|
|
data, _ := os.ReadFile(svgs[0])
|
|
svg := string(data)
|
|
|
|
polyCount := strings.Count(svg, "<polygon")
|
|
if polyCount != 6 {
|
|
t.Errorf("expected 6 cube face polygons, got %d", polyCount)
|
|
}
|
|
// All face labels present
|
|
for face := 1; face <= 6; face++ {
|
|
if !strings.Contains(svg, fmt.Sprintf(">%d<", face)) {
|
|
t.Errorf("missing face label %d", face)
|
|
}
|
|
}
|
|
// Bullseye fiducial markers
|
|
if !strings.Contains(svg, `fill="black"`) {
|
|
t.Error("missing fiducial markers")
|
|
}
|
|
// Calibration barcodes
|
|
if strings.Count(svg, "mm</text>") < 3 {
|
|
t.Error("missing calibration bar labels")
|
|
}
|
|
// Cell borders (dashed)
|
|
if !strings.Contains(svg, `stroke-dasharray="4,2"`) {
|
|
t.Error("missing cell border dashes")
|
|
}
|
|
}
|
|
|
|
func TestCubeMultiPageLarge(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cube := cubeFaces(120)
|
|
cfg := FaceTemplateConfig{NumFaces: 6, LongestSide: 120}
|
|
files, err := generateFaceTemplateSVGsWithShapes(cfg, dir, cube)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
svgs := svgOnly(files)
|
|
if len(svgs) < 2 {
|
|
t.Errorf("120mm faces should need multiple pages, got %d", len(svgs))
|
|
}
|
|
for _, f := range svgs {
|
|
data, _ := os.ReadFile(f)
|
|
if !strings.Contains(string(data), "<polygon") {
|
|
t.Errorf("%s missing cube face polygon", f)
|
|
}
|
|
}
|
|
// PDF should also be generated
|
|
hasPDF := false
|
|
for _, f := range files {
|
|
if filepath.Ext(f) == ".pdf" {
|
|
hasPDF = true
|
|
info, _ := os.Stat(f)
|
|
if info.Size() < 100 {
|
|
t.Error("PDF file is suspiciously small")
|
|
}
|
|
}
|
|
}
|
|
if !hasPDF {
|
|
t.Error("multi-page output should include a combined PDF")
|
|
}
|
|
}
|
|
|
|
func TestCubeMediumPacking(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cube := cubeFaces(60)
|
|
cfg := FaceTemplateConfig{NumFaces: 6, LongestSide: 60}
|
|
files, err := generateFaceTemplateSVGsWithShapes(cfg, dir, cube)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
svgs := svgOnly(files)
|
|
if len(svgs) >= 6 {
|
|
t.Errorf("60mm faces should pack onto fewer than 6 pages, got %d", len(svgs))
|
|
}
|
|
totalPolys := 0
|
|
for _, f := range svgs {
|
|
data, _ := os.ReadFile(f)
|
|
totalPolys += strings.Count(string(data), "<polygon")
|
|
}
|
|
if totalPolys != 6 {
|
|
t.Errorf("expected 6 total cube face polygons across all pages, got %d", totalPolys)
|
|
}
|
|
}
|
|
|
|
func TestNoShapes(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cfg := FaceTemplateConfig{NumFaces: 4, LongestSide: 80}
|
|
files, err := GenerateFaceTemplateSVGs(cfg, dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(files) < 1 {
|
|
t.Fatal("expected at least 1 file")
|
|
}
|
|
for _, f := range svgOnly(files) {
|
|
data, _ := os.ReadFile(f)
|
|
if strings.Contains(string(data), "<polygon") {
|
|
t.Errorf("%s has polygon but no shapes were provided", f)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDefaults(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cfg := FaceTemplateConfig{}
|
|
files, err := GenerateFaceTemplateSVGs(cfg, dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(files) < 1 {
|
|
t.Fatal("default config should produce at least 1 file")
|
|
}
|
|
for _, f := range svgOnly(files) {
|
|
data, _ := os.ReadFile(f)
|
|
svg := string(data)
|
|
if !strings.HasPrefix(svg, "<?xml") {
|
|
t.Errorf("%s missing XML header", f)
|
|
}
|
|
if !strings.Contains(svg, "</svg>") {
|
|
t.Errorf("%s missing closing SVG tag", f)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGridSpacingAuto(t *testing.T) {
|
|
tests := []struct {
|
|
longest float64
|
|
expected float64
|
|
}{
|
|
{15, 5},
|
|
{30, 5},
|
|
{50, 10},
|
|
{100, 10},
|
|
{150, 20},
|
|
{250, 20},
|
|
{300, 25},
|
|
}
|
|
for _, tt := range tests {
|
|
got := pickGridSpacing(tt.longest)
|
|
if got != tt.expected {
|
|
t.Errorf("pickGridSpacing(%.0f) = %.0f, want %.0f", tt.longest, got, tt.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCubeFaces(t *testing.T) {
|
|
faces := cubeFaces(50)
|
|
if len(faces) != 6 {
|
|
t.Fatalf("cube should have 6 faces, got %d", len(faces))
|
|
}
|
|
for i, f := range faces {
|
|
if f.FaceNum != i+1 {
|
|
t.Errorf("face %d has FaceNum %d", i, f.FaceNum)
|
|
}
|
|
if len(f.Outline) != 4 {
|
|
t.Errorf("cube face should have 4 vertices, got %d", len(f.Outline))
|
|
}
|
|
for _, pt := range f.Outline {
|
|
if pt[0] != 25 && pt[0] != -25 {
|
|
t.Errorf("unexpected x coordinate: %f", pt[0])
|
|
}
|
|
if pt[1] != 25 && pt[1] != -25 {
|
|
t.Errorf("unexpected y coordinate: %f", pt[1])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPageLayout(t *testing.T) {
|
|
// Verify computePageLayout packs correctly
|
|
cfg := FaceTemplateConfig{NumFaces: 6, LongestSide: 30, PageWidth: 215.9, PageHeight: 279.4}
|
|
pages := computePageLayout(cfg)
|
|
|
|
totalCells := 0
|
|
for _, pg := range pages {
|
|
totalCells += len(pg.Cells)
|
|
for _, cell := range pg.Cells {
|
|
if cell.W < cfg.LongestSide {
|
|
t.Errorf("cell width %.1f < longest side %.1f", cell.W, cfg.LongestSide)
|
|
}
|
|
if cell.H < cfg.LongestSide {
|
|
t.Errorf("cell height %.1f < longest side %.1f", cell.H, cfg.LongestSide)
|
|
}
|
|
}
|
|
}
|
|
if totalCells != cfg.NumFaces {
|
|
t.Errorf("total cells %d != numFaces %d", totalCells, cfg.NumFaces)
|
|
}
|
|
}
|
|
|
|
func TestSingleFacePage(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cfg := FaceTemplateConfig{NumFaces: 1, LongestSide: 100}
|
|
files, err := GenerateFaceTemplateSVGs(cfg, dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
svgs := svgOnly(files)
|
|
if len(svgs) != 1 {
|
|
t.Fatalf("1 face should produce 1 SVG, got %d", len(svgs))
|
|
}
|
|
}
|
|
|
|
func TestManyFaces(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cfg := FaceTemplateConfig{NumFaces: 12, LongestSide: 40}
|
|
files, err := GenerateFaceTemplateSVGs(cfg, dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
svgs := svgOnly(files)
|
|
totalCells := 0
|
|
for _, f := range svgs {
|
|
data, _ := os.ReadFile(f)
|
|
totalCells += strings.Count(string(data), `font-weight="bold"`)
|
|
}
|
|
totalCells -= len(svgs) // subtract page headers
|
|
if totalCells != 12 {
|
|
t.Errorf("expected 12 face cells across all pages, got %d", totalCells)
|
|
}
|
|
}
|
|
|
|
func TestPDFStructure(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cube := cubeFaces(120)
|
|
cfg := FaceTemplateConfig{NumFaces: 6, LongestSide: 120, PageWidth: 215.9, PageHeight: 279.4}
|
|
|
|
path, err := GenerateFaceTemplatePDF(cfg, dir, cube)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(data) < 1000 {
|
|
t.Errorf("PDF too small: %d bytes", len(data))
|
|
}
|
|
if !strings.HasPrefix(string(data), "%PDF-") {
|
|
t.Error("missing PDF header")
|
|
}
|
|
if !strings.Contains(string(data), "%%EOF") {
|
|
t.Error("missing PDF EOF marker")
|
|
}
|
|
t.Logf("PDF: %d bytes, %s", len(data), path)
|
|
}
|
|
|
|
func TestPDFVisual(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cube := cubeFaces(120)
|
|
cfg := FaceTemplateConfig{NumFaces: 6, LongestSide: 120, PageWidth: 215.9, PageHeight: 279.4}
|
|
path, err := GenerateFaceTemplatePDF(cfg, dir, cube)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Logf("PDF written to: %s", path)
|
|
}
|