- commit
- 3f36d964b8fa781fa6b3a6d687bde9e96eb16594
- parent
- 68d3ceb24a74d7c64a8aa13408fd04341f1f6787
- author
- Dami <dami.etoile@gmail.com>
- kind
- feat risk score 1.9 of 3, Jev's estimate
| M | internal/ | 11 | +++++- |
| M | internal/ | 2 | +- |
| M | internal/ | 7 | +++- |
| M | internal/ | 2 | + |
| A | internal/ | 36 | ++++++++++++++++++++ |
| A | internal/ | 54 | ++++++++++++++++++++++++++++++ |
| M | internal/ | 17 | ++++++---- |
| M | internal/ | 8 | ++-- |
@@ -18,7 +18,7 @@ type Description struct {
1818 // SheetDescription is one tab: a sheet or a notebook.1919 type SheetDescription struct {2020 Name string `json:"name"`21- Kind string `json:"kind"` // "sheet", "notebook" or "pivot"21+ Kind string `json:"kind"` // "sheet", "notebook", "pivot" or "source"2222 Shown bool `json:"shown"`2323 Hidden bool `json:"hidden"`2424 // Used is the range from A1 to the last cell with contents, "" when@@ -59,7 +59,7 @@ type TableDescription struct {
5959 // the sheet.6060 type RegionDescription struct {6161 Name string `json:"name"`62- Kind string `json:"kind"` // "output" or "linked"62+ Kind string `json:"kind"` // "output", "linked" or "source"6363 Range string `json:"range"`6464 File string `json:"file,omitempty"` // a linked region's file6565 }@@ -137,6 +137,10 @@ func describeSheet(s *sheet.Sheet, shown bool) SheetDescription {
137137 if s.IsNotebook() {138138 d.Kind, d.Notebook = "notebook", describeNotebook(s)139139 }140+ if info, ok := s.Source(); ok {141+ d.Kind, d.Rows, d.Cols = "source", info.Shape.Rows+1, len(info.Shape.Cols)142+ d.Header, d.Columns = 1, append([]string{}, info.Shape.Cols...)143+ }140144 return d141145 }142146 @@ -165,6 +169,9 @@ func describeRegion(s *sheet.Sheet, r sheet.Region) RegionDescription {
165169 if r.Linked() {166170 d.Kind, d.File = "linked", r.File.Path167171 }172+ if r.File.Paged {173+ d.Kind, d.File = "source", r.File.Path174+ }168175 return d169176 }170177 @@ -46,7 +46,7 @@ func TestDescribeFixture(t *testing.T) {
4646 if err := json.Unmarshal(js.Bytes(), &back); err != nil {4747 t.Fatal(err)4848 }49- if len(back.Sheets) != len(d.Sheets) || back.Sheets[0].Charts[2].Type != "pie" || back.Sheets[4].Notebook[1].Name != "files" {49+ if len(back.Sheets) != len(d.Sheets) || back.Sheets[0].Charts[2].Type != "pie" || back.Sheets[6].Notebook[1].Name != "files" {5050 t.Errorf("JSON round trip lost something: %+v", back.Sheets[0])5151 }5252 var nu bytes.Buffer@@ -81,9 +81,12 @@ func describeParts(b *strings.Builder, s SheetDescription) {
8181 fmt.Fprintf(b, " table %s %s %s\n", t.Name, t.Range, strings.Join(t.Columns, ", "))8282 }8383 for _, r := range s.Regions {84- if r.Kind == "linked" {84+ switch r.Kind {85+ case "linked":8586 fmt.Fprintf(b, " linked %s %s following %s\n", r.Name, r.Range, r.File)86- } else {87+ case "source":88+ fmt.Fprintf(b, " source %s %s reading %s in place\n", r.Name, r.Range, r.File)89+ default:8790 fmt.Fprintf(b, " output %s %s\n", r.Name, r.Range)8891 }8992 }@@ -8,6 +8,7 @@
88 package headless99 1010 import (11+ "context"1112 "errors"1213 "fmt"1314 "io"@@ -52,6 +53,7 @@ func Open(path string, create bool) (*File, error) {
5253 return nil, fmt.Errorf("%s: %w", path, err)5354 }5455 SyncOutputs(f.Book)56+ f.ReadSources(context.Background())5557 return f, nil5658 }5759 @@ -0,0 +1,36 @@
1+package headless2+3+import (4+ "context"5+ "path/filepath"6+ "strings"7+8+ "github.com/FelineStateMachine/012/internal/paged"9+ "github.com/FelineStateMachine/012/internal/sheet"10+)11+12+// ReadSources answers what the workbook's formulas and pivot tables ask13+// of its linked sources, reading them to the end before it returns, as14+// the screen does in the background. Only sources in the workbook's15+// folder or below are read: a path out of it (absolute, or through ..)16+// shows why it wasn't, as the screen asks first about such paths in a17+// workbook from another computer.18+func (f *File) ReadSources(ctx context.Context) {19+ w := f.Book20+ if len(w.Sources()) == 0 {21+ return22+ }23+ dir := filepath.Dir(f.Path)24+ h := paged.NewHost(sheet.MaxCells())25+ defer h.Close()26+ var inside []paged.Linked27+ for _, l := range paged.Links(w, func(p string) string { return p }, "") {28+ if p := l.Spec.Path; filepath.IsAbs(p) || p == ".." || strings.HasPrefix(p, ".."+string(filepath.Separator)) {29+ w.SetSourceShape(l.Name, sheet.SourceShape{}, "Not read: outside the workbook's folder")30+ continue31+ }32+ l.Spec.Path = filepath.Join(dir, l.Spec.Path)33+ inside = append(inside, l)34+ }35+ h.Settle(ctx, w, inside)36+}@@ -0,0 +1,54 @@
1+package headless2+3+import (4+ "database/sql"5+ "os"6+ "path/filepath"7+ "testing"8+9+ "github.com/FelineStateMachine/012/internal/sheet"10+)11+12+func TestOpenReadsSources(t *testing.T) {13+ dir := t.TempDir()14+ db, err := sql.Open("sqlite", filepath.Join(dir, "n.sqlite"))15+ if err != nil {16+ t.Fatal(err)17+ }18+ for _, q := range []string{"CREATE TABLE n (x INTEGER)", "WITH RECURSIVE c(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM c WHERE i < 100) INSERT INTO n SELECT i FROM c"} {19+ if _, err := db.Exec(q); err != nil {20+ t.Fatal(err)21+ }22+ }23+ db.Close()24+ w := sheet.NewBook()25+ s := w.Sheet(0)26+ if _, err := w.AddSource("n", sheet.LinkSource{Path: "n.sqlite", Table: "n"}, s); err != nil {27+ t.Fatal(err)28+ }29+ if _, err := w.AddSource("far", sheet.LinkSource{Path: "../far.sqlite"}, s); err != nil {30+ t.Fatal(err)31+ }32+ s.Set(sheet.Addr{}, "=SUM(n[x])")33+ s.Set(sheet.Addr{Row: 1}, "=ROWS(far)")34+ path := filepath.Join(dir, "book.012")35+ out, err := os.Create(path)36+ if err != nil {37+ t.Fatal(err)38+ }39+ if err := w.Write(out); err != nil {40+ t.Fatal(err)41+ }42+ out.Close()43+ f, err := Open(path, false)44+ if err != nil {45+ t.Fatal(err)46+ }47+ got := f.Book.Sheet(0)48+ if v := got.Value(sheet.Addr{}); v.Num != 5050 {49+ t.Errorf("SUM over the source: %v", v)50+ }51+ if why := got.ExplainError(sheet.Addr{Row: 1}); why != "far: Not read: outside the workbook's folder" {52+ t.Errorf("a source outside the folder: %q", why)53+ }54+}@@ -83,7 +83,7 @@ func linked(t *testing.T, name string, budget int) (*sheet.Workbook, *Host) {
8383 t.Fatal(err)8484 }8585 h := NewHost(budget)86- h.Settle(context.Background(), w, func(p string) string { return p })86+ h.Settle(context.Background(), w, Links(w, same, ""))8787 return w, h8888 }8989 @@ -119,7 +119,7 @@ func TestFormulasOverASource(t *testing.T) {
119119 t.Fatalf("%s: %v", f, err)120120 }121121 }122- h.Settle(context.Background(), w, func(p string) string { return p })122+ h.Settle(context.Background(), w, Links(w, same, ""))123123 for i, f := range sameFormulas {124124 got, want := s.Value(sheet.Addr{Col: 10, Row: i}), s.Value(sheet.Addr{Col: 11, Row: i})125125 if testing.Verbose() {@@ -174,7 +174,7 @@ func TestPastTheBudget(t *testing.T) {
174174 med, sum := sheet.Addr{Col: 10}, sheet.Addr{Col: 10, Row: 1}175175 s.Set(med, "=MEDIAN(sales[amount])")176176 s.Set(sum, "=SUM(sales[amount])")177- h.Settle(context.Background(), w, func(p string) string { return p })177+ h.Settle(context.Background(), w, Links(w, same, ""))178178 if v := s.Value(med); v != sheet.ErrValue {179179 t.Errorf("MEDIAN past the budget: %v, want #VALUE!", v)180180 }@@ -208,7 +208,7 @@ func TestPivotOverASource(t *testing.T) {
208208 if err != nil {209209 t.Fatal(err)210210 }211- h.Settle(context.Background(), w, func(p string) string { return p })211+ h.Settle(context.Background(), w, Links(w, same, ""))212212 wb, _ := want.UsedRange()213213 gb, _ := got.UsedRange()214214 if wb != gb {@@ -232,7 +232,7 @@ func TestSourceChanges(t *testing.T) {
232232 s := w.Sheet(0)233233 a := sheet.Addr{Col: 10}234234 s.Set(a, "=SUM(sales[amount])")235- h.Settle(context.Background(), w, func(p string) string { return p })235+ h.Settle(context.Background(), w, Links(w, same, ""))236236 before := s.Value(a)237237 now := time.Now()238238 h.Now = func() time.Time { return now }@@ -244,7 +244,7 @@ func TestSourceChanges(t *testing.T) {
244244 if !h.Poll() {245245 t.Fatal("a changed file isn't read again")246246 }247- h.Settle(context.Background(), w, func(p string) string { return p })247+ h.Settle(context.Background(), w, Links(w, same, ""))248248 after := s.Value(a)249249 if after.Kind != sheet.Number || after.Num-before.Num < 80 {250250 t.Errorf("SUM %v before the file changed, %v after", before, after)@@ -259,7 +259,7 @@ func TestMissingSource(t *testing.T) {
259259 }260260 a := sheet.Addr{}261261 s.Set(a, "=SUM(gone[x])")262- NewHost(1000).Settle(context.Background(), w, func(p string) string { return p })262+ NewHost(1000).Settle(context.Background(), w, Links(w, same, ""))263263 if v := s.Value(a); v != sheet.ErrRef {264264 t.Errorf("a missing source: %v, want #REF!", v)265265 }@@ -267,3 +267,6 @@ func TestMissingSource(t *testing.T) {
267267 t.Errorf("explained as %q", why)268268 }269269 }270+271+// same is a path as it is: the tests' files are named whole.272+func same(p string) string { return p }@@ -32,11 +32,11 @@ func Apply(w *sheet.Workbook, d Done) {
3232 }3333 }3434 35-// Settle links the workbook's sources and runs every job the host36-// queues, one after another, applying each, until none is left: for35+// Settle links the sources links names (Links) and runs every job the36+// host queues, one after another, applying each, until none is left: for3737 // what has no screen to keep live while they run (012 get, tests).38-func (h *Host) Settle(ctx context.Context, w *sheet.Workbook, abs func(string) string) {39- h.Link(Links(w, abs, ""))38+func (h *Host) Settle(ctx context.Context, w *sheet.Workbook, links []Linked) {39+ h.Link(links)4040 w.SetSources(h)4141 for jobs := h.Jobs(); len(jobs) > 0; jobs = h.Jobs() {4242 for _, j := range jobs {