feat: add docfill mvp cli
This commit is contained in:
309
internal/docfill/docfill_test.go
Normal file
309
internal/docfill/docfill_test.go
Normal file
@@ -0,0 +1,309 @@
|
||||
package docfill
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
func TestReadExcelRowsUsesFirstRowAsHeaders(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "data.xlsx")
|
||||
writeWorkbook(t, path, "Daily", [][]string{
|
||||
{"name", "date", "done"},
|
||||
{"Alice", "2026-06-18", "接口数据已整理"},
|
||||
{"Bob", "2026-06-18", "报表已提交"},
|
||||
})
|
||||
|
||||
rows, err := readRows(dataConfig{Type: "excel", Path: path, Sheet: "Daily"})
|
||||
if err != nil {
|
||||
t.Fatalf("readRows returned error: %v", err)
|
||||
}
|
||||
|
||||
if len(rows) != 2 {
|
||||
t.Fatalf("expected 2 rows, got %d", len(rows))
|
||||
}
|
||||
if rows[0]["name"] != "Alice" || rows[0]["done"] != "接口数据已整理" {
|
||||
t.Fatalf("unexpected first row: %#v", rows[0])
|
||||
}
|
||||
if rows[1]["name"] != "Bob" || rows[1]["date"] != "2026-06-18" {
|
||||
t.Fatalf("unexpected second row: %#v", rows[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadSQLiteRowsUsesQueryColumnNames(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "data.db")
|
||||
db, err := sql.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.Exec(`
|
||||
create table daily (name text, done text);
|
||||
insert into daily (name, done) values ('Alice', '完成日报'), ('Bob', '完成周报');
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("seed sqlite: %v", err)
|
||||
}
|
||||
|
||||
rows, err := readRows(dataConfig{
|
||||
Type: "sqlite",
|
||||
Path: dbPath,
|
||||
Query: "select name, done from daily order by name",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("readRows returned error: %v", err)
|
||||
}
|
||||
|
||||
if len(rows) != 2 {
|
||||
t.Fatalf("expected 2 rows, got %d", len(rows))
|
||||
}
|
||||
if rows[0]["name"] != "Alice" || rows[0]["done"] != "完成日报" {
|
||||
t.Fatalf("unexpected first row: %#v", rows[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderXLSXReplacesPlaceholders(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
templatePath := filepath.Join(dir, "template.xlsx")
|
||||
outputPath := filepath.Join(dir, "out.xlsx")
|
||||
writeWorkbook(t, templatePath, "Sheet1", [][]string{
|
||||
{"姓名", "内容"},
|
||||
{"{{name}}", "今天完成:{{done}}"},
|
||||
})
|
||||
|
||||
err := renderTemplate(templatePath, outputPath, map[string]string{
|
||||
"name": "Alice",
|
||||
"done": "接口数据整理",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("renderTemplate returned error: %v", err)
|
||||
}
|
||||
|
||||
f, err := excelize.OpenFile(outputPath)
|
||||
if err != nil {
|
||||
t.Fatalf("open output: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
name, err := f.GetCellValue("Sheet1", "A2")
|
||||
if err != nil {
|
||||
t.Fatalf("read A2: %v", err)
|
||||
}
|
||||
done, err := f.GetCellValue("Sheet1", "B2")
|
||||
if err != nil {
|
||||
t.Fatalf("read B2: %v", err)
|
||||
}
|
||||
|
||||
if name != "Alice" || done != "今天完成:接口数据整理" {
|
||||
t.Fatalf("unexpected rendered values: A2=%q B2=%q", name, done)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderDOCXReplacesPlaceholdersInXMLParts(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
templatePath := filepath.Join(dir, "template.docx")
|
||||
outputPath := filepath.Join(dir, "out.docx")
|
||||
writeMinimalDocx(t, templatePath, "日报:{{name}} 完成 {{done}}")
|
||||
|
||||
err := renderTemplate(templatePath, outputPath, map[string]string{
|
||||
"name": "Alice",
|
||||
"done": "接口数据整理",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("renderTemplate returned error: %v", err)
|
||||
}
|
||||
|
||||
got := readZipEntry(t, outputPath, "word/document.xml")
|
||||
want := "日报:Alice 完成 接口数据整理"
|
||||
if got != want {
|
||||
t.Fatalf("document.xml = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderDOCXReplacesPlaceholderSplitAcrossTextNodes(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
templatePath := filepath.Join(dir, "template.docx")
|
||||
outputPath := filepath.Join(dir, "out.docx")
|
||||
writeMinimalDocx(t, templatePath, `<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:p><w:r><w:t>员工:{{na</w:t></w:r><w:r><w:t>me}}</w:t></w:r><w:r><w:t>,完成:{{done}}</w:t></w:r></w:p></w:body></w:document>`)
|
||||
|
||||
err := renderTemplate(templatePath, outputPath, map[string]string{
|
||||
"name": "Alice",
|
||||
"done": "接口数据整理",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("renderTemplate returned error: %v", err)
|
||||
}
|
||||
|
||||
got := readZipEntry(t, outputPath, "word/document.xml")
|
||||
if !strings.Contains(got, "员工:Alice,完成:接口数据整理") {
|
||||
t.Fatalf("document.xml = %q, want rendered split placeholder", got)
|
||||
}
|
||||
if strings.Contains(got, "{{") || strings.Contains(got, "}}") {
|
||||
t.Fatalf("document.xml still has placeholder markers: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunGeneratesOneOutputPerInputRow(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dataPath := filepath.Join(dir, "data.xlsx")
|
||||
templatePath := filepath.Join(dir, "template.xlsx")
|
||||
outputPattern := filepath.Join(dir, "out", "report_{{name}}_{{_row}}.xlsx")
|
||||
configPath := filepath.Join(dir, "task.json")
|
||||
|
||||
writeWorkbook(t, dataPath, "Daily", [][]string{
|
||||
{"name", "done"},
|
||||
{"Alice", "日报"},
|
||||
{"Bob", "周报"},
|
||||
})
|
||||
writeWorkbook(t, templatePath, "Sheet1", [][]string{
|
||||
{"{{name}}", "{{done}}"},
|
||||
})
|
||||
writeJSON(t, configPath, map[string]any{
|
||||
"data": map[string]any{
|
||||
"type": "excel",
|
||||
"path": dataPath,
|
||||
"sheet": "Daily",
|
||||
},
|
||||
"template": map[string]any{
|
||||
"path": templatePath,
|
||||
},
|
||||
"output": map[string]any{
|
||||
"path": outputPattern,
|
||||
},
|
||||
})
|
||||
|
||||
if err := Run(configPath); err != nil {
|
||||
t.Fatalf("Run returned error: %v", err)
|
||||
}
|
||||
|
||||
assertWorkbookCell(t, filepath.Join(dir, "out", "report_Alice_1.xlsx"), "Sheet1", "A1", "Alice")
|
||||
assertWorkbookCell(t, filepath.Join(dir, "out", "report_Bob_2.xlsx"), "Sheet1", "B1", "周报")
|
||||
}
|
||||
|
||||
func writeWorkbook(t *testing.T, path, sheet string, rows [][]string) {
|
||||
t.Helper()
|
||||
|
||||
f := excelize.NewFile()
|
||||
defer f.Close()
|
||||
|
||||
defaultSheet := f.GetSheetName(0)
|
||||
if defaultSheet != sheet {
|
||||
if err := f.SetSheetName(defaultSheet, sheet); err != nil {
|
||||
t.Fatalf("set sheet name: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
for r, row := range rows {
|
||||
for c, value := range row {
|
||||
cell, err := excelize.CoordinatesToCellName(c+1, r+1)
|
||||
if err != nil {
|
||||
t.Fatalf("cell name: %v", err)
|
||||
}
|
||||
if err := f.SetCellValue(sheet, cell, value); err != nil {
|
||||
t.Fatalf("set cell value: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := f.SaveAs(path); err != nil {
|
||||
t.Fatalf("save workbook: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func writeMinimalDocx(t *testing.T, path, body string) {
|
||||
t.Helper()
|
||||
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
t.Fatalf("create docx: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
zw := zip.NewWriter(file)
|
||||
writeZipEntry(t, zw, "[Content_Types].xml", `<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"/>`)
|
||||
writeZipEntry(t, zw, "word/document.xml", body)
|
||||
if err := zw.Close(); err != nil {
|
||||
t.Fatalf("close zip: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func writeZipEntry(t *testing.T, zw *zip.Writer, name, body string) {
|
||||
t.Helper()
|
||||
|
||||
w, err := zw.Create(name)
|
||||
if err != nil {
|
||||
t.Fatalf("create zip entry %s: %v", name, err)
|
||||
}
|
||||
if _, err := w.Write([]byte(body)); err != nil {
|
||||
t.Fatalf("write zip entry %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
func readZipEntry(t *testing.T, path, name string) string {
|
||||
t.Helper()
|
||||
|
||||
zr, err := zip.OpenReader(path)
|
||||
if err != nil {
|
||||
t.Fatalf("open zip: %v", err)
|
||||
}
|
||||
defer zr.Close()
|
||||
|
||||
for _, file := range zr.File {
|
||||
if file.Name != name {
|
||||
continue
|
||||
}
|
||||
rc, err := file.Open()
|
||||
if err != nil {
|
||||
t.Fatalf("open zip entry %s: %v", name, err)
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
data, err := io.ReadAll(rc)
|
||||
if err != nil {
|
||||
t.Fatalf("read zip entry %s: %v", name, err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
t.Fatalf("zip entry %s not found", name)
|
||||
return ""
|
||||
}
|
||||
|
||||
func writeJSON(t *testing.T, path string, value any) {
|
||||
t.Helper()
|
||||
|
||||
data, err := json.MarshalIndent(value, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("marshal json: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(path, data, 0o644); err != nil {
|
||||
t.Fatalf("write json: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertWorkbookCell(t *testing.T, path, sheet, cell, want string) {
|
||||
t.Helper()
|
||||
|
||||
f, err := excelize.OpenFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("open workbook %s: %v", path, err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
got, err := f.GetCellValue(sheet, cell)
|
||||
if err != nil {
|
||||
t.Fatalf("read cell %s: %v", cell, err)
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("%s %s = %q, want %q", filepath.Base(path), cell, got, want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user