package ledger import ( "encoding/base64" "fmt" "os" "path" "path/filepath" "testing" ) func TestFileAdd(t *testing.T) { filesAppendDelim = " " payee := "name:3" delta := Delta{ Date: "2999-88-77", Description: "66", Name: "name:1", Value: 2.00, Currency: USD, } cases := map[string]struct { given []byte want string }{ "no file": { given: nil, want: ` 2999-88-77 66 name:1 $2.00 name:3`, }, "empty file": { given: []byte{}, want: ` 2999-88-77 66 name:1 $2.00 name:3`, }, "happy without trailing whitespace": { given: []byte(` 2000-01-02 desc name:1 $1.00 name:2 $-1.00`), want: ` 2000-01-02 desc name:1 $1.00 name:2 $-1.00 2999-88-77 66 name:1 $2.00 name:3`, }, "happy with trailing newline": { given: []byte(` 2000-01-02 desc name:1 $1.00 name:2 $-1.00 `), want: ` 2000-01-02 desc name:1 $1.00 name:2 $-1.00 2999-88-77 66 name:1 $2.00 name:3`, }, } for name, d := range cases { c := d t.Run(name, func(t *testing.T) { p := path.Join(t.TempDir(), "input") if c.given != nil { if err := os.WriteFile(p, []byte(c.given), os.ModePerm); err != nil { t.Fatal(err) } } f, err := NewFiles(p) if err != nil { t.Fatal(err) } if err := f.Add(payee, delta); err != nil { t.Fatal(err) } if got, err := os.ReadFile(p); err != nil { t.Fatal(err) } else if string(got) != c.want { t.Errorf("wanted\n\t%s, got\n\t%s", c.want, got) } }) } } func TestFileTestdataMacroWithBPI(t *testing.T) { paths, err := filepath.Glob("./testdata/macro.d/*") if err != nil { t.Fatal(err) } t.Log(paths) f, err := NewFiles(paths[0], paths[1:]...) if err != nil { t.Fatal(err) } deltas, err := f.Deltas() if err != nil { t.Fatal(err) } bpis, err := NewBPIs("./testdata/bpi.bpi") if err != nil { t.Fatal(err) } t.Run("bal like", func(t *testing.T) { bal := deltas.Like(LikeName(`^AssetAccount:Bond`)).Balances().WithBPIs(bpis) for k, v := range bal { t.Logf("%s: %+v", k, v) } }) t.Run("reg like", func(t *testing.T) { reg := deltas.Like(LikeName(`^AssetAccount:Bond`)).Register() for k, v := range reg { t.Logf("%s: %+v", k, v.WithBPIs(bpis)) } }) } func TestFileTestdata(t *testing.T) { t.Run("macro.d", func(t *testing.T) { paths, err := filepath.Glob("./testdata/macro.d/*") if err != nil { t.Fatal(err) } f, err := NewFiles(paths[0], paths[1:]...) if err != nil { t.Fatal(err) } deltas, err := f.Deltas() if err != nil { t.Fatal(err) } t.Run("deltas", func(t *testing.T) { for i := range deltas { t.Logf("%+v", deltas[i].Debug()) } }) t.Run("balances", func(t *testing.T) { balances := deltas.Balances() for k, v := range balances { t.Logf("%s: %+v", k, v) } }) t.Run("balances like", func(t *testing.T) { balances := deltas.Like(LikeName(`^AssetAccount:`)).Balances() for k, v := range balances { t.Logf("%s: %+v", k, v) } }) }) t.Run("single files", func(t *testing.T) { paths, err := filepath.Glob("./testdata/*.dat") if err != nil { t.Fatal(err) } for _, pathd := range paths { path := pathd t.Run(path, func(t *testing.T) { f, err := NewFiles(path) if err != nil { t.Fatal(err) } deltas, err := f.Deltas() if err != nil { t.Fatal(err) } t.Run("deltas", func(t *testing.T) { for i := range deltas { t.Logf("%+v", deltas[i].Debug()) } }) t.Run("balances", func(t *testing.T) { balances := deltas.Balances() for k, v := range balances { t.Logf("%s: %+v", k, v) } }) t.Run("balances like", func(t *testing.T) { balances := deltas.Like(LikeName(`AssetAccount:Cash:Fidelity76`)).Balances() for k, v := range balances { t.Logf("%s: %+v", k, v) } }) }) } }) } func TestFileDeltas(t *testing.T) { happy := []Delta{ { Date: "2022-12-12", Name: "AssetAccount:Cash:Fidelity76", Value: -97.92, Currency: USD, Description: "Electricity / Power Bill TG2PJ-2PLP5", }, { Date: "2022-12-12", Name: "Withdrawal:0:SharedHome:DominionEnergy", Value: 97.92, Currency: USD, Description: "Electricity / Power Bill TG2PJ-2PLP5", }, { Date: "2022-12-12", Name: "AssetAccount:Cash:Fidelity76", Value: -1.00, Currency: USD, Description: "Test pay chase TG32S-BT2FF", }, { Date: "2022-12-12", Name: "Debts:Credit:ChaseFreedomUltdVisa", Value: 1.00, Currency: USD, Description: "Test pay chase TG32S-BT2FF", }, } cases := map[string][]Delta{ "empty": nil, "one": happy[:2], "happy": happy[:], } for name, d := range cases { want := d t.Run(name, func(t *testing.T) { f, err := NewFiles("./testdata/" + name + ".dat") if err != nil { t.Fatal(err) } deltas, err := f.Deltas() if err != nil { t.Fatal(err) } if len(deltas) != len(want) { t.Error(len(deltas)) } for i := range want { if i >= len(deltas) { break } if want[i] != deltas[i] { t.Errorf("[%d] \n\twant=%s, \n\t got=%s", i, want[i].Debug(), deltas[i].Debug()) } } }) } } func TestFilesOfDir(t *testing.T) { d := t.TempDir() files := Files([]string{d, "/dev/null"}) if paths := files.paths(); len(paths) != 1 { t.Error(paths) } os.WriteFile(path.Join(d, "1"), []byte{}, os.ModePerm) os.Mkdir(path.Join(d, "d2"), os.ModePerm) os.WriteFile(path.Join(d, "d2", "2"), []byte{}, os.ModePerm) if paths := files.paths(); len(paths) != 3 || paths[0] != path.Join(d, "1") || paths[1] != path.Join(d, "d2", "2") { t.Error(paths) } } func TestFilesTempGetLastNLines(t *testing.T) { cases := map[string]struct { input string n int want []string }{ "empty": {}, "get n lines from empty file": { input: "", n: 5, want: []string{}, }, "get 0 lines from file": { input: "#a\n#b", n: 0, want: []string{}, }, "get 3 lines from 2 line file": { input: "#a\n#b", n: 3, want: []string{"#a", "#b"}, }, "get 2 lines from 3 line file": { input: "#a\n#b\n#c", n: 2, want: []string{"#b", "#c"}, }, } for name, d := range cases { c := d t.Run(name, func(t *testing.T) { p := path.Join(t.TempDir(), base64.URLEncoding.EncodeToString([]byte(t.Name()))) os.WriteFile(p, []byte(c.input), os.ModePerm) files, err := NewFiles(p) if err != nil { panic(err) } if got, err := files.TempGetLastNLines(c.n); err != nil { t.Fatal(err) } else if fmt.Sprint(got) != fmt.Sprint(c.want) { for i := range c.want { t.Logf("want[%d] = %q", i, c.want[i]) } for i := range got { t.Logf(" got[%d] = %q", i, got[i]) } t.Errorf("wanted\n\t%+v, got\n\t%+v", c.want, got) } }) } } func TestFilesTempSetLastNLines(t *testing.T) { cases := map[string]struct { given string input []string n int want string }{ "empty": {}, "append to empty": { input: []string{"hello", "world"}, n: 100, want: "hello\nworld\n", }, "replace last 10 of 1 lines with 2": { given: "ohno", input: []string{"hello", "world"}, n: 10, want: "hello\nworld\n", }, "replace last 1 of 1 lines with 2": { given: "ohno", input: []string{"hello", "world"}, n: 1, want: "hello\nworld\n", }, "replace last 0 of 1 lines with 2": { given: "ohno", input: []string{"hello", "world"}, n: 0, want: "ohno\nhello\nworld\n", }, "replace last 1 of 1 lines with 0": { given: "ohno", input: []string{}, n: 1, want: "", }, "replace last 1 of 2 lines with 1": { given: "ohno\nhaha", input: []string{"replaced"}, n: 1, want: "ohno\nreplaced\n", }, "replace last 1 of 2 lines with 2": { given: "ohno\nhaha", input: []string{"replac", "ed"}, n: 1, want: "ohno\nreplac\ned\n", }, } for name, d := range cases { c := d t.Run(name, func(t *testing.T) { p := path.Join(t.TempDir(), base64.URLEncoding.EncodeToString([]byte(t.Name()))) os.WriteFile(p, []byte(c.given), os.ModePerm) files := Files([]string{p}) if err := files.TempSetLastNLines(c.n, c.input); err != nil { s := err.Error() if _, err := os.Stat(s); err == nil { got, _ := os.ReadFile(s) if string(got) != c.want { t.Errorf("want\n%s, got\n%s", c.want, got) } } t.Fatal(err) } got, _ := os.ReadFile(p) if string(got) != c.want { t.Errorf("want\n%s, got\n%s", c.want, got) } }) } }