1package sheet23import (4 "bytes"5 "flag"6 "fmt"7 "os"8 "path/filepath"9 "regexp"10 "strings"11 "testing"12)1314// saveFixtures names a release: go test -run TestFixtures -fixtures15// v1.2.3 saves the workbooks in testdata/fixtures/new with this build16// into testdata/fixtures/v1.2.3. See docs/contributing/releasing.md.17var saveFixtures = flag.String("fixtures", "", "save testdata/fixtures/new into testdata/fixtures/<release>")1819var releaseDir = regexp.MustCompile(`^v\d+\.\d+\.\d+$`)2021// TestFixturesOpenAndSaveUnchanged opens every workbook a release saved22// (testdata/fixtures/<release>) and saves it again: the bytes must be23// the file's own. The writer puts every map in a fixed order (cells by24// row then column, widths and lines by column, names by name), so byte25// order never legitimately varies and the comparison is exact.26// v0.2.0's fixtures were saved by that tag from fixtures/new, less the27// rule and validation options it refuses (date periods, top values,28// duplicates, averages, data bars, icon sets, dropdown display and29// checkbox values).30func TestFixturesOpenAndSaveUnchanged(t *testing.T) {31 if *saveFixtures != "" {32 writeFixtures(t, *saveFixtures)33 }34 dirs, err := os.ReadDir("testdata/fixtures")35 if err != nil {36 t.Fatal(err)37 }38 n := 039 for _, d := range dirs {40 if !d.IsDir() || !releaseDir.MatchString(d.Name()) {41 continue42 }43 files, _ := filepath.Glob(filepath.Join("testdata/fixtures", d.Name(), "*.012"))44 for _, path := range files {45 n++46 t.Run(d.Name()+"/"+filepath.Base(path), func(t *testing.T) {47 want, err := os.ReadFile(path)48 if err != nil {49 t.Fatal(err)50 }51 if got := resave(t, want); !bytes.Equal(got, want) {52 t.Errorf("saved differently:\n%s", lineDiff(string(want), string(got)))53 }54 })55 }56 }57 if n == 0 {58 t.Fatal("no fixtures in testdata/fixtures/v*")59 }60}6162// TestFixtureSourcesStable checks that the workbooks the next release's63// fixtures are made from open, and that what this build saves of them64// saves again unchanged, so -fixtures always makes fixtures that pass.65func TestFixtureSourcesStable(t *testing.T) {66 files, _ := filepath.Glob("testdata/fixtures/new/*.012")67 if len(files) == 0 {68 t.Fatal("no workbooks in testdata/fixtures/new")69 }70 for _, path := range files {71 t.Run(filepath.Base(path), func(t *testing.T) {72 src, err := os.ReadFile(path)73 if err != nil {74 t.Fatal(err)75 }76 once := resave(t, src)77 if twice := resave(t, once); !bytes.Equal(once, twice) {78 t.Errorf("saved differently the second time:\n%s", lineDiff(string(once), string(twice)))79 }80 })81 }82}8384// resave opens a file and returns what this build saves of it.85func resave(t *testing.T, data []byte) []byte {86 t.Helper()87 wb, err := ReadBook(bytes.NewReader(data))88 if err != nil {89 t.Fatalf("open: %v", err)90 }91 var out bytes.Buffer92 if err := wb.Write(&out); err != nil {93 t.Fatalf("save: %v", err)94 }95 return out.Bytes()96}9798// writeFixtures saves testdata/fixtures/new with this build as the99// fixtures of release, refusing to replace a release's fixtures.100func writeFixtures(t *testing.T, release string) {101 if !releaseDir.MatchString(release) {102 t.Fatalf("-fixtures %q: want a release such as v1.2.3", release)103 }104 dir := filepath.Join("testdata/fixtures", release)105 if _, err := os.Stat(dir); err == nil {106 t.Fatalf("%s exists: a release's fixtures are never replaced", dir)107 }108 if err := os.MkdirAll(dir, 0o755); err != nil {109 t.Fatal(err)110 }111 files, _ := filepath.Glob("testdata/fixtures/new/*.012")112 for _, path := range files {113 src, err := os.ReadFile(path)114 if err != nil {115 t.Fatal(err)116 }117 if err := os.WriteFile(filepath.Join(dir, filepath.Base(path)), resave(t, src), 0o644); err != nil {118 t.Fatal(err)119 }120 }121}122123// lineDiff lists the lines of want and got that differ, by line number.124func lineDiff(want, got string) string {125 w, g := strings.Split(want, "\n"), strings.Split(got, "\n")126 var b strings.Builder127 for i := range max(len(w), len(g)) {128 var wl, gl string129 if i < len(w) {130 wl = w[i]131 }132 if i < len(g) {133 gl = g[i]134 }135 if wl != gl {136 fmt.Fprintf(&b, "%d:\n want %s\n got %s\n", i+1, wl, gl)137 }138 }139 return b.String()140}