Files
kjol/bundler/segment_test.go

109 lines
3.6 KiB
Go

package bundler
import (
"os"
"path/filepath"
"strings"
"testing"
)
// walkRepoTSX returns every .tsx/.jsx under frontend/src (relative to the
// package dir, which is internal/bundler).
func walkRepoTSX(t *testing.T) []string {
t.Helper()
root := filepath.Join("..", "..", "frontend", "src")
var files []string
err := filepath.WalkDir(root, func(p string, d os.DirEntry, err error) error {
if err != nil || d.IsDir() {
return nil
}
switch filepath.Ext(p) {
case ".tsx", ".jsx":
files = append(files, p)
}
return nil
})
if err != nil {
t.Skipf("cannot walk frontend/src: %v", err)
}
if len(files) == 0 {
t.Skip("no .tsx files found")
}
return files
}
// Segmentation must be lossless for every real component in the repo, and the
// join invariant must hold exactly (byte-for-byte).
func TestSegmentLosslessRepo(t *testing.T) {
files := walkRepoTSX(t)
totalChunks, multiChunkFiles := 0, 0
for _, f := range files {
data, err := os.ReadFile(f)
if err != nil {
t.Fatalf("read %s: %v", f, err)
}
src := string(data)
chunks := segmentTopLevel(src)
if strings.Join(chunks, "") != src {
t.Errorf("%s: segmentation not lossless (%d chunks)", f, len(chunks))
continue
}
totalChunks += len(chunks)
if len(chunks) > 1 {
multiChunkFiles++
}
// Every chunk (except possibly the first) must begin at a declaration
// keyword after optional leading blank lines/comments were attached to
// the previous chunk — i.e. its first non-space line starts with a kw.
}
t.Logf("segmented %d files: %d split into >1 chunk, %d chunks total (avg %.1f)",
len(files), multiChunkFiles, totalChunks, float64(totalChunks)/float64(len(files)))
}
// Focused check on the pain file: it should split into many chunks so that
// editing any single declaration recompiles only that declaration.
func TestSegmentAutoTable(t *testing.T) {
p := filepath.Join("..", "..", "frontend", "src", "ui", "AutoTable.tsx")
data, err := os.ReadFile(p)
if err != nil {
t.Skipf("cannot read AutoTable: %v", err)
}
chunks := segmentTopLevel(string(data))
if strings.Join(chunks, "") != string(data) {
t.Fatal("AutoTable segmentation not lossless")
}
// Size distribution: how big is the largest chunk (the residual worst case)?
maxLen, maxIdx := 0, 0
for i, c := range chunks {
if len(c) > maxLen {
maxLen, maxIdx = len(c), i
}
}
head := strings.TrimSpace(chunks[maxIdx])
if len(head) > 80 {
head = head[:80]
}
t.Logf("AutoTable: %d chunks, largest = %d bytes (%.0f%% of file), starts: %q",
len(chunks), maxLen, 100*float64(maxLen)/float64(len(data)), head)
if len(chunks) < 20 {
t.Errorf("expected AutoTable to split into many chunks, got %d", len(chunks))
}
}
// A few hand-written cases exercise the lexer edge cases the repo may not cover.
func TestSegmentEdgeCases(t *testing.T) {
cases := map[string]string{
"template with braces and decl-looking text": "const a = `x${ {y:1} }z\nconst notReal = 2`;\nexport const b = 3;\n",
"regex with braces": "const re = /[{}]/g;\nfunction f() { return /a{2}/; }\nexport function g() {}\n",
"block comment spanning decl keyword": "/*\nconst hidden = 1;\nfunction alsoHidden() {}\n*/\nexport const real = 1;\n",
"string with keyword": "const s = \"export function fake() {}\";\nfunction real() {}\n",
"nested template": "const t = `a${`b${1}c`}d`;\nexport const u = 1;\n",
}
for name, src := range cases {
chunks := segmentTopLevel(src)
if got := strings.Join(chunks, ""); got != src {
t.Errorf("%s: not lossless\n src=%q\n got=%q", name, src, got)
}
}
}