PHP Classes

File: extension/core/workbook_test.go

Recommend this page to a friend!
  Packages of Roni   Easy-Excel   extension/core/workbook_test.go   Download  
File: extension/core/workbook_test.go
Role: Auxiliary data
Content type: text/plain
Description: Auxiliary data
Class: Easy-Excel
Export and read XLSX files with a PHP Go extension
Author: By
Last change:
Date: 7 days ago
Size: 11,050 bytes
 

Contents

Class file image Download
package core import ( "bytes" "os" "path/filepath" "strings" "testing" "github.com/xuri/excelize/v2" "github.com/xiidea/easy-excel/extension/compat" "github.com/xiidea/easy-excel/extension/limits" ) func testEnv() *Env { return &Env{Gate: limits.NewGate(limits.Config{})} } func mustCells(t *testing.T, vals ...any) []compat.Cell { t.Helper() out := make([]compat.Cell, len(vals)) for i, v := range vals { c, err := compat.Decode(v) if err != nil { t.Fatal(err) } out[i] = c } return out } func TestNewWorkbookDefaultSheetName(t *testing.T) { w, err := New(testEnv()) if err != nil { t.Fatal(err) } defer w.Close() sheets := w.Sheets() if len(sheets) != 1 || sheets[0] != "Worksheet" { t.Fatalf("PhpSpreadsheet default sheet must be \"Worksheet\", got %v", sheets) } } func TestStreamWriteSaveReload(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "out.xlsx") w, err := New(testEnv()) if err != nil { t.Fatal(err) } rows := [][]compat.Cell{ mustCells(t, "name", "qty", "price"), mustCells(t, "widget", int64(3), 9.99), mustCells(t, "=B2*C2", true, "0042"), } if err := w.WriteRows("Worksheet", 1, 1, rows); err != nil { t.Fatal(err) } if w.Degraded() { t.Fatal("sequential writes must stay in streaming mode") } if err := w.SaveXlsx(path, ""); err != nil { t.Fatal(err) } if err := w.Close(); err != nil { t.Fatal(err) } r, err := Open(path, "", testEnv()) if err != nil { t.Fatal(err) } defer r.Close() if v, _ := r.GetCell("Worksheet", "A1", GetRaw); v != "name" { t.Fatalf("A1 = %#v", v) } if v, _ := r.GetCell("Worksheet", "B2", GetRaw); v != float64(3) { t.Fatalf("B2 = %#v, want float64(3)", v) } if v, _ := r.GetCell("Worksheet", "B3", GetRaw); v != true { t.Fatalf("B3 = %#v, want true", v) } // "0042" must survive as a string (leading-zero rule) if v, _ := r.GetCell("Worksheet", "C3", GetRaw); v != "0042" { t.Fatalf("C3 = %#v, want \"0042\"", v) } // formula cells round-trip as "=..." like Cell::getValue() if v, _ := r.GetCell("Worksheet", "A3", GetRaw); v != "=B2*C2" { t.Fatalf("A3 = %#v, want \"=B2*C2\"", v) } if v, err := r.GetCell("Worksheet", "A3", GetCalculated); err != nil || v != "29.97" { t.Fatalf("calc A3 = %#v, %v", v, err) } // regression: excelize writes a degenerate <dimension ref="A1"/>; // dimensions of a reloaded file must come from a lazy scan if mr, mc, err := r.Dimensions("Worksheet"); err != nil || mr != 3 || mc != 3 { t.Fatalf("reloaded dims = %d,%d, %v; want 3,3", mr, mc, err) } } func TestOutOfOrderWriteDegradesOnce(t *testing.T) { w, err := New(testEnv()) if err != nil { t.Fatal(err) } defer w.Close() if err := w.WriteRows("Worksheet", 1, 1, [][]compat.Cell{ mustCells(t, "r1"), mustCells(t, "r2"), mustCells(t, "r3"), }); err != nil { t.Fatal(err) } // going back to row 2 forces the documented fallback if err := w.WriteRows("Worksheet", 2, 2, [][]compat.Cell{mustCells(t, "patched")}); err != nil { t.Fatal(err) } if !w.Degraded() { t.Fatal("out-of-order write must degrade to random mode") } // streamed data and the patch must both be present if v, _ := w.GetCell("Worksheet", "A1", GetRaw); v != "r1" { t.Fatalf("A1 = %#v", v) } if v, _ := w.GetCell("Worksheet", "A3", GetRaw); v != "r3" { t.Fatalf("A3 = %#v", v) } if v, _ := w.GetCell("Worksheet", "B2", GetRaw); v != "patched" { t.Fatalf("B2 = %#v", v) } // further writes keep working in random mode if err := w.WriteRows("Worksheet", 10, 1, [][]compat.Cell{mustCells(t, int64(7))}); err != nil { t.Fatal(err) } if v, _ := w.GetCell("Worksheet", "A10", GetRaw); v != float64(7) { t.Fatalf("A10 = %#v", v) } } func TestReadOnStreamingSheetDegrades(t *testing.T) { w, _ := New(testEnv()) defer w.Close() _ = w.WriteRows("Worksheet", 1, 1, [][]compat.Cell{mustCells(t, "x")}) if v, err := w.GetCell("Worksheet", "A1", GetRaw); err != nil || v != "x" { t.Fatalf("read-back: %#v, %v", v, err) } if !w.Degraded() { t.Fatal("reading a streamed sheet must degrade") } } func TestExplicitMarkersThroughWriteRows(t *testing.T) { w, _ := New(testEnv()) defer w.Close() row := []compat.Cell{} for _, v := range []any{ []any{compat.MarkString, "=NOT_A_FORMULA()"}, []any{compat.MarkNumeric, "19.5"}, } { c, err := compat.Decode(v) if err != nil { t.Fatal(err) } row = append(row, c) } if err := w.WriteRows("Worksheet", 1, 1, [][]compat.Cell{row}); err != nil { t.Fatal(err) } if v, _ := w.GetCell("Worksheet", "A1", GetRaw); v != "=NOT_A_FORMULA()" { // explicit strings are stored as text; getValue still shows the text t.Fatalf("A1 = %#v", v) } if f, _ := w.f.GetCellFormula("Worksheet", "A1"); f != "" { t.Fatalf("A1 must not be a formula cell, got %q", f) } if v, _ := w.GetCell("Worksheet", "B1", GetRaw); v != 19.5 { t.Fatalf("B1 = %#v", v) } } func TestChunkedSequentialRead(t *testing.T) { w, _ := New(testEnv()) defer w.Close() var rows [][]compat.Cell for i := 1; i <= 25; i++ { rows = append(rows, mustCells(t, int64(i), "row")) } _ = w.WriteRows("Worksheet", 1, 1, rows) got := 0 for start := 1; ; { chunk, more, err := w.ReadRows("Worksheet", start, 10, true, false) if err != nil { t.Fatal(err) } got += len(chunk) start += len(chunk) if !more || len(chunk) == 0 { break } } if got != 25 { t.Fatalf("read %d rows, want 25", got) } // rewind: a lower start must transparently restart the iterator chunk, _, err := w.ReadRows("Worksheet", 1, 1, true, false) if err != nil || len(chunk) != 1 || chunk[0][0] != "1" { t.Fatalf("rewind read: %v, %v", chunk, err) } } // Documents how sparse sheets behave through the iterator: excelize must pad // skipped rows, otherwise toArray() row alignment breaks (COMPAT.md). func TestSparseRowAlignment(t *testing.T) { w, _ := New(testEnv()) defer w.Close() _ = w.SetCell("Worksheet", "A1", mustCells(t, "first")[0]) _ = w.SetCell("Worksheet", "A5", mustCells(t, "fifth")[0]) rows, _, err := w.ReadRows("Worksheet", 1, 10, true, false) if err != nil { t.Fatal(err) } if len(rows) != 5 { t.Fatalf("expected 5 rows (gaps padded), got %d: %v", len(rows), rows) } if rows[0][0] != "first" || rows[4][0] != "fifth" { t.Fatalf("row alignment broken: %v", rows) } for i := 1; i <= 3; i++ { if len(rows[i]) != 0 { t.Fatalf("row %d should be empty, got %v", i+1, rows[i]) } } } func TestDimensions(t *testing.T) { w, _ := New(testEnv()) defer w.Close() if r, c, _ := w.Dimensions("Worksheet"); r != 0 || c != 0 { t.Fatalf("empty sheet dims = %d,%d", r, c) } _ = w.WriteRows("Worksheet", 2, 3, [][]compat.Cell{mustCells(t, "a", "b"), mustCells(t, "c")}) r, c, err := w.Dimensions("Worksheet") if err != nil || r != 3 || c != 4 { t.Fatalf("dims = %d,%d, %v; want 3,4", r, c, err) } } func TestSheetManagement(t *testing.T) { w, _ := New(testEnv()) defer w.Close() idx, err := w.AddSheet("Data") if err != nil || idx < 0 { t.Fatalf("AddSheet: %d, %v", idx, err) } if _, err := w.AddSheet("Data"); err == nil { t.Fatal("duplicate sheet must error") } if err := w.RenameSheet("Data", "Numbers"); err != nil { t.Fatal(err) } if got := w.Sheets(); len(got) != 2 || got[1] != "Numbers" { t.Fatalf("sheets = %v", got) } if err := w.SetActiveSheet(1); err != nil { t.Fatal(err) } if pos, name := w.ActiveSheet(); pos != 1 || name != "Numbers" { t.Fatalf("active = %d %q", pos, name) } // writes to the renamed sheet keep streaming state if err := w.WriteRows("Numbers", 1, 1, [][]compat.Cell{mustCells(t, int64(1))}); err != nil { t.Fatal(err) } if err := w.DeleteSheet("Numbers"); err != nil { t.Fatal(err) } if err := w.DeleteSheet("Worksheet"); err == nil { t.Fatal("removing the last sheet must error") } } func TestTwoSheetsStreamIndependently(t *testing.T) { w, _ := New(testEnv()) defer w.Close() if _, err := w.AddSheet("Second"); err != nil { t.Fatal(err) } if err := w.WriteRows("Worksheet", 1, 1, [][]compat.Cell{mustCells(t, "a")}); err != nil { t.Fatal(err) } if err := w.WriteRows("Second", 1, 1, [][]compat.Cell{mustCells(t, "b")}); err != nil { t.Fatal(err) } if w.Degraded() { t.Fatal("independent sheet streams must not degrade") } var buf bytes.Buffer if err := w.WriteXlsxTo(&buf); err != nil { t.Fatal(err) } f, err := excelize.OpenReader(bytes.NewReader(buf.Bytes())) if err != nil { t.Fatal(err) } defer f.Close() if v, _ := f.GetCellValue("Worksheet", "A1"); v != "a" { t.Fatalf("Worksheet!A1 = %q", v) } if v, _ := f.GetCellValue("Second", "A1"); v != "b" { t.Fatalf("Second!A1 = %q", v) } } func TestNumberFormatAndFormattedRead(t *testing.T) { w, _ := New(testEnv()) defer w.Close() _ = w.WriteRows("Worksheet", 1, 1, [][]compat.Cell{mustCells(t, 0.125)}) if err := w.SetNumberFormat("Worksheet", "A1", "0.00%"); err != nil { t.Fatal(err) } v, err := w.GetCell("Worksheet", "A1", GetFormatted) if err != nil || v != "12.50%" { t.Fatalf("formatted = %#v, %v", v, err) } // raw value untouched if v, _ := w.GetCell("Worksheet", "A1", GetRaw); v != 0.125 { t.Fatalf("raw = %#v", v) } } func TestMergeCells(t *testing.T) { w, _ := New(testEnv()) defer w.Close() _ = w.SetCell("Worksheet", "A1", mustCells(t, "title")[0]) if err := w.MergeCells("Worksheet", "A1:C1"); err != nil { t.Fatal(err) } } func TestSaveCsv(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "out.csv") w, _ := New(testEnv()) defer w.Close() _ = w.WriteRows("Worksheet", 1, 1, [][]compat.Cell{ mustCells(t, "a", "b,c", `say "hi"`), mustCells(t, "=1+1", int64(2), "-x"), }) if err := w.SaveCsv(path, "Worksheet", CsvOptions{GuardFormula: true}); err != nil { t.Fatal(err) } data, err := os.ReadFile(path) if err != nil { t.Fatal(err) } got := string(data) if !strings.Contains(got, `"b,c"`) || !strings.Contains(got, `"say ""hi"""`) { t.Fatalf("csv quoting wrong:\n%s", got) } // formula cells export their calculated value; the guard prefixes // dangerous leading characters if !strings.Contains(got, "'-x") { t.Fatalf("injection guard missing:\n%s", got) } } func TestPathPolicyEnforcedOnSave(t *testing.T) { w, _ := New(testEnv()) defer w.Close() if err := w.SaveXlsx("https://evil/out.xlsx", ""); err == nil { t.Fatal("URL scheme must be rejected") } } func TestClosedWorkbookErrors(t *testing.T) { w, _ := New(testEnv()) if err := w.Close(); err != nil { t.Fatal(err) } if err := w.Close(); err != nil { t.Fatal("Close must be idempotent") } if err := w.WriteRows("Worksheet", 1, 1, [][]compat.Cell{mustCells(t, "x")}); err == nil { t.Fatal("write after close must error") } } func TestMemoryAccountingReleasedOnClose(t *testing.T) { gate := limits.NewGate(limits.Config{}) env := &Env{Gate: gate} w, err := New(env) if err != nil { t.Fatal(err) } if gate.MemoryUsed() == 0 { t.Fatal("live workbook must be accounted") } _ = w.Close() if got := gate.MemoryUsed(); got != 0 { t.Fatalf("accounting leak after close: %d", got) } }