diff --git a/go.mod b/go.mod index 646f7569..06728d6f 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,11 @@ go 1.22.1 require ( github.com/antlr4-go/antlr/v4 v4.13.1 github.com/gkampitakis/go-snaps v0.5.4 + github.com/sergi/go-diff v1.0.0 ) +require gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect + require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect diff --git a/go.sum b/go.sum index d5c7b793..94969ab7 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,8 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -57,7 +59,8 @@ golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/ansi/ansi.go b/internal/ansi/ansi.go index 77eb617b..81227cbe 100644 --- a/internal/ansi/ansi.go +++ b/internal/ansi/ansi.go @@ -1,18 +1,48 @@ package ansi -import "fmt" +import ( + "fmt" + "strings" +) const resetCol = "\033[0m" +func Compose(cols ...func(string) string) func(string) string { + return func(s string) string { + for _, mod := range cols { + s = mod(s) + } + return s + } +} + +func replaceLast(s, oldStr, newStr string) string { + lastIndex := strings.LastIndex(s, oldStr) + if lastIndex == -1 { + return s + } + return s[:lastIndex] + newStr + s[lastIndex+len(oldStr):] +} + func col(s string, code int) string { - c := fmt.Sprintf("\033[%dm", code) - return c + s + resetCol + colorCode := fmt.Sprintf("\033[%dm", code) + // This trick should allow to stack colors (TODO test) + s = replaceLast(s, resetCol, resetCol+colorCode) + return colorCode + s + resetCol } func ColorRed(s string) string { return col(s, 31) } +func ColorWhite(s string) string { + return col(s, 37) +} + +func ColorGreen(s string) string { + return col(s, 32) +} + func ColorYellow(s string) string { return col(s, 33) } @@ -20,3 +50,46 @@ func ColorYellow(s string) string { func ColorCyan(s string) string { return col(s, 36) } + +func ColorLight(s string) string { + return col(s, 97) // Bright white → light +} + +func ColorBrightBlack(s string) string { + return col(s, 90) +} + +func ColorBrightRed(s string) string { + return col(s, 91) +} + +func ColorBrightGreen(s string) string { + return col(s, 92) +} + +func ColorBrightYellow(s string) string { + return col(s, 93) +} + +// BG +func BgDark(s string) string { + return col(s, 100) +} + +func BgRed(s string) string { + return col(s, 41) +} + +func BgGreen(s string) string { + return col(s, 42) +} + +// modifiers + +func Bold(s string) string { + return col(s, 1) +} + +func Underline(s string) string { + return col(s, 4) +} diff --git a/internal/cmd/root.go b/internal/cmd/root.go index b19ff0f1..557ad454 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -25,6 +25,7 @@ func Execute(options CliOptions) { rootCmd.AddCommand(lspCmd) rootCmd.AddCommand(checkCmd) + rootCmd.AddCommand(getTestCmd()) rootCmd.AddCommand(getRunCmd()) if err := rootCmd.Execute(); err != nil { diff --git a/internal/cmd/run.go b/internal/cmd/run.go index 0374723f..f6d74535 100644 --- a/internal/cmd/run.go +++ b/internal/cmd/run.go @@ -8,7 +8,6 @@ import ( "os" "strings" - "github.com/formancehq/numscript/internal/ansi" "github.com/formancehq/numscript/internal/flags" "github.com/formancehq/numscript/internal/interpreter" "github.com/formancehq/numscript/internal/parser" @@ -21,7 +20,7 @@ const ( OutputFormatJson = "json" ) -type Args struct { +type runArgs struct { VariablesOpt string BalancesOpt string MetaOpt string @@ -38,7 +37,7 @@ type inputOpts struct { Balances interpreter.Balances `json:"balances"` } -func (o *inputOpts) fromRaw(opts Args) { +func (o *inputOpts) fromRaw(opts runArgs) { if opts.RawOpt == "" { return } @@ -49,7 +48,7 @@ func (o *inputOpts) fromRaw(opts Args) { } } -func (o *inputOpts) fromStdin(opts Args) { +func (o *inputOpts) fromStdin(opts runArgs) { if !opts.StdinFlag { return } @@ -65,7 +64,7 @@ func (o *inputOpts) fromStdin(opts Args) { } } -func (o *inputOpts) fromOptions(path string, opts Args) { +func (o *inputOpts) fromOptions(path string, opts runArgs) { if path != "" { numscriptContent, err := os.ReadFile(path) if err != nil { @@ -103,7 +102,7 @@ func (o *inputOpts) fromOptions(path string, opts Args) { } } -func run(path string, opts Args) { +func run(path string, opts runArgs) { opt := inputOpts{ Variables: make(map[string]string), Meta: make(interpreter.AccountsMetadata), @@ -164,25 +163,19 @@ func showJson(result *interpreter.ExecutionResult) { } func showPretty(result *interpreter.ExecutionResult) { - fmt.Println(ansi.ColorCyan("Postings:")) - postingsJson, err := json.MarshalIndent(result.Postings, "", " ") - if err != nil { - panic(err) - } - fmt.Println(string(postingsJson)) - fmt.Println() + fmt.Println("Postings:") + fmt.Println(interpreter.PrettyPrintPostings(result.Postings)) - fmt.Println(ansi.ColorCyan("Meta:")) - txMetaJson, err := json.MarshalIndent(result.Metadata, "", " ") - if err != nil { - panic(err) + if len(result.Metadata) != 0 { + fmt.Println("Meta:") + fmt.Println(interpreter.PrettyPrintMeta(result.Metadata)) } - fmt.Println(string(txMetaJson)) + } func getRunCmd() *cobra.Command { - opts := Args{} + opts := runArgs{} cmd := cobra.Command{ Use: "run", diff --git a/internal/cmd/test.go b/internal/cmd/test.go new file mode 100644 index 00000000..9436be1b --- /dev/null +++ b/internal/cmd/test.go @@ -0,0 +1,398 @@ +package cmd + +import ( + "bufio" + "encoding/json" + "fmt" + "os" + "path/filepath" + "slices" + "strings" + + "github.com/formancehq/numscript/internal/ansi" + "github.com/formancehq/numscript/internal/interpreter" + "github.com/formancehq/numscript/internal/parser" + "github.com/formancehq/numscript/internal/specs_format" + "github.com/formancehq/numscript/internal/utils" + "github.com/spf13/cobra" + + "github.com/sergi/go-diff/diffmatchpatch" +) + +func showDiff(expected_ any, got_ any) { + dmp := diffmatchpatch.New() + + expected, _ := json.MarshalIndent(expected_, "", " ") + actual, _ := json.MarshalIndent(got_, "", " ") + + aChars, bChars, lineArray := dmp.DiffLinesToChars(string(expected), string(actual)) + diffs := dmp.DiffMain(aChars, bChars, true) + diffs = dmp.DiffCharsToLines(diffs, lineArray) + + for _, diff := range diffs { + lines := strings.Split(diff.Text, "\n") + for _, line := range lines { + if line == "" { + continue + } + switch diff.Type { + case diffmatchpatch.DiffDelete: + fmt.Println(ansi.ColorGreen("- " + line)) + case diffmatchpatch.DiffInsert: + fmt.Println(ansi.ColorRed("+ " + line)) + case diffmatchpatch.DiffEqual: + fmt.Println(ansi.ColorBrightBlack(" " + line)) + } + } + } +} + +func fixSnapshot(testResult testResult, failedAssertion specs_format.AssertionMismatch[any]) bool { + if !opts.interactive { + return false + } + + fmt.Println(ansi.ColorBrightBlack( + fmt.Sprintf("\nPress %s to update snapshot, %s to go the the next one", + ansi.ColorBrightYellow("u"), + ansi.ColorBrightYellow("n"), + ))) + + reader := bufio.NewReader(os.Stdin) + line, _, err := reader.ReadLine() + if err != nil { + panic(err) + } + + switch string(line) { + case "u": + testResult.Specs.TestCases = utils.Map(testResult.Specs.TestCases, func(t specs_format.TestCase) specs_format.TestCase { + // TODO check there are no duplicate "It" + if t.It == testResult.Result.It { + switch failedAssertion.Expected { + case "expect.postings": + t.ExpectPostings = failedAssertion.Expected.([]interpreter.Posting) + + default: + panic("TODO implement") + + } + + } + + return t + }) + + newSpecs, err := json.MarshalIndent(testResult.Specs, "", " ") + if err != nil { + panic(err) + } + + err = os.WriteFile(testResult.File, newSpecs, os.ModePerm) + if err != nil { + panic(err) + } + return true + + case "n": + return false + + default: + panic("TODO invalid command") + } +} + +func showFailingTestCase(testResult testResult) (rerun bool) { + specsFilePath := testResult.File + result := testResult.Result + + if result.Pass { + return false + } + + fmt.Print("\n\n") + + failColor := ansi.Compose(ansi.BgRed, ansi.ColorLight, ansi.Bold) + fmt.Print(failColor(" FAIL ")) + fmt.Println(ansi.ColorRed(" " + specsFilePath + " > " + result.It)) + + showGiven := len(result.Balances) != 0 || len(result.Meta) != 0 || len(result.Vars) != 0 + if showGiven { + fmt.Println(ansi.Underline("\nGIVEN:")) + } + + if len(result.Balances) != 0 { + fmt.Println() + fmt.Println(result.Balances.PrettyPrint()) + fmt.Println() + } + + if len(result.Meta) != 0 { + fmt.Println() + fmt.Println(result.Meta.PrettyPrint()) + fmt.Println() + } + + if len(result.Vars) != 0 { + fmt.Println() + fmt.Println(utils.CsvPrettyMap("Name", "Value", result.Vars)) + fmt.Println() + } + + fmt.Println() + fmt.Println(ansi.ColorGreen("- Expected")) + fmt.Println(ansi.ColorRed("+ Received\n")) + + for _, failedAssertion := range result.FailedAssertions { + + fmt.Println(ansi.Underline(failedAssertion.Assertion)) + fmt.Println() + showDiff(failedAssertion.Expected, failedAssertion.Got) + + rerun := fixSnapshot(testResult, failedAssertion) + if rerun { + return true + } + + } + + return false +} + +func test(specsFilePath string) (specs_format.Specs, specs_format.SpecsResult) { + if !strings.HasSuffix(specsFilePath, ".num.specs.json") { + panic("Wrong name") + } + + numscriptFileName := strings.TrimSuffix(specsFilePath, ".specs.json") + + numscriptContent, err := os.ReadFile(numscriptFileName) + if err != nil { + os.Stderr.Write([]byte(err.Error())) + os.Exit(1) + } + + parseResult := parser.Parse(string(numscriptContent)) + // TODO assert no parse err + // TODO we might want to do static checking + + specsFileContent, err := os.ReadFile(specsFilePath) + if err != nil { + os.Stderr.Write([]byte(err.Error())) + os.Exit(1) + } + + var specs specs_format.Specs + err = json.Unmarshal([]byte(specsFileContent), &specs) + if err != nil { + os.Stderr.Write([]byte(ansi.ColorRed(err.Error()))) + os.Exit(1) + } + + out, iErr := specs_format.Check(parseResult.Value, specs) + if iErr != nil { + rng := iErr.GetRange() + + errFile := fmt.Sprintf("\nError: %s:%d:%d\n\n", numscriptFileName, rng.Start.Line+1, rng.Start.Character+1) + + os.Stderr.Write([]byte(ansi.ColorRed(errFile))) + + os.Stderr.Write([]byte(iErr.Error())) + if rng.Start != rng.End { + os.Stderr.Write([]byte("\n")) + os.Stderr.Write([]byte(iErr.GetRange().ShowOnSource(parseResult.Source))) + } + os.Exit(1) + } + + if out.Total == 0 { + fmt.Println(ansi.ColorRed("Empty test suite: " + specsFilePath)) + os.Exit(1) + } else if out.Failing == 0 { + testsCount := ansi.ColorBrightBlack(fmt.Sprintf("(%d tests)", out.Total)) + fmt.Printf("%s %s %s\n", ansi.ColorGreen("✓"), numscriptFileName, testsCount) + } else { + failedTestsCount := ansi.ColorRed(fmt.Sprintf("%d failed", out.Failing)) + + testsCount := ansi.ColorBrightBlack(fmt.Sprintf("(%d tests | %s)", out.Total, failedTestsCount)) + fmt.Printf("%s %s %s\n", ansi.ColorRed("❯"), numscriptFileName, testsCount) + + for _, result := range out.Cases { + if result.Pass { + continue + } + + fmt.Printf(" %s %s\n", ansi.ColorRed("×"), result.It) + } + + } + + return specs, out +} + +type testResult struct { + Specs specs_format.Specs + File string + Result specs_format.TestCaseResult +} + +func testPaths() { + testFiles := 0 + failedTestFiles := 0 + + var allTests []testResult + for _, path := range opts.paths { + path = strings.TrimSuffix(path, "/") + + glob := fmt.Sprintf(path + "/*.num.specs.json") + + files, err := filepath.Glob(glob) + if err != nil { + panic(err) + } + testFiles += len(files) + + for _, file := range files { + specs, out := test(file) + + for _, testCase := range out.Cases { + allTests = append(allTests, testResult{ + Specs: specs, + File: file, + Result: testCase, + }) + } + + // Count tests + isTestFailed := slices.ContainsFunc(out.Cases, func(tc specs_format.TestCaseResult) bool { + return tc.Pass + }) + if isTestFailed { + failedTestFiles += 1 + } + } + } + + for _, test_ := range allTests { + rerun := showFailingTestCase(test_) + if rerun { + fmt.Print("\033[H\033[2J") + testPaths() + return + } + } + + // Stats + printFilesStats(allTests) + +} + +func printFilesStats(allTests []testResult) { + failedTests := utils.Filter(allTests, func(t testResult) bool { + return !t.Result.Pass + }) + + testFilesLabel := "Test files" + testsLabel := "Tests" + + paddedLabel := func(s string) string { + maxLen := max(len(testFilesLabel), len(testsLabel)) // yeah, ok, this could be hardcoded, I know + return ansi.ColorBrightBlack(fmt.Sprintf(" %*s ", maxLen, s)) + } + + fmt.Println() + + // Files stats + { + filesCount := len(slices.CompactFunc(allTests, func(t1 testResult, t2 testResult) bool { + return t1.File == t2.File + })) + failedTestsFilesCount := len(slices.CompactFunc(failedTests, func(t1 testResult, t2 testResult) bool { + return t1.File == t2.File + })) + passedTestsFilesCount := filesCount - failedTestsFilesCount + + var testFilesUIParts []string + if failedTestsFilesCount != 0 { + testFilesUIParts = append(testFilesUIParts, + ansi.Compose(ansi.ColorBrightRed, ansi.Bold)(fmt.Sprintf("%d failed", failedTestsFilesCount)), + ) + } + if passedTestsFilesCount != 0 { + testFilesUIParts = append(testFilesUIParts, + ansi.Compose(ansi.ColorBrightGreen, ansi.Bold)(fmt.Sprintf("%d passed", passedTestsFilesCount)), + ) + } + testFilesUI := strings.Join(testFilesUIParts, ansi.ColorBrightBlack(" | ")) + totalTestFilesUI := ansi.ColorBrightBlack(fmt.Sprintf("(%d)", filesCount)) + fmt.Print(paddedLabel(testFilesLabel) + " " + testFilesUI + " " + totalTestFilesUI) + } + + fmt.Println() + + // Tests stats + { + + testsCount := len(allTests) + failedTestsCount := len(failedTests) + passedTestsCount := testsCount - failedTestsCount + + var testUIParts []string + if failedTestsCount != 0 { + testUIParts = append(testUIParts, + ansi.Compose(ansi.ColorBrightRed, ansi.Bold)(fmt.Sprintf("%d failed", failedTestsCount)), + ) + } + if passedTestsCount != 0 { + testUIParts = append(testUIParts, + ansi.Compose(ansi.ColorBrightGreen, ansi.Bold)(fmt.Sprintf("%d passed", passedTestsCount)), + ) + } + + testsUI := strings.Join(testUIParts, ansi.ColorBrightBlack(" | ")) + totalTestsUI := ansi.ColorBrightBlack(fmt.Sprintf("(%d)", testsCount)) + + fmt.Println(paddedLabel(testsLabel) + " " + testsUI + " " + totalTestsUI) + + if failedTestsCount != 0 { + os.Exit(1) + } + } + +} + +type testArgs struct { + paths []string + interactive bool +} + +var opts = testArgs{} + +func getTestCmd() *cobra.Command { + + cmd := &cobra.Command{ + Use: "test folder...", + Short: "Test numscript file using the numscript specs format", + Long: `Searches for any .num.specs files in the given directory (or directories), +and tests the corresponding .num file (if any). +Defaults to "." if there are no given paths`, + Args: cobra.MatchAll(), + Run: func(cmd *cobra.Command, paths []string) { + + if len(paths) == 0 { + paths = []string{"."} + } + + opts.paths = paths + testPaths() + }, + } + + // A poor man's feature flag + // that's a post-mvp feature so we'll keep it as dead code for now + if false { + cmd.Flags().BoolVar(&opts.interactive, "experimental-interactive", false, "Interactively update the expectations with the received value") + } + + return cmd +} diff --git a/internal/interpreter/__snapshots__/balances_test.snap b/internal/interpreter/__snapshots__/balances_test.snap new file mode 100755 index 00000000..220eb761 --- /dev/null +++ b/internal/interpreter/__snapshots__/balances_test.snap @@ -0,0 +1,7 @@ + +[TestPrettyPrintBalance - 1] +| Account | Asset | Balance | +| alice | EUR/2 | 1 | +| alice | USD/1234 | 999999 | +| bob | BTC | 3 | +--- diff --git a/internal/interpreter/accounts_metadata.go b/internal/interpreter/accounts_metadata.go new file mode 100644 index 00000000..a0cec91a --- /dev/null +++ b/internal/interpreter/accounts_metadata.go @@ -0,0 +1,50 @@ +package interpreter + +import ( + "github.com/formancehq/numscript/internal/utils" +) + +func (m AccountsMetadata) fetchAccountMetadata(account string) AccountMetadata { + return utils.MapGetOrPutDefault(m, account, func() AccountMetadata { + return AccountMetadata{} + }) +} + +func (m AccountsMetadata) DeepClone() AccountsMetadata { + cloned := make(AccountsMetadata) + for account, accountBalances := range m { + for asset, metadataValue := range accountBalances { + clonedAccountBalances := cloned.fetchAccountMetadata(account) + utils.MapGetOrPutDefault(clonedAccountBalances, asset, func() string { + return metadataValue + }) + } + } + return cloned +} + +func (m AccountsMetadata) Merge(update AccountsMetadata) { + for acc, accBalances := range update { + cachedAcc := utils.MapGetOrPutDefault(m, acc, func() AccountMetadata { + return AccountMetadata{} + }) + + for curr, amt := range accBalances { + cachedAcc[curr] = amt + } + } +} + +func (m AccountsMetadata) PrettyPrint() string { + header := []string{"Account", "Name", "Value"} + + var rows [][]string + for account, accMetadata := range m { + for name, value := range accMetadata { + row := []string{account, name, value} + rows = append(rows, row) + } + } + + return utils.CsvPretty(header, rows, true) +} diff --git a/internal/interpreter/balances.go b/internal/interpreter/balances.go index 89c1ba81..222eb865 100644 --- a/internal/interpreter/balances.go +++ b/internal/interpreter/balances.go @@ -7,18 +7,11 @@ import ( "github.com/formancehq/numscript/internal/utils" ) -func (b Balances) fetchAccountBalances(account string) AccountBalance { - return defaultMapGet(b, account, func() AccountBalance { - return AccountBalance{} - }) -} - -func (b Balances) deepClone() Balances { +func (b Balances) DeepClone() Balances { cloned := make(Balances) for account, accountBalances := range b { for asset, amount := range accountBalances { - clonedAccountBalances := cloned.fetchAccountBalances(account) - defaultMapGet(clonedAccountBalances, asset, func() *big.Int { + utils.NestedMapGetOrPutDefault(cloned, account, asset, func() *big.Int { return new(big.Int).Set(amount) }) } @@ -44,15 +37,13 @@ func coloredAsset(asset string, color *string) string { // Get the (account, asset) tuple from the Balances // if the tuple is not present, it will write a big.NewInt(0) in it and return it func (b Balances) fetchBalance(account string, uncoloredAsset string, color string) *big.Int { - accountBalances := b.fetchAccountBalances(account) - - return defaultMapGet(accountBalances, coloredAsset(uncoloredAsset, &color), func() *big.Int { + return utils.NestedMapGetOrPutDefault(b, account, coloredAsset(uncoloredAsset, &color), func() *big.Int { return new(big.Int) }) } func (b Balances) has(account string, asset string) bool { - accountBalances := defaultMapGet(b, account, func() AccountBalance { + accountBalances := utils.MapGetOrPutDefault(b, account, func() AccountBalance { return AccountBalance{} }) @@ -78,10 +69,10 @@ func (b Balances) filterQuery(q BalanceQuery) BalanceQuery { } // Merge balances by adding balances in the "update" arg -func (b Balances) mergeBalance(update Balances) { +func (b Balances) Merge(update Balances) { // merge queried balance for acc, accBalances := range update { - cachedAcc := defaultMapGet(b, acc, func() AccountBalance { + cachedAcc := utils.MapGetOrPutDefault(b, acc, func() AccountBalance { return AccountBalance{} }) @@ -90,3 +81,22 @@ func (b Balances) mergeBalance(update Balances) { } } } + +func (b Balances) PrettyPrint() string { + header := []string{"Account", "Asset", "Balance"} + + var rows [][]string + for account, accBalances := range b { + for asset, balance := range accBalances { + row := []string{account, asset, balance.String()} + rows = append(rows, row) + } + } + return utils.CsvPretty(header, rows, true) +} + +func CompareBalances(b1 Balances, b2 Balances) bool { + return utils.Map2Cmp(b1, b2, func(ab1, ab2 *big.Int) bool { + return ab1.Cmp(ab2) == 0 + }) +} diff --git a/internal/interpreter/balances_test.go b/internal/interpreter/balances_test.go index 5fe642a8..f41e24c9 100644 --- a/internal/interpreter/balances_test.go +++ b/internal/interpreter/balances_test.go @@ -4,6 +4,7 @@ import ( "math/big" "testing" + "github.com/gkampitakis/go-snaps/snaps" "github.com/stretchr/testify/require" ) @@ -41,9 +42,40 @@ func TestCloneBalances(t *testing.T) { }, } - cloned := fullBalance.deepClone() + cloned := fullBalance.DeepClone() fullBalance["alice"]["USD/2"].Set(big.NewInt(42)) require.Equal(t, big.NewInt(2), cloned["alice"]["USD/2"]) } + +func TestPrettyPrintBalance(t *testing.T) { + fullBalance := Balances{ + "alice": AccountBalance{ + "EUR/2": big.NewInt(1), + "USD/1234": big.NewInt(999999), + }, + "bob": AccountBalance{ + "BTC": big.NewInt(3), + }, + } + + snaps.MatchSnapshot(t, fullBalance.PrettyPrint()) +} + +func TestCmpMaps(t *testing.T) { + + b1 := Balances{ + "alice": AccountBalance{ + "EUR": big.NewInt(100), + }, + } + + b2 := Balances{ + "alice": AccountBalance{ + "EUR": big.NewInt(42), + }, + } + + require.Equal(t, false, CompareBalances(b1, b2)) +} diff --git a/internal/interpreter/batch_balances_query.go b/internal/interpreter/batch_balances_query.go index af1c58f3..57c4754c 100644 --- a/internal/interpreter/batch_balances_query.go +++ b/internal/interpreter/batch_balances_query.go @@ -77,7 +77,7 @@ func (st *programState) runBalancesQuery() error { // reset batch query st.CurrentBalanceQuery = BalanceQuery{} - st.CachedBalances.mergeBalance(queriedBalances) + st.CachedBalances.Merge(queriedBalances) return nil } diff --git a/internal/interpreter/function_statements.go b/internal/interpreter/function_statements.go index edb77dba..89179b78 100644 --- a/internal/interpreter/function_statements.go +++ b/internal/interpreter/function_statements.go @@ -1,6 +1,9 @@ package interpreter -import "github.com/formancehq/numscript/internal/parser" +import ( + "github.com/formancehq/numscript/internal/parser" + "github.com/formancehq/numscript/internal/utils" +) func setTxMeta(st *programState, r parser.Range, args []Value) InterpreterError { p := NewArgsParser(args) @@ -25,7 +28,7 @@ func setAccountMeta(st *programState, r parser.Range, args []Value) InterpreterE return err } - accountMeta := defaultMapGet(st.SetAccountsMeta, *account, func() AccountMetadata { + accountMeta := utils.MapGetOrPutDefault(st.SetAccountsMeta, *account, func() AccountMetadata { return AccountMetadata{} }) diff --git a/internal/interpreter/interpreter.go b/internal/interpreter/interpreter.go index 944b4487..136596d5 100644 --- a/internal/interpreter/interpreter.go +++ b/internal/interpreter/interpreter.go @@ -46,7 +46,10 @@ func (s StaticStore) GetBalances(_ context.Context, q BalanceQuery) (Balances, e outputAccountBalance := AccountBalance{} outputBalance[queriedAccount] = outputAccountBalance - accountBalanceLookup := s.Balances.fetchAccountBalances(queriedAccount) + accountBalanceLookup := utils.MapGetOrPutDefault(s.Balances, queriedAccount, func() AccountBalance { + return AccountBalance{} + }) + for _, curr := range queriedCurrencies { n := new(big.Int) outputAccountBalance[curr] = n @@ -599,7 +602,7 @@ func (s *programState) trySendingToAccount(accountLiteral parser.ValueExpr, amou func (s *programState) cloneState() func() { fsBackup := s.fundsStack.Clone() - balancesBackup := s.CachedBalances.deepClone() + balancesBackup := s.CachedBalances.DeepClone() return func() { s.fundsStack = fsBackup @@ -989,3 +992,21 @@ func CalculateSafeWithdraw( safe := CalculateMaxSafeWithdraw(balance, overdraft) return utils.MinBigInt(safe, requestedAmount) } + +func PrettyPrintPostings(postings []Posting) string { + var rows [][]string + for _, posting := range postings { + row := []string{posting.Source, posting.Destination, posting.Asset, posting.Amount.String()} + rows = append(rows, row) + } + return utils.CsvPretty([]string{"Source", "Destination", "Asset", "Amount"}, rows, false) +} + +func PrettyPrintMeta(meta Metadata) string { + m := map[string]string{} + for k, v := range meta { + m[k] = v.String() + } + + return utils.CsvPrettyMap("Name", "Value", m) +} diff --git a/internal/interpreter/script-tests/account-interp.num b/internal/interpreter/script-tests/account-interp.num new file mode 100644 index 00000000..ed6f6200 --- /dev/null +++ b/internal/interpreter/script-tests/account-interp.num @@ -0,0 +1,6 @@ +vars { + number $id + string $status + account $acc +} +set_tx_meta("k", @acc:$id:$status:$acc) diff --git a/internal/interpreter/script-tests/account-interp.num.specs.json b/internal/interpreter/script-tests/account-interp.num.specs.json new file mode 100644 index 00000000..2011c022 --- /dev/null +++ b/internal/interpreter/script-tests/account-interp.num.specs.json @@ -0,0 +1,17 @@ +{ + "$schema": "../../../specs.schema.json", + "featureFlags": ["experimental-account-interpolation"], + "testCases": [ + { + "it": "account interpolation", + "vars": { + "id": "42", + "status": "pending", + "acc": "user:001" + }, + "expect.txMeta": { + "k": "acc:42:pending:user:001" + } + } + ] +} diff --git a/internal/interpreter/script-tests/add-monetaries-same-currency.num b/internal/interpreter/script-tests/add-monetaries-same-currency.num new file mode 100644 index 00000000..47eb107f --- /dev/null +++ b/internal/interpreter/script-tests/add-monetaries-same-currency.num @@ -0,0 +1,4 @@ +send [COIN 1] + [COIN 2] ( + source = @world + destination = @dest +) diff --git a/internal/interpreter/script-tests/add-monetaries-same-currency.num.specs.json b/internal/interpreter/script-tests/add-monetaries-same-currency.num.specs.json new file mode 100644 index 00000000..454126e7 --- /dev/null +++ b/internal/interpreter/script-tests/add-monetaries-same-currency.num.specs.json @@ -0,0 +1,16 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "adds monetaries with same currency", + "expect.postings": [ + { + "asset": "COIN", + "amount": 3, + "source": "world", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/add-numbers.num b/internal/interpreter/script-tests/add-numbers.num new file mode 100644 index 00000000..dd9c45f7 --- /dev/null +++ b/internal/interpreter/script-tests/add-numbers.num @@ -0,0 +1 @@ +set_tx_meta("k", 1 + 2) diff --git a/internal/interpreter/script-tests/add-numbers.num.specs.json b/internal/interpreter/script-tests/add-numbers.num.specs.json new file mode 100644 index 00000000..260ac9b3 --- /dev/null +++ b/internal/interpreter/script-tests/add-numbers.num.specs.json @@ -0,0 +1,11 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "adds numbers", + "expect.txMeta": { + "k": "3" + } + } + ] +} diff --git a/internal/interpreter/script-tests/allocate-dont-take-too-much.num b/internal/interpreter/script-tests/allocate-dont-take-too-much.num new file mode 100644 index 00000000..7ae2c72d --- /dev/null +++ b/internal/interpreter/script-tests/allocate-dont-take-too-much.num @@ -0,0 +1,10 @@ +send [CREDIT 200] ( + source = { + @users:001 + @users:002 + } + destination = { + 1/2 to @foo + 1/2 to @bar + } +) diff --git a/internal/interpreter/script-tests/allocate-dont-take-too-much.num.specs.json b/internal/interpreter/script-tests/allocate-dont-take-too-much.num.specs.json new file mode 100644 index 00000000..44877673 --- /dev/null +++ b/internal/interpreter/script-tests/allocate-dont-take-too-much.num.specs.json @@ -0,0 +1,26 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "allocates without taking too much", + "balances": { + "users:001": { "CREDIT": 100 }, + "users:002": { "CREDIT": 110 } + }, + "expect.postings": [ + { + "asset": "CREDIT", + "amount": 100, + "source": "users:001", + "destination": "foo" + }, + { + "asset": "CREDIT", + "amount": 100, + "source": "users:002", + "destination": "bar" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/allocation.num b/internal/interpreter/script-tests/allocation.num new file mode 100644 index 00000000..5dc3a1e2 --- /dev/null +++ b/internal/interpreter/script-tests/allocation.num @@ -0,0 +1,12 @@ +vars { + account $rider + account $driver +} +send [GEM 15] ( + source = $rider + destination = { + 80% to $driver + 8% to @a + 12% to @b + } +) diff --git a/internal/interpreter/script-tests/allocation.num.specs.json b/internal/interpreter/script-tests/allocation.num.specs.json new file mode 100644 index 00000000..0217baae --- /dev/null +++ b/internal/interpreter/script-tests/allocation.num.specs.json @@ -0,0 +1,35 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "allocates funds with percentages", + "balances": { + "users:001": { "GEM": 15 } + }, + "vars": { + "rider": "users:001", + "driver": "users:002" + }, + "expect.postings": [ + { + "asset": "GEM", + "amount": 13, + "source": "users:001", + "destination": "users:002" + }, + { + "asset": "GEM", + "amount": 1, + "source": "users:001", + "destination": "a" + }, + { + "asset": "GEM", + "amount": 1, + "source": "users:001", + "destination": "b" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/ask-balance-twice.num b/internal/interpreter/script-tests/ask-balance-twice.num new file mode 100644 index 00000000..883a6a74 --- /dev/null +++ b/internal/interpreter/script-tests/ask-balance-twice.num @@ -0,0 +1,8 @@ +vars { + monetary $bal = balance(@alice, USD/2) +} + +send $bal ( + source = @alice + destination = @dest +) diff --git a/internal/interpreter/script-tests/ask-balance-twice.num.specs.json b/internal/interpreter/script-tests/ask-balance-twice.num.specs.json new file mode 100644 index 00000000..3995fa8f --- /dev/null +++ b/internal/interpreter/script-tests/ask-balance-twice.num.specs.json @@ -0,0 +1,19 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "asks balance twice", + "balances": { + "alice": { "USD/2": 10 } + }, + "expect.postings": [ + { + "asset": "USD/2", + "amount": 10, + "source": "alice", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/balance-not-found.num b/internal/interpreter/script-tests/balance-not-found.num new file mode 100644 index 00000000..a1b6c2b2 --- /dev/null +++ b/internal/interpreter/script-tests/balance-not-found.num @@ -0,0 +1,8 @@ +vars { + monetary $balance = balance(@a, EUR/2) +} + +send $balance ( + source = @world + destination = @dest +) diff --git a/internal/interpreter/script-tests/balance-not-found.num.specs.json b/internal/interpreter/script-tests/balance-not-found.num.specs.json new file mode 100644 index 00000000..1dc64b7e --- /dev/null +++ b/internal/interpreter/script-tests/balance-not-found.num.specs.json @@ -0,0 +1,9 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "handles balance not found", + "expect.postings": [] + } + ] +} diff --git a/internal/interpreter/script-tests/balance-simple.num b/internal/interpreter/script-tests/balance-simple.num new file mode 100644 index 00000000..13c3621c --- /dev/null +++ b/internal/interpreter/script-tests/balance-simple.num @@ -0,0 +1,8 @@ +vars { + monetary $bal = balance(@alice, USD/2) +} + +send $bal ( + source = @world + destination = @dest +) diff --git a/internal/interpreter/script-tests/balance-simple.num.specs.json b/internal/interpreter/script-tests/balance-simple.num.specs.json new file mode 100644 index 00000000..39c05320 --- /dev/null +++ b/internal/interpreter/script-tests/balance-simple.num.specs.json @@ -0,0 +1,19 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "uses simple balance function", + "balances": { + "alice": { "USD/2": 10 } + }, + "expect.postings": [ + { + "asset": "USD/2", + "amount": 10, + "source": "world", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/balance.num b/internal/interpreter/script-tests/balance.num new file mode 100644 index 00000000..a1b6c2b2 --- /dev/null +++ b/internal/interpreter/script-tests/balance.num @@ -0,0 +1,8 @@ +vars { + monetary $balance = balance(@a, EUR/2) +} + +send $balance ( + source = @world + destination = @dest +) diff --git a/internal/interpreter/script-tests/balance.num.specs.json b/internal/interpreter/script-tests/balance.num.specs.json new file mode 100644 index 00000000..15242de9 --- /dev/null +++ b/internal/interpreter/script-tests/balance.num.specs.json @@ -0,0 +1,19 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "uses balance function", + "balances": { + "a": { "EUR/2": 123 } + }, + "expect.postings": [ + { + "asset": "EUR/2", + "amount": 123, + "source": "world", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/capped-when-less-than-needed.num b/internal/interpreter/script-tests/capped-when-less-than-needed.num new file mode 100644 index 00000000..aff7bc5f --- /dev/null +++ b/internal/interpreter/script-tests/capped-when-less-than-needed.num @@ -0,0 +1,7 @@ +send [COIN 100] ( + source = { + max [COIN 40] from @src1 + @src2 + } + destination = @platform +) diff --git a/internal/interpreter/script-tests/capped-when-less-than-needed.num.specs.json b/internal/interpreter/script-tests/capped-when-less-than-needed.num.specs.json new file mode 100644 index 00000000..d72620df --- /dev/null +++ b/internal/interpreter/script-tests/capped-when-less-than-needed.num.specs.json @@ -0,0 +1,26 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "caps amount when less than needed", + "balances": { + "src1": { "COIN": 1000 }, + "src2": { "COIN": 1000 } + }, + "expect.postings": [ + { + "asset": "COIN", + "amount": 40, + "source": "src1", + "destination": "platform" + }, + { + "asset": "COIN", + "amount": 60, + "source": "src2", + "destination": "platform" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/capped-when-more-than-balance.num b/internal/interpreter/script-tests/capped-when-more-than-balance.num new file mode 100644 index 00000000..da2fe355 --- /dev/null +++ b/internal/interpreter/script-tests/capped-when-more-than-balance.num @@ -0,0 +1,7 @@ +send [COIN 100] ( + source = { + max [COIN 200] from @world + @src + } + destination = @platform +) diff --git a/internal/interpreter/script-tests/capped-when-more-than-balance.num.specs.json b/internal/interpreter/script-tests/capped-when-more-than-balance.num.specs.json new file mode 100644 index 00000000..db43e74e --- /dev/null +++ b/internal/interpreter/script-tests/capped-when-more-than-balance.num.specs.json @@ -0,0 +1,19 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "caps amount when more than needed", + "balances": { + "src": { "COIN": 1000 } + }, + "expect.postings": [ + { + "asset": "COIN", + "amount": 100, + "source": "world", + "destination": "platform" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/cascading-sources.num b/internal/interpreter/script-tests/cascading-sources.num new file mode 100644 index 00000000..cece1034 --- /dev/null +++ b/internal/interpreter/script-tests/cascading-sources.num @@ -0,0 +1,8 @@ +send [USD/2 100] ( + source = { + @users:001 + @users:002 + @users:003 + } + destination = @platform +) diff --git a/internal/interpreter/script-tests/cascading-sources.num.specs.json b/internal/interpreter/script-tests/cascading-sources.num.specs.json new file mode 100644 index 00000000..df65c80a --- /dev/null +++ b/internal/interpreter/script-tests/cascading-sources.num.specs.json @@ -0,0 +1,39 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "handles cascading sources", + "balances": { + "users:001": { + "USD/2": 25 + }, + "users:002": { + "USD/2": 25 + }, + "users:003": { + "USD/2": 100 + } + }, + "expect.postings": [ + { + "asset": "USD/2", + "amount": 25, + "source": "users:001", + "destination": "platform" + }, + { + "asset": "USD/2", + "amount": 25, + "source": "users:002", + "destination": "platform" + }, + { + "asset": "USD/2", + "amount": 50, + "source": "users:003", + "destination": "platform" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/color-inorder-send-all.num b/internal/interpreter/script-tests/color-inorder-send-all.num new file mode 100644 index 00000000..1fa774d0 --- /dev/null +++ b/internal/interpreter/script-tests/color-inorder-send-all.num @@ -0,0 +1,8 @@ +send [COIN *] ( + source = { + @src \ "RED" + @src \ "BLUE" + @src + } + destination = @dest +) diff --git a/internal/interpreter/script-tests/color-inorder-send-all.num.specs.json b/internal/interpreter/script-tests/color-inorder-send-all.num.specs.json new file mode 100644 index 00000000..0508eaf5 --- /dev/null +++ b/internal/interpreter/script-tests/color-inorder-send-all.num.specs.json @@ -0,0 +1,36 @@ +{ + "$schema": "../../../specs.schema.json", + "featureFlags": ["experimental-asset-colors"], + "testCases": [ + { + "it": "color inorder send all", + "balances": { + "src": { + "COIN": 100, + "COIN_RED": 20, + "COIN_BLUE": 30 + } + }, + "expect.postings": [ + { + "asset": "COIN_RED", + "amount": 20, + "source": "src", + "destination": "dest" + }, + { + "asset": "COIN_BLUE", + "amount": 30, + "source": "src", + "destination": "dest" + }, + { + "asset": "COIN", + "amount": 100, + "source": "src", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/color-inorder.num b/internal/interpreter/script-tests/color-inorder.num new file mode 100644 index 00000000..a6334b99 --- /dev/null +++ b/internal/interpreter/script-tests/color-inorder.num @@ -0,0 +1,8 @@ +send [COIN 100] ( + source = { + @src \ "RED" + @src \ "BLUE" + @src + } + destination = @dest +) diff --git a/internal/interpreter/script-tests/color-inorder.num.specs.json b/internal/interpreter/script-tests/color-inorder.num.specs.json new file mode 100644 index 00000000..930c3156 --- /dev/null +++ b/internal/interpreter/script-tests/color-inorder.num.specs.json @@ -0,0 +1,36 @@ +{ + "$schema": "../../../specs.schema.json", + "featureFlags": ["experimental-asset-colors"], + "testCases": [ + { + "it": "color inorder", + "balances": { + "src": { + "COIN": 100, + "COIN_RED": 20, + "COIN_BLUE": 30 + } + }, + "expect.postings": [ + { + "asset": "COIN_RED", + "amount": 20, + "source": "src", + "destination": "dest" + }, + { + "asset": "COIN_BLUE", + "amount": 30, + "source": "src", + "destination": "dest" + }, + { + "asset": "COIN", + "amount": 50, + "source": "src", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/color-restrict-balance-when-missing-funds.num b/internal/interpreter/script-tests/color-restrict-balance-when-missing-funds.num new file mode 100644 index 00000000..d6ba0363 --- /dev/null +++ b/internal/interpreter/script-tests/color-restrict-balance-when-missing-funds.num @@ -0,0 +1,4 @@ +send [COIN 20] ( + source = @acc \ "RED" + destination = @dest +) diff --git a/internal/interpreter/script-tests/color-restrict-balance-when-missing-funds.num.specs.json b/internal/interpreter/script-tests/color-restrict-balance-when-missing-funds.num.specs.json new file mode 100644 index 00000000..d9edff5a --- /dev/null +++ b/internal/interpreter/script-tests/color-restrict-balance-when-missing-funds.num.specs.json @@ -0,0 +1,16 @@ +{ + "$schema": "../../../specs.schema.json", + "featureFlags": ["experimental-asset-colors"], + "testCases": [ + { + "it": "color restrict balance when missing funds", + "balances": { + "acc": { + "COIN": 100, + "COIN_RED": 1 + } + }, + "expect.missingFunds": true + } + ] +} diff --git a/internal/interpreter/script-tests/color-restrict-balance.num b/internal/interpreter/script-tests/color-restrict-balance.num new file mode 100644 index 00000000..d6ba0363 --- /dev/null +++ b/internal/interpreter/script-tests/color-restrict-balance.num @@ -0,0 +1,4 @@ +send [COIN 20] ( + source = @acc \ "RED" + destination = @dest +) diff --git a/internal/interpreter/script-tests/color-restrict-balance.num.specs.json b/internal/interpreter/script-tests/color-restrict-balance.num.specs.json new file mode 100644 index 00000000..fddd6046 --- /dev/null +++ b/internal/interpreter/script-tests/color-restrict-balance.num.specs.json @@ -0,0 +1,23 @@ +{ + "$schema": "../../../specs.schema.json", + "featureFlags": ["experimental-asset-colors"], + "testCases": [ + { + "it": "color restrict balance", + "balances": { + "acc": { + "COIN": 1, + "COIN_RED": 100 + } + }, + "expect.postings": [ + { + "asset": "COIN_RED", + "amount": 20, + "source": "acc", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/color-restriction-in-send-all.num b/internal/interpreter/script-tests/color-restriction-in-send-all.num new file mode 100644 index 00000000..664ae597 --- /dev/null +++ b/internal/interpreter/script-tests/color-restriction-in-send-all.num @@ -0,0 +1,4 @@ +send [COIN *] ( + source = @src \ "RED" + destination = @dest +) diff --git a/internal/interpreter/script-tests/color-restriction-in-send-all.num.specs.json b/internal/interpreter/script-tests/color-restriction-in-send-all.num.specs.json new file mode 100644 index 00000000..74c47c40 --- /dev/null +++ b/internal/interpreter/script-tests/color-restriction-in-send-all.num.specs.json @@ -0,0 +1,22 @@ +{ + "$schema": "../../../specs.schema.json", + "featureFlags": ["experimental-asset-colors"], + "testCases": [ + { + "it": "color restriction in send all", + "balances": { + "src": { + "COIN_RED": 42 + } + }, + "expect.postings": [ + { + "asset": "COIN_RED", + "amount": 42, + "source": "src", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/color-send-overdraft.num b/internal/interpreter/script-tests/color-send-overdraft.num new file mode 100644 index 00000000..34080516 --- /dev/null +++ b/internal/interpreter/script-tests/color-send-overdraft.num @@ -0,0 +1,4 @@ +send [COIN 100] ( + source = @acc \ "RED" allowing unbounded overdraft + destination = @dest +) diff --git a/internal/interpreter/script-tests/color-send-overdraft.num.specs.json b/internal/interpreter/script-tests/color-send-overdraft.num.specs.json new file mode 100644 index 00000000..0d3f8a48 --- /dev/null +++ b/internal/interpreter/script-tests/color-send-overdraft.num.specs.json @@ -0,0 +1,17 @@ +{ + "$schema": "../../../specs.schema.json", + "featureFlags": ["experimental-asset-colors"], + "testCases": [ + { + "it": "color send overdraft", + "expect.postings": [ + { + "asset": "COIN_RED", + "amount": 100, + "source": "acc", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/color-send.num b/internal/interpreter/script-tests/color-send.num new file mode 100644 index 00000000..dfc97269 --- /dev/null +++ b/internal/interpreter/script-tests/color-send.num @@ -0,0 +1,4 @@ +send [COIN 100] ( + source = @world \ "RED" + destination = @dest +) diff --git a/internal/interpreter/script-tests/color-send.num.specs.json b/internal/interpreter/script-tests/color-send.num.specs.json new file mode 100644 index 00000000..1bb1ca41 --- /dev/null +++ b/internal/interpreter/script-tests/color-send.num.specs.json @@ -0,0 +1,17 @@ +{ + "$schema": "../../../specs.schema.json", + "featureFlags": ["experimental-asset-colors"], + "testCases": [ + { + "it": "color send", + "expect.postings": [ + { + "asset": "COIN_RED", + "amount": 100, + "source": "world", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/color-with-asset-precision.num b/internal/interpreter/script-tests/color-with-asset-precision.num new file mode 100644 index 00000000..8a851d3f --- /dev/null +++ b/internal/interpreter/script-tests/color-with-asset-precision.num @@ -0,0 +1,4 @@ +send [USD/4 10] ( + source = @src \ "COL" allowing unbounded overdraft + destination = @dest +) diff --git a/internal/interpreter/script-tests/color-with-asset-precision.num.specs.json b/internal/interpreter/script-tests/color-with-asset-precision.num.specs.json new file mode 100644 index 00000000..14985817 --- /dev/null +++ b/internal/interpreter/script-tests/color-with-asset-precision.num.specs.json @@ -0,0 +1,17 @@ +{ + "$schema": "../../../specs.schema.json", + "featureFlags": ["experimental-asset-colors"], + "testCases": [ + { + "it": "color with asset precision", + "expect.postings": [ + { + "asset": "USD_COL/4", + "amount": 10, + "source": "src", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/do-not-exceed-overdraft-on-send-all.num b/internal/interpreter/script-tests/do-not-exceed-overdraft-on-send-all.num new file mode 100644 index 00000000..d58c8926 --- /dev/null +++ b/internal/interpreter/script-tests/do-not-exceed-overdraft-on-send-all.num @@ -0,0 +1,4 @@ +send [COIN *] ( + source = @s allowing overdraft up to [COIN 5] + destination = @dest +) diff --git a/internal/interpreter/script-tests/do-not-exceed-overdraft-on-send-all.num.specs.json b/internal/interpreter/script-tests/do-not-exceed-overdraft-on-send-all.num.specs.json new file mode 100644 index 00000000..efae4402 --- /dev/null +++ b/internal/interpreter/script-tests/do-not-exceed-overdraft-on-send-all.num.specs.json @@ -0,0 +1,21 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "do not exceed overdraft on send all", + "balances": { + "s": { + "COIN": -4 + } + }, + "expect.postings": [ + { + "asset": "COIN", + "amount": 1, + "source": "s", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/do-not-exceed-overdraft-when-double-spending.num b/internal/interpreter/script-tests/do-not-exceed-overdraft-when-double-spending.num new file mode 100644 index 00000000..415416c8 --- /dev/null +++ b/internal/interpreter/script-tests/do-not-exceed-overdraft-when-double-spending.num @@ -0,0 +1,10 @@ +send [COIN 10] ( + source = { + @s allowing overdraft up to [COIN 2] + + @s allowing overdraft up to [COIN 5] + + @world + } + destination = @dest +) diff --git a/internal/interpreter/script-tests/do-not-exceed-overdraft-when-double-spending.num.specs.json b/internal/interpreter/script-tests/do-not-exceed-overdraft-when-double-spending.num.specs.json new file mode 100644 index 00000000..a4db8b5b --- /dev/null +++ b/internal/interpreter/script-tests/do-not-exceed-overdraft-when-double-spending.num.specs.json @@ -0,0 +1,22 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "do not exceed overdraft when double spending", + "expect.postings": [ + { + "asset": "COIN", + "amount": 5, + "source": "s", + "destination": "dest" + }, + { + "asset": "COIN", + "amount": 5, + "source": "world", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/do-not-exceed-overdraft.num b/internal/interpreter/script-tests/do-not-exceed-overdraft.num new file mode 100644 index 00000000..27563484 --- /dev/null +++ b/internal/interpreter/script-tests/do-not-exceed-overdraft.num @@ -0,0 +1,8 @@ +send [COIN 10] ( + source = { + @s allowing overdraft up to [COIN 5] + + @world + } + destination = @dest +) diff --git a/internal/interpreter/script-tests/do-not-exceed-overdraft.num.specs.json b/internal/interpreter/script-tests/do-not-exceed-overdraft.num.specs.json new file mode 100644 index 00000000..bff594cd --- /dev/null +++ b/internal/interpreter/script-tests/do-not-exceed-overdraft.num.specs.json @@ -0,0 +1,27 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "do not exceed overdraft", + "balances": { + "s": { + "COIN": -2 + } + }, + "expect.postings": [ + { + "asset": "COIN", + "amount": 3, + "source": "s", + "destination": "dest" + }, + { + "asset": "COIN", + "amount": 7, + "source": "world", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/dynamic-allocation.num b/internal/interpreter/script-tests/dynamic-allocation.num new file mode 100644 index 00000000..6260b6fb --- /dev/null +++ b/internal/interpreter/script-tests/dynamic-allocation.num @@ -0,0 +1,11 @@ +vars { + portion $p +} +send [GEM 15] ( + source = @a + destination = { + 80% to @b + $p to @c + remaining to @d + } +) diff --git a/internal/interpreter/script-tests/dynamic-allocation.num.specs.json b/internal/interpreter/script-tests/dynamic-allocation.num.specs.json new file mode 100644 index 00000000..0fb3eaca --- /dev/null +++ b/internal/interpreter/script-tests/dynamic-allocation.num.specs.json @@ -0,0 +1,28 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "uses dynamic allocation with variables", + "balances": { + "a": { "GEM": 15 } + }, + "vars": { + "p": "15%" + }, + "expect.postings": [ + { + "asset": "GEM", + "amount": 13, + "source": "a", + "destination": "b" + }, + { + "asset": "GEM", + "amount": 2, + "source": "a", + "destination": "c" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/empty-color.num b/internal/interpreter/script-tests/empty-color.num new file mode 100644 index 00000000..23f6ed59 --- /dev/null +++ b/internal/interpreter/script-tests/empty-color.num @@ -0,0 +1,4 @@ +send [COIN *] ( + source = @src \ "" + destination = @dest +) diff --git a/internal/interpreter/script-tests/empty-color.num.specs.json b/internal/interpreter/script-tests/empty-color.num.specs.json new file mode 100644 index 00000000..5fbf1cf2 --- /dev/null +++ b/internal/interpreter/script-tests/empty-color.num.specs.json @@ -0,0 +1,22 @@ +{ + "$schema": "../../../specs.schema.json", + "featureFlags": ["experimental-asset-colors"], + "testCases": [ + { + "it": "empty color", + "balances": { + "src": { + "COIN": 100 + } + }, + "expect.postings": [ + { + "asset": "COIN", + "amount": 100, + "source": "src", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/empty-postings.num b/internal/interpreter/script-tests/empty-postings.num new file mode 100644 index 00000000..3b9ab8b3 --- /dev/null +++ b/internal/interpreter/script-tests/empty-postings.num @@ -0,0 +1,4 @@ +send [GEM *] ( + source = @foo + destination = @bar +) diff --git a/internal/interpreter/script-tests/empty-postings.num.specs.json b/internal/interpreter/script-tests/empty-postings.num.specs.json new file mode 100644 index 00000000..e92774db --- /dev/null +++ b/internal/interpreter/script-tests/empty-postings.num.specs.json @@ -0,0 +1,12 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "handles empty postings when no balance", + "balances": { + "foo": { "GEM": 0 } + }, + "expect.postings": [] + } + ] +} diff --git a/internal/interpreter/script-tests/expr-in-var-origin.num b/internal/interpreter/script-tests/expr-in-var-origin.num new file mode 100644 index 00000000..08a302f9 --- /dev/null +++ b/internal/interpreter/script-tests/expr-in-var-origin.num @@ -0,0 +1,3 @@ +vars { + number $x = 1 + 2 +} diff --git a/internal/interpreter/script-tests/expr-in-var-origin.num.specs.json b/internal/interpreter/script-tests/expr-in-var-origin.num.specs.json new file mode 100644 index 00000000..b0c988d4 --- /dev/null +++ b/internal/interpreter/script-tests/expr-in-var-origin.num.specs.json @@ -0,0 +1,10 @@ +{ + "$schema": "../../../specs.schema.json", + "featureFlags": ["experimental-mid-script-function-call"], + "testCases": [ + { + "it": "expr in var origin", + "expect.postings": [] + } + ] +} diff --git a/internal/interpreter/script-tests/get-amount-function.num b/internal/interpreter/script-tests/get-amount-function.num new file mode 100644 index 00000000..014d7588 --- /dev/null +++ b/internal/interpreter/script-tests/get-amount-function.num @@ -0,0 +1,3 @@ +vars { number $a = get_amount([ABC 100]) } + +set_tx_meta("amt", $a) diff --git a/internal/interpreter/script-tests/get-amount-function.num.specs.json b/internal/interpreter/script-tests/get-amount-function.num.specs.json new file mode 100644 index 00000000..e0629bb5 --- /dev/null +++ b/internal/interpreter/script-tests/get-amount-function.num.specs.json @@ -0,0 +1,12 @@ +{ + "$schema": "../../../specs.schema.json", + "featureFlags": ["experimental-get-amount-function"], + "testCases": [ + { + "it": "get amount function", + "expect.txMeta": { + "amt": "100" + } + } + ] +} diff --git a/internal/interpreter/script-tests/get-asset-function.num b/internal/interpreter/script-tests/get-asset-function.num new file mode 100644 index 00000000..8dd3a6a9 --- /dev/null +++ b/internal/interpreter/script-tests/get-asset-function.num @@ -0,0 +1,3 @@ +vars { asset $a = get_asset([ABC 100]) } + +set_tx_meta("asset", $a) diff --git a/internal/interpreter/script-tests/get-asset-function.num.specs.json b/internal/interpreter/script-tests/get-asset-function.num.specs.json new file mode 100644 index 00000000..eaf36393 --- /dev/null +++ b/internal/interpreter/script-tests/get-asset-function.num.specs.json @@ -0,0 +1,12 @@ +{ + "$schema": "../../../specs.schema.json", + "featureFlags": ["experimental-get-asset-function"], + "testCases": [ + { + "it": "get asset function", + "expect.txMeta": { + "asset": "ABC" + } + } + ] +} diff --git a/internal/interpreter/script-tests/insufficient-funds.num b/internal/interpreter/script-tests/insufficient-funds.num new file mode 100644 index 00000000..18a42340 --- /dev/null +++ b/internal/interpreter/script-tests/insufficient-funds.num @@ -0,0 +1,12 @@ +vars { + account $balance + account $payment + account $seller +} +send [GEM 16] ( + source = { + $balance + $payment + } + destination = $seller +) diff --git a/internal/interpreter/script-tests/insufficient-funds.num.specs.json b/internal/interpreter/script-tests/insufficient-funds.num.specs.json new file mode 100644 index 00000000..2300c01d --- /dev/null +++ b/internal/interpreter/script-tests/insufficient-funds.num.specs.json @@ -0,0 +1,18 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "handles insufficient funds error", + "balances": { + "users:001": { "GEM": 3 }, + "payments:001": { "GEM": 12 } + }, + "vars": { + "balance": "users:001", + "payment": "payments:001", + "seller": "users:002" + }, + "expect.missingFunds": true + } + ] +} diff --git a/internal/interpreter/script-tests/kept-in-send-all-inorder.num b/internal/interpreter/script-tests/kept-in-send-all-inorder.num new file mode 100644 index 00000000..6d035df3 --- /dev/null +++ b/internal/interpreter/script-tests/kept-in-send-all-inorder.num @@ -0,0 +1,7 @@ +send [COIN *] ( + source = @src + destination = { + max [COIN 1] kept + remaining to @dest + } +) diff --git a/internal/interpreter/script-tests/kept-in-send-all-inorder.num.specs.json b/internal/interpreter/script-tests/kept-in-send-all-inorder.num.specs.json new file mode 100644 index 00000000..74a5e273 --- /dev/null +++ b/internal/interpreter/script-tests/kept-in-send-all-inorder.num.specs.json @@ -0,0 +1,19 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "keeps amount in send all with inorder", + "balances": { + "src": { "COIN": 10 } + }, + "expect.postings": [ + { + "asset": "COIN", + "amount": 9, + "source": "src", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/kept-inorder.num b/internal/interpreter/script-tests/kept-inorder.num new file mode 100644 index 00000000..8daa92d6 --- /dev/null +++ b/internal/interpreter/script-tests/kept-inorder.num @@ -0,0 +1,7 @@ +send [COIN 100] ( + source = @world + destination = { + max [COIN 10] kept + remaining to @dest + } +) diff --git a/internal/interpreter/script-tests/kept-inorder.num.specs.json b/internal/interpreter/script-tests/kept-inorder.num.specs.json new file mode 100644 index 00000000..017f8a6e --- /dev/null +++ b/internal/interpreter/script-tests/kept-inorder.num.specs.json @@ -0,0 +1,16 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "keeps amount in order", + "expect.postings": [ + { + "asset": "COIN", + "amount": 90, + "source": "world", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/kept-with-balance.num b/internal/interpreter/script-tests/kept-with-balance.num new file mode 100644 index 00000000..6cfe8da4 --- /dev/null +++ b/internal/interpreter/script-tests/kept-with-balance.num @@ -0,0 +1,7 @@ +send [COIN 100] ( + source = @src + destination = { + max [COIN 10] kept + remaining to @dest + } +) diff --git a/internal/interpreter/script-tests/kept-with-balance.num.specs.json b/internal/interpreter/script-tests/kept-with-balance.num.specs.json new file mode 100644 index 00000000..ec483265 --- /dev/null +++ b/internal/interpreter/script-tests/kept-with-balance.num.specs.json @@ -0,0 +1,19 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "keeps amount with balance", + "balances": { + "src": { "COIN": 1000 } + }, + "expect.postings": [ + { + "asset": "COIN", + "amount": 90, + "source": "src", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/many-kept-dest.num b/internal/interpreter/script-tests/many-kept-dest.num new file mode 100644 index 00000000..d1bd39d5 --- /dev/null +++ b/internal/interpreter/script-tests/many-kept-dest.num @@ -0,0 +1,8 @@ +send [USD/2 100] ( + source = @world + destination = { + max [USD/2 10] kept + max [USD/2 12] to @d2 + remaining to @rem + } +) diff --git a/internal/interpreter/script-tests/many-kept-dest.num.specs.json b/internal/interpreter/script-tests/many-kept-dest.num.specs.json new file mode 100644 index 00000000..d64fc0dd --- /dev/null +++ b/internal/interpreter/script-tests/many-kept-dest.num.specs.json @@ -0,0 +1,25 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "sends with many kept destinations", + "balances": { + "src": { "USD/2": 100 } + }, + "expect.postings": [ + { + "asset": "USD/2", + "amount": 12, + "source": "world", + "destination": "d2" + }, + { + "asset": "USD/2", + "amount": 78, + "source": "world", + "destination": "rem" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/many-max-dest.num b/internal/interpreter/script-tests/many-max-dest.num new file mode 100644 index 00000000..3253be91 --- /dev/null +++ b/internal/interpreter/script-tests/many-max-dest.num @@ -0,0 +1,8 @@ +send [USD/2 100] ( + source = @world + destination = { + max [USD/2 10] to @d1 + max [USD/2 12] to @d2 + remaining to @rem + } +) diff --git a/internal/interpreter/script-tests/many-max-dest.num.specs.json b/internal/interpreter/script-tests/many-max-dest.num.specs.json new file mode 100644 index 00000000..0482a8f6 --- /dev/null +++ b/internal/interpreter/script-tests/many-max-dest.num.specs.json @@ -0,0 +1,31 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "sends with many max destinations", + "balances": { + "src": { "USD/2": 100 } + }, + "expect.postings": [ + { + "asset": "USD/2", + "amount": 10, + "source": "world", + "destination": "d1" + }, + { + "asset": "USD/2", + "amount": 12, + "source": "world", + "destination": "d2" + }, + { + "asset": "USD/2", + "amount": 78, + "source": "world", + "destination": "rem" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/max-with-unbounded-overdraft.num b/internal/interpreter/script-tests/max-with-unbounded-overdraft.num new file mode 100644 index 00000000..04ffa368 --- /dev/null +++ b/internal/interpreter/script-tests/max-with-unbounded-overdraft.num @@ -0,0 +1,7 @@ +send [COIN 100] ( + source = { + max [COIN 10] from @account1 allowing unbounded overdraft + @account2 + } + destination = @world +) diff --git a/internal/interpreter/script-tests/max-with-unbounded-overdraft.num.specs.json b/internal/interpreter/script-tests/max-with-unbounded-overdraft.num.specs.json new file mode 100644 index 00000000..bfb72f80 --- /dev/null +++ b/internal/interpreter/script-tests/max-with-unbounded-overdraft.num.specs.json @@ -0,0 +1,30 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "handles max with unbounded overdraft", + "balances": { + "account1": { + "COIN": 10000 + }, + "account2": { + "COIN": 10000 + } + }, + "expect.postings": [ + { + "asset": "COIN", + "amount": 10, + "source": "account1", + "destination": "world" + }, + { + "asset": "COIN", + "amount": 90, + "source": "account2", + "destination": "world" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/metadata.num b/internal/interpreter/script-tests/metadata.num new file mode 100644 index 00000000..655ee0fb --- /dev/null +++ b/internal/interpreter/script-tests/metadata.num @@ -0,0 +1,12 @@ +vars { + account $sale + account $seller = meta($sale, "seller") + portion $commission = meta($seller, "commission") +} +send [EUR/2 100] ( + source = $sale + destination = { + remaining to $seller + $commission to @platform + } +) diff --git a/internal/interpreter/script-tests/metadata.num.specs.json b/internal/interpreter/script-tests/metadata.num.specs.json new file mode 100644 index 00000000..4135e5b7 --- /dev/null +++ b/internal/interpreter/script-tests/metadata.num.specs.json @@ -0,0 +1,37 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "uses metadata for variables", + "balances": { + "sales:042": { "EUR/2": 2500 }, + "users:053": { "EUR/2": 500 } + }, + "vars": { + "sale": "sales:042" + }, + "accountsMeta": { + "sales:042": { + "seller": "users:053" + }, + "users:053": { + "commission": "12.5%" + } + }, + "expect.postings": [ + { + "asset": "EUR/2", + "amount": 88, + "source": "sales:042", + "destination": "users:053" + }, + { + "asset": "EUR/2", + "amount": 12, + "source": "sales:042", + "destination": "platform" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/midscript-balance-after-decrease.num b/internal/interpreter/script-tests/midscript-balance-after-decrease.num new file mode 100644 index 00000000..e28ecb52 --- /dev/null +++ b/internal/interpreter/script-tests/midscript-balance-after-decrease.num @@ -0,0 +1,9 @@ +send [USD/2 3] ( + source = @acc + destination = @world +) + +send balance(@acc, USD/2) ( + source = @world + destination = @dest +) diff --git a/internal/interpreter/script-tests/midscript-balance-after-decrease.num.specs.json b/internal/interpreter/script-tests/midscript-balance-after-decrease.num.specs.json new file mode 100644 index 00000000..a839f8d3 --- /dev/null +++ b/internal/interpreter/script-tests/midscript-balance-after-decrease.num.specs.json @@ -0,0 +1,28 @@ +{ + "$schema": "../../../specs.schema.json", + "featureFlags": ["experimental-mid-script-function-call"], + "testCases": [ + { + "it": "midscript balance after decrease", + "balances": { + "acc": { + "USD/2": 10 + } + }, + "expect.postings": [ + { + "asset": "USD/2", + "amount": 3, + "source": "acc", + "destination": "world" + }, + { + "asset": "USD/2", + "amount": 7, + "source": "world", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/midscript-balance.num b/internal/interpreter/script-tests/midscript-balance.num new file mode 100644 index 00000000..01ad1aa2 --- /dev/null +++ b/internal/interpreter/script-tests/midscript-balance.num @@ -0,0 +1,4 @@ +send balance(@acc, USD/2) ( + source = @world + destination = @dest +) diff --git a/internal/interpreter/script-tests/midscript-balance.num.specs.json b/internal/interpreter/script-tests/midscript-balance.num.specs.json new file mode 100644 index 00000000..82d352ae --- /dev/null +++ b/internal/interpreter/script-tests/midscript-balance.num.specs.json @@ -0,0 +1,22 @@ +{ + "$schema": "../../../specs.schema.json", + "featureFlags": ["experimental-mid-script-function-call"], + "testCases": [ + { + "it": "midscript balance", + "balances": { + "acc": { + "USD/2": 42 + } + }, + "expect.postings": [ + { + "asset": "USD/2", + "amount": 42, + "source": "world", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/negative-max-send-all.num b/internal/interpreter/script-tests/negative-max-send-all.num new file mode 100644 index 00000000..5a7f99db --- /dev/null +++ b/internal/interpreter/script-tests/negative-max-send-all.num @@ -0,0 +1,4 @@ +send [USD/2 *] ( + source = max [USD/2 -50] from @src + destination = @dest +) diff --git a/internal/interpreter/script-tests/negative-max-send-all.num.specs.json b/internal/interpreter/script-tests/negative-max-send-all.num.specs.json new file mode 100644 index 00000000..4a6ff680 --- /dev/null +++ b/internal/interpreter/script-tests/negative-max-send-all.num.specs.json @@ -0,0 +1,12 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "sends all with negative max", + "balances": { + "src": { "USD/2": 0 } + }, + "expect.postings": [] + } + ] +} diff --git a/internal/interpreter/script-tests/negative-max.num b/internal/interpreter/script-tests/negative-max.num new file mode 100644 index 00000000..38b0df1e --- /dev/null +++ b/internal/interpreter/script-tests/negative-max.num @@ -0,0 +1,7 @@ +send [USD/2 100] ( + source = { + max [USD/2 -50] from @src + @world + } + destination = @dest +) diff --git a/internal/interpreter/script-tests/negative-max.num.specs.json b/internal/interpreter/script-tests/negative-max.num.specs.json new file mode 100644 index 00000000..25eeb12c --- /dev/null +++ b/internal/interpreter/script-tests/negative-max.num.specs.json @@ -0,0 +1,19 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "uses negative max with world fallback", + "balances": { + "src": { "USD/2": 0 } + }, + "expect.postings": [ + { + "asset": "USD/2", + "amount": 100, + "source": "world", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/nested-remaining-complex.num b/internal/interpreter/script-tests/nested-remaining-complex.num new file mode 100644 index 00000000..414accca --- /dev/null +++ b/internal/interpreter/script-tests/nested-remaining-complex.num @@ -0,0 +1,13 @@ +send [COIN 100] ( + source = @world + destination = { + max [COIN 10] to @a + remaining to { + max [COIN 20] to @b + remaining to { + max [COIN 30] to @c + remaining to @d + } + } + } +) diff --git a/internal/interpreter/script-tests/nested-remaining-complex.num.specs.json b/internal/interpreter/script-tests/nested-remaining-complex.num.specs.json new file mode 100644 index 00000000..4f6688aa --- /dev/null +++ b/internal/interpreter/script-tests/nested-remaining-complex.num.specs.json @@ -0,0 +1,34 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "handles nested remaining complex", + "expect.postings": [ + { + "asset": "COIN", + "amount": 10, + "source": "world", + "destination": "a" + }, + { + "asset": "COIN", + "amount": 20, + "source": "world", + "destination": "b" + }, + { + "asset": "COIN", + "amount": 30, + "source": "world", + "destination": "c" + }, + { + "asset": "COIN", + "amount": 40, + "source": "world", + "destination": "d" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/nested-remaining.num b/internal/interpreter/script-tests/nested-remaining.num new file mode 100644 index 00000000..a0718823 --- /dev/null +++ b/internal/interpreter/script-tests/nested-remaining.num @@ -0,0 +1,10 @@ +send [COIN 100] ( + source = @world + destination = { + max [COIN 10] to @a + remaining to { + max [COIN 20] to @b + remaining to @c + } + } +) diff --git a/internal/interpreter/script-tests/nested-remaining.num.specs.json b/internal/interpreter/script-tests/nested-remaining.num.specs.json new file mode 100644 index 00000000..dfd543b3 --- /dev/null +++ b/internal/interpreter/script-tests/nested-remaining.num.specs.json @@ -0,0 +1,28 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "handles nested remaining", + "expect.postings": [ + { + "asset": "COIN", + "amount": 10, + "source": "world", + "destination": "a" + }, + { + "asset": "COIN", + "amount": 20, + "source": "world", + "destination": "b" + }, + { + "asset": "COIN", + "amount": 70, + "source": "world", + "destination": "c" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/no-double-spending-in-colored-send-all.num b/internal/interpreter/script-tests/no-double-spending-in-colored-send-all.num new file mode 100644 index 00000000..a886bb45 --- /dev/null +++ b/internal/interpreter/script-tests/no-double-spending-in-colored-send-all.num @@ -0,0 +1,8 @@ +send [COIN *] ( + source = { + @src \ "X" + @src \ "X" + @src + } + destination = @dest +) diff --git a/internal/interpreter/script-tests/no-double-spending-in-colored-send-all.num.specs.json b/internal/interpreter/script-tests/no-double-spending-in-colored-send-all.num.specs.json new file mode 100644 index 00000000..61d1acdf --- /dev/null +++ b/internal/interpreter/script-tests/no-double-spending-in-colored-send-all.num.specs.json @@ -0,0 +1,29 @@ +{ + "$schema": "../../../specs.schema.json", + "featureFlags": ["experimental-asset-colors"], + "testCases": [ + { + "it": "no double spending in colored send all", + "balances": { + "src": { + "COIN": 100, + "COIN_X": 20 + } + }, + "expect.postings": [ + { + "asset": "COIN_X", + "amount": 20, + "source": "src", + "destination": "dest" + }, + { + "asset": "COIN", + "amount": 100, + "source": "src", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/no-double-spending-in-colored-send.num b/internal/interpreter/script-tests/no-double-spending-in-colored-send.num new file mode 100644 index 00000000..df843dcb --- /dev/null +++ b/internal/interpreter/script-tests/no-double-spending-in-colored-send.num @@ -0,0 +1,8 @@ +send [COIN 100] ( + source = { + @src \ "X" + @src \ "X" + @src + } + destination = @dest +) diff --git a/internal/interpreter/script-tests/no-double-spending-in-colored-send.num.specs.json b/internal/interpreter/script-tests/no-double-spending-in-colored-send.num.specs.json new file mode 100644 index 00000000..5024cc97 --- /dev/null +++ b/internal/interpreter/script-tests/no-double-spending-in-colored-send.num.specs.json @@ -0,0 +1,29 @@ +{ + "$schema": "../../../specs.schema.json", + "featureFlags": ["experimental-asset-colors"], + "testCases": [ + { + "it": "no double spending in colored send", + "balances": { + "src": { + "COIN": 99999, + "COIN_X": 20 + } + }, + "expect.postings": [ + { + "asset": "COIN_X", + "amount": 20, + "source": "src", + "destination": "dest" + }, + { + "asset": "COIN", + "amount": 80, + "source": "src", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/no-empty-postings.num b/internal/interpreter/script-tests/no-empty-postings.num new file mode 100644 index 00000000..f7288a2b --- /dev/null +++ b/internal/interpreter/script-tests/no-empty-postings.num @@ -0,0 +1,7 @@ +send [GEM 2] ( + source = @world + destination = { + 90% to @a + 10% to @b + } +) diff --git a/internal/interpreter/script-tests/no-empty-postings.num.specs.json b/internal/interpreter/script-tests/no-empty-postings.num.specs.json new file mode 100644 index 00000000..d2f0adf2 --- /dev/null +++ b/internal/interpreter/script-tests/no-empty-postings.num.specs.json @@ -0,0 +1,16 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "filters out empty postings", + "expect.postings": [ + { + "asset": "GEM", + "amount": 2, + "source": "world", + "destination": "a" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/oneof-all-failing.num b/internal/interpreter/script-tests/oneof-all-failing.num new file mode 100644 index 00000000..2ef7a7f6 --- /dev/null +++ b/internal/interpreter/script-tests/oneof-all-failing.num @@ -0,0 +1,8 @@ +send [GEM 1] ( + source = oneof { + @empty1 + @empty2 + @empty3 + } + destination = @dest +) diff --git a/internal/interpreter/script-tests/oneof-all-failing.num.specs.json b/internal/interpreter/script-tests/oneof-all-failing.num.specs.json new file mode 100644 index 00000000..2e022e2e --- /dev/null +++ b/internal/interpreter/script-tests/oneof-all-failing.num.specs.json @@ -0,0 +1,10 @@ +{ + "$schema": "../../../specs.schema.json", + "featureFlags": ["experimental-oneof"], + "testCases": [ + { + "it": "oneof all failing", + "expect.missingFunds": true + } + ] +} diff --git a/internal/interpreter/script-tests/oneof-destination-first-clause.num b/internal/interpreter/script-tests/oneof-destination-first-clause.num new file mode 100644 index 00000000..91b87711 --- /dev/null +++ b/internal/interpreter/script-tests/oneof-destination-first-clause.num @@ -0,0 +1,7 @@ +send [GEM 10] ( + source = @world + destination = oneof { + max [GEM 99999] to @a + remaining to @b + } +) diff --git a/internal/interpreter/script-tests/oneof-destination-first-clause.num.specs.json b/internal/interpreter/script-tests/oneof-destination-first-clause.num.specs.json new file mode 100644 index 00000000..6d770439 --- /dev/null +++ b/internal/interpreter/script-tests/oneof-destination-first-clause.num.specs.json @@ -0,0 +1,17 @@ +{ + "$schema": "../../../specs.schema.json", + "featureFlags": ["experimental-oneof"], + "testCases": [ + { + "it": "oneof destination first clause", + "expect.postings": [ + { + "asset": "GEM", + "amount": 10, + "source": "world", + "destination": "a" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/oneof-destination-remaining-clause.num b/internal/interpreter/script-tests/oneof-destination-remaining-clause.num new file mode 100644 index 00000000..a0d6aae1 --- /dev/null +++ b/internal/interpreter/script-tests/oneof-destination-remaining-clause.num @@ -0,0 +1,8 @@ +send [GEM 100] ( + source = @world + destination = oneof { + max [GEM 9] to @a + max [GEM 10] to @b + remaining to @rem + } +) diff --git a/internal/interpreter/script-tests/oneof-destination-remaining-clause.num.specs.json b/internal/interpreter/script-tests/oneof-destination-remaining-clause.num.specs.json new file mode 100644 index 00000000..83e93725 --- /dev/null +++ b/internal/interpreter/script-tests/oneof-destination-remaining-clause.num.specs.json @@ -0,0 +1,17 @@ +{ + "$schema": "../../../specs.schema.json", + "featureFlags": ["experimental-oneof"], + "testCases": [ + { + "it": "oneof destination remaining clause", + "expect.postings": [ + { + "asset": "GEM", + "amount": 100, + "source": "world", + "destination": "rem" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/oneof-destination-second-clause.num b/internal/interpreter/script-tests/oneof-destination-second-clause.num new file mode 100644 index 00000000..bf22291d --- /dev/null +++ b/internal/interpreter/script-tests/oneof-destination-second-clause.num @@ -0,0 +1,8 @@ +send [GEM 10] ( + source = @world + destination = oneof { + max [GEM 9] to @a + max [GEM 10] to @b + remaining to @rem + } +) diff --git a/internal/interpreter/script-tests/oneof-destination-second-clause.num.specs.json b/internal/interpreter/script-tests/oneof-destination-second-clause.num.specs.json new file mode 100644 index 00000000..c73e10db --- /dev/null +++ b/internal/interpreter/script-tests/oneof-destination-second-clause.num.specs.json @@ -0,0 +1,17 @@ +{ + "$schema": "../../../specs.schema.json", + "featureFlags": ["experimental-oneof"], + "testCases": [ + { + "it": "oneof destination second clause", + "expect.postings": [ + { + "asset": "GEM", + "amount": 10, + "source": "world", + "destination": "b" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/oneof-in-send-all.num b/internal/interpreter/script-tests/oneof-in-send-all.num new file mode 100644 index 00000000..39fe42e0 --- /dev/null +++ b/internal/interpreter/script-tests/oneof-in-send-all.num @@ -0,0 +1,8 @@ +send [GEM *] ( + source = oneof { + @s1 + @s2 + @s3 + } + destination = @dest +) diff --git a/internal/interpreter/script-tests/oneof-in-send-all.num.specs.json b/internal/interpreter/script-tests/oneof-in-send-all.num.specs.json new file mode 100644 index 00000000..ba6874be --- /dev/null +++ b/internal/interpreter/script-tests/oneof-in-send-all.num.specs.json @@ -0,0 +1,22 @@ +{ + "$schema": "../../../specs.schema.json", + "featureFlags": ["experimental-oneof"], + "testCases": [ + { + "it": "oneof in send all", + "balances": { + "s1": { + "GEM": 10 + } + }, + "expect.postings": [ + { + "asset": "GEM", + "amount": 10, + "source": "s1", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/oneof-in-source-send-first-branch.num b/internal/interpreter/script-tests/oneof-in-source-send-first-branch.num new file mode 100644 index 00000000..8d407d7a --- /dev/null +++ b/internal/interpreter/script-tests/oneof-in-source-send-first-branch.num @@ -0,0 +1,7 @@ +send [GEM 15] ( + source = oneof { + @a allowing unbounded overdraft + @empty + } + destination = @dest +) diff --git a/internal/interpreter/script-tests/oneof-in-source-send-first-branch.num.specs.json b/internal/interpreter/script-tests/oneof-in-source-send-first-branch.num.specs.json new file mode 100644 index 00000000..1c335faf --- /dev/null +++ b/internal/interpreter/script-tests/oneof-in-source-send-first-branch.num.specs.json @@ -0,0 +1,17 @@ +{ + "$schema": "../../../specs.schema.json", + "featureFlags": ["experimental-oneof"], + "testCases": [ + { + "it": "oneof in source sends first branch", + "expect.postings": [ + { + "asset": "GEM", + "amount": 15, + "source": "a", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/oneof-in-source.num b/internal/interpreter/script-tests/oneof-in-source.num new file mode 100644 index 00000000..503e5e55 --- /dev/null +++ b/internal/interpreter/script-tests/oneof-in-source.num @@ -0,0 +1,7 @@ +send [GEM 15] ( + source = oneof { + @a allowing overdraft up to [GEM 14] + @world + } + destination = @dest +) diff --git a/internal/interpreter/script-tests/oneof-in-source.num.specs.json b/internal/interpreter/script-tests/oneof-in-source.num.specs.json new file mode 100644 index 00000000..814610a8 --- /dev/null +++ b/internal/interpreter/script-tests/oneof-in-source.num.specs.json @@ -0,0 +1,17 @@ +{ + "$schema": "../../../specs.schema.json", + "featureFlags": ["experimental-oneof"], + "testCases": [ + { + "it": "oneof in source", + "expect.postings": [ + { + "asset": "GEM", + "amount": 15, + "source": "world", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/oneof-singleton.num b/internal/interpreter/script-tests/oneof-singleton.num new file mode 100644 index 00000000..b226ec83 --- /dev/null +++ b/internal/interpreter/script-tests/oneof-singleton.num @@ -0,0 +1,4 @@ +send [GEM 10] ( + source = oneof { @a } + destination = @dest +) diff --git a/internal/interpreter/script-tests/oneof-singleton.num.specs.json b/internal/interpreter/script-tests/oneof-singleton.num.specs.json new file mode 100644 index 00000000..3b11530f --- /dev/null +++ b/internal/interpreter/script-tests/oneof-singleton.num.specs.json @@ -0,0 +1,22 @@ +{ + "$schema": "../../../specs.schema.json", + "featureFlags": ["experimental-oneof"], + "testCases": [ + { + "it": "oneof singleton", + "balances": { + "a": { + "GEM": 10 + } + }, + "expect.postings": [ + { + "asset": "GEM", + "amount": 10, + "source": "a", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/overdraft-in-send-all-when-noop.num b/internal/interpreter/script-tests/overdraft-in-send-all-when-noop.num new file mode 100644 index 00000000..1d92c901 --- /dev/null +++ b/internal/interpreter/script-tests/overdraft-in-send-all-when-noop.num @@ -0,0 +1,4 @@ +send [USD/2 *] ( + source = @src allowing overdraft up to [USD/2 10] + destination = @dest +) diff --git a/internal/interpreter/script-tests/overdraft-in-send-all-when-noop.num.specs.json b/internal/interpreter/script-tests/overdraft-in-send-all-when-noop.num.specs.json new file mode 100644 index 00000000..871855f4 --- /dev/null +++ b/internal/interpreter/script-tests/overdraft-in-send-all-when-noop.num.specs.json @@ -0,0 +1,19 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "sends all with overdraft when small balance", + "balances": { + "src": { "USD/2": 1 } + }, + "expect.postings": [ + { + "asset": "USD/2", + "amount": 11, + "source": "src", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/overdraft-in-send-all.num b/internal/interpreter/script-tests/overdraft-in-send-all.num new file mode 100644 index 00000000..1d92c901 --- /dev/null +++ b/internal/interpreter/script-tests/overdraft-in-send-all.num @@ -0,0 +1,4 @@ +send [USD/2 *] ( + source = @src allowing overdraft up to [USD/2 10] + destination = @dest +) diff --git a/internal/interpreter/script-tests/overdraft-in-send-all.num.specs.json b/internal/interpreter/script-tests/overdraft-in-send-all.num.specs.json new file mode 100644 index 00000000..c5fcc2db --- /dev/null +++ b/internal/interpreter/script-tests/overdraft-in-send-all.num.specs.json @@ -0,0 +1,19 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "sends all with overdraft", + "balances": { + "src": { "USD/2": 1000 } + }, + "expect.postings": [ + { + "asset": "USD/2", + "amount": 1010, + "source": "src", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/overdraft-not-enough-funds.num b/internal/interpreter/script-tests/overdraft-not-enough-funds.num new file mode 100644 index 00000000..17de8f9b --- /dev/null +++ b/internal/interpreter/script-tests/overdraft-not-enough-funds.num @@ -0,0 +1,9 @@ +send [USD/2 2200] ( + source = { + // let the user pay with their credit account first, + @users:2345:credit allowing overdraft up to [USD/2 1000] + // then, use their main balance + @users:2345:main + } + destination = @payments:4567 + ) diff --git a/internal/interpreter/script-tests/overdraft-not-enough-funds.num.specs.json b/internal/interpreter/script-tests/overdraft-not-enough-funds.num.specs.json new file mode 100644 index 00000000..9d38a053 --- /dev/null +++ b/internal/interpreter/script-tests/overdraft-not-enough-funds.num.specs.json @@ -0,0 +1,27 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "handles overdraft not enough funds", + "balances": { + "users:2345:main": { + "USD/2": 8000 + } + }, + "expect.postings": [ + { + "asset": "USD/2", + "amount": 1000, + "source": "users:2345:credit", + "destination": "payments:4567" + }, + { + "asset": "USD/2", + "amount": 1200, + "source": "users:2345:main", + "destination": "payments:4567" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/overdraft-when-enough-funds.num b/internal/interpreter/script-tests/overdraft-when-enough-funds.num new file mode 100644 index 00000000..53dc7c1a --- /dev/null +++ b/internal/interpreter/script-tests/overdraft-when-enough-funds.num @@ -0,0 +1,4 @@ +send [COIN 100] ( + source = @users:1234 allowing overdraft up to [COIN 100] + destination = @dest +) diff --git a/internal/interpreter/script-tests/overdraft-when-enough-funds.num.specs.json b/internal/interpreter/script-tests/overdraft-when-enough-funds.num.specs.json new file mode 100644 index 00000000..44ac8ece --- /dev/null +++ b/internal/interpreter/script-tests/overdraft-when-enough-funds.num.specs.json @@ -0,0 +1,16 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "handles overdraft when enough funds", + "expect.postings": [ + { + "asset": "COIN", + "amount": 100, + "source": "users:1234", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/overdraft-when-negative-balance-in-send-all.num b/internal/interpreter/script-tests/overdraft-when-negative-balance-in-send-all.num new file mode 100644 index 00000000..bc00f418 --- /dev/null +++ b/internal/interpreter/script-tests/overdraft-when-negative-balance-in-send-all.num @@ -0,0 +1,6 @@ +send [COIN *] ( + source = { + @s allowing overdraft up to [COIN 2] + } + destination = @dest +) diff --git a/internal/interpreter/script-tests/overdraft-when-negative-balance-in-send-all.num.specs.json b/internal/interpreter/script-tests/overdraft-when-negative-balance-in-send-all.num.specs.json new file mode 100644 index 00000000..3b25daa0 --- /dev/null +++ b/internal/interpreter/script-tests/overdraft-when-negative-balance-in-send-all.num.specs.json @@ -0,0 +1,21 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "overdraft when negative balance in send all", + "balances": { + "s": { + "COIN": -1 + } + }, + "expect.postings": [ + { + "asset": "COIN", + "amount": 1, + "source": "s", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/overdraft-when-negative-balance.num b/internal/interpreter/script-tests/overdraft-when-negative-balance.num new file mode 100644 index 00000000..9cdf11de --- /dev/null +++ b/internal/interpreter/script-tests/overdraft-when-negative-balance.num @@ -0,0 +1,6 @@ +send [COIN 10] ( + source = { + @s allowing overdraft up to [COIN -10] + } + destination = @dest +) diff --git a/internal/interpreter/script-tests/overdraft-when-negative-balance.num.specs.json b/internal/interpreter/script-tests/overdraft-when-negative-balance.num.specs.json new file mode 100644 index 00000000..fdf44813 --- /dev/null +++ b/internal/interpreter/script-tests/overdraft-when-negative-balance.num.specs.json @@ -0,0 +1,21 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "overdraft when negative balance", + "balances": { + "s": { + "COIN": 11 + } + }, + "expect.postings": [ + { + "asset": "COIN", + "amount": 10, + "source": "s", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/overdraft-when-negative-overdraft-in-send-all.num b/internal/interpreter/script-tests/overdraft-when-negative-overdraft-in-send-all.num new file mode 100644 index 00000000..7b73e20c --- /dev/null +++ b/internal/interpreter/script-tests/overdraft-when-negative-overdraft-in-send-all.num @@ -0,0 +1,6 @@ +send [COIN *] ( + source = { + @s allowing overdraft up to [COIN -10] + } + destination = @dest +) diff --git a/internal/interpreter/script-tests/overdraft-when-negative-overdraft-in-send-all.num.specs.json b/internal/interpreter/script-tests/overdraft-when-negative-overdraft-in-send-all.num.specs.json new file mode 100644 index 00000000..f95bf39c --- /dev/null +++ b/internal/interpreter/script-tests/overdraft-when-negative-overdraft-in-send-all.num.specs.json @@ -0,0 +1,21 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "overdraft when negative overdraft in send all", + "balances": { + "s": { + "COIN": 1 + } + }, + "expect.postings": [ + { + "asset": "COIN", + "amount": 1, + "source": "s", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/overdraft-when-not-enough-funds.num b/internal/interpreter/script-tests/overdraft-when-not-enough-funds.num new file mode 100644 index 00000000..c68bc0e0 --- /dev/null +++ b/internal/interpreter/script-tests/overdraft-when-not-enough-funds.num @@ -0,0 +1,4 @@ +send [COIN 100] ( + source = @users:1234 allowing overdraft up to [COIN 10] + destination = @dest +) diff --git a/internal/interpreter/script-tests/overdraft-when-not-enough-funds.num.specs.json b/internal/interpreter/script-tests/overdraft-when-not-enough-funds.num.specs.json new file mode 100644 index 00000000..93b5ef90 --- /dev/null +++ b/internal/interpreter/script-tests/overdraft-when-not-enough-funds.num.specs.json @@ -0,0 +1,14 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "handles overdraft when not enough funds", + "balances": { + "users:1234": { + "COIN": 1 + } + }, + "expect.missingFunds": true + } + ] +} diff --git a/internal/interpreter/script-tests/overdrafts-playground-example.num b/internal/interpreter/script-tests/overdrafts-playground-example.num new file mode 100644 index 00000000..de280a08 --- /dev/null +++ b/internal/interpreter/script-tests/overdrafts-playground-example.num @@ -0,0 +1,7 @@ +send [USD/2 100] ( + source = { + @users:001 + @users:002 allowing overdraft up to [USD/2 50] + } + destination = @platform +) diff --git a/internal/interpreter/script-tests/overdrafts-playground-example.num.specs.json b/internal/interpreter/script-tests/overdrafts-playground-example.num.specs.json new file mode 100644 index 00000000..29f8ab8e --- /dev/null +++ b/internal/interpreter/script-tests/overdrafts-playground-example.num.specs.json @@ -0,0 +1,30 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "handles overdrafts playground example", + "balances": { + "users:001": { + "USD/2": 25 + }, + "users:002": { + "USD/2": 25 + } + }, + "expect.postings": [ + { + "asset": "USD/2", + "amount": 25, + "source": "users:001", + "destination": "platform" + }, + { + "asset": "USD/2", + "amount": 75, + "source": "users:002", + "destination": "platform" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/override-account-meta.num b/internal/interpreter/script-tests/override-account-meta.num new file mode 100644 index 00000000..3acfc21e --- /dev/null +++ b/internal/interpreter/script-tests/override-account-meta.num @@ -0,0 +1,2 @@ +set_account_meta(@acc, "overridden", 100) +set_account_meta(@acc, "new", 2) diff --git a/internal/interpreter/script-tests/override-account-meta.num.specs.json b/internal/interpreter/script-tests/override-account-meta.num.specs.json new file mode 100644 index 00000000..e32dbb45 --- /dev/null +++ b/internal/interpreter/script-tests/override-account-meta.num.specs.json @@ -0,0 +1,20 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "outputs the metadata", + "accountsMeta": { + "acc": { + "initial": "0", + "overridden": "1" + } + }, + "expect.accountsMeta": { + "acc": { + "overridden": "100", + "new": "2" + } + } + } + ] +} diff --git a/internal/interpreter/script-tests/portion-syntax.num b/internal/interpreter/script-tests/portion-syntax.num new file mode 100644 index 00000000..f9d56413 --- /dev/null +++ b/internal/interpreter/script-tests/portion-syntax.num @@ -0,0 +1,10 @@ +vars { + portion $por +} +send [COIN 3] ( + source = @world + destination = { + $por to @a + remaining kept + } +) diff --git a/internal/interpreter/script-tests/portion-syntax.num.specs.json b/internal/interpreter/script-tests/portion-syntax.num.specs.json new file mode 100644 index 00000000..6c18937b --- /dev/null +++ b/internal/interpreter/script-tests/portion-syntax.num.specs.json @@ -0,0 +1,19 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "uses portion syntax correctly", + "vars": { + "por": "1/3" + }, + "expect.postings": [ + { + "asset": "COIN", + "amount": 1, + "source": "world", + "destination": "a" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/remaining-kept-in-send-all-inorder.num b/internal/interpreter/script-tests/remaining-kept-in-send-all-inorder.num new file mode 100644 index 00000000..ca6a5baa --- /dev/null +++ b/internal/interpreter/script-tests/remaining-kept-in-send-all-inorder.num @@ -0,0 +1,7 @@ +send [COIN *] ( + source = @src + destination = { + max [COIN 1] to @dest + remaining kept + } +) diff --git a/internal/interpreter/script-tests/remaining-kept-in-send-all-inorder.num.specs.json b/internal/interpreter/script-tests/remaining-kept-in-send-all-inorder.num.specs.json new file mode 100644 index 00000000..8c595450 --- /dev/null +++ b/internal/interpreter/script-tests/remaining-kept-in-send-all-inorder.num.specs.json @@ -0,0 +1,19 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "keeps remaining in send all with inorder", + "balances": { + "src": { "COIN": 1000 } + }, + "expect.postings": [ + { + "asset": "COIN", + "amount": 1, + "source": "src", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/remaining-kept-inorder.num b/internal/interpreter/script-tests/remaining-kept-inorder.num new file mode 100644 index 00000000..f93d767a --- /dev/null +++ b/internal/interpreter/script-tests/remaining-kept-inorder.num @@ -0,0 +1,7 @@ +send [COIN 100] ( + source = @world + destination = { + max [COIN 1] to @a + remaining kept + } +) diff --git a/internal/interpreter/script-tests/remaining-kept-inorder.num.specs.json b/internal/interpreter/script-tests/remaining-kept-inorder.num.specs.json new file mode 100644 index 00000000..d7052d7b --- /dev/null +++ b/internal/interpreter/script-tests/remaining-kept-inorder.num.specs.json @@ -0,0 +1,16 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "keeps remaining in order", + "expect.postings": [ + { + "asset": "COIN", + "amount": 1, + "source": "world", + "destination": "a" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/remaining-none-in-send-all.num b/internal/interpreter/script-tests/remaining-none-in-send-all.num new file mode 100644 index 00000000..45dcfefe --- /dev/null +++ b/internal/interpreter/script-tests/remaining-none-in-send-all.num @@ -0,0 +1,7 @@ +send [COIN *] ( + source = @src + destination = { + 100% to @dest + remaining to @rem + } +) diff --git a/internal/interpreter/script-tests/remaining-none-in-send-all.num.specs.json b/internal/interpreter/script-tests/remaining-none-in-send-all.num.specs.json new file mode 100644 index 00000000..3e74755a --- /dev/null +++ b/internal/interpreter/script-tests/remaining-none-in-send-all.num.specs.json @@ -0,0 +1,19 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "handles remaining none in send all", + "balances": { + "src": { "COIN": 100 } + }, + "expect.postings": [ + { + "asset": "COIN", + "amount": 100, + "source": "src", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/remaining-none.num b/internal/interpreter/script-tests/remaining-none.num new file mode 100644 index 00000000..0628b4e9 --- /dev/null +++ b/internal/interpreter/script-tests/remaining-none.num @@ -0,0 +1,7 @@ +send [COIN 100] ( + source = @world + destination = { + 100% to @dest + remaining to @rem + } +) diff --git a/internal/interpreter/script-tests/remaining-none.num.specs.json b/internal/interpreter/script-tests/remaining-none.num.specs.json new file mode 100644 index 00000000..3325e829 --- /dev/null +++ b/internal/interpreter/script-tests/remaining-none.num.specs.json @@ -0,0 +1,16 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "handles remaining when none left", + "expect.postings": [ + { + "asset": "COIN", + "amount": 100, + "source": "world", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/send-all-destination-allot-complex.num b/internal/interpreter/script-tests/send-all-destination-allot-complex.num new file mode 100644 index 00000000..84a52d9e --- /dev/null +++ b/internal/interpreter/script-tests/send-all-destination-allot-complex.num @@ -0,0 +1,10 @@ +send [USD/2 *] ( + source = { + @users:001 + @users:002 + } + destination = { + 1/3 to @d1 + 2/3 to @d2 + } +) diff --git a/internal/interpreter/script-tests/send-all-destination-allot-complex.num.specs.json b/internal/interpreter/script-tests/send-all-destination-allot-complex.num.specs.json new file mode 100644 index 00000000..18491ccd --- /dev/null +++ b/internal/interpreter/script-tests/send-all-destination-allot-complex.num.specs.json @@ -0,0 +1,32 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "sends all with complex destination allotment", + "balances": { + "users:001": { "USD/2": 15 }, + "users:002": { "USD/2": 15 } + }, + "expect.postings": [ + { + "asset": "USD/2", + "amount": 10, + "source": "users:001", + "destination": "d1" + }, + { + "asset": "USD/2", + "amount": 5, + "source": "users:001", + "destination": "d2" + }, + { + "asset": "USD/2", + "amount": 15, + "source": "users:002", + "destination": "d2" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/send-all-destination-allot.num b/internal/interpreter/script-tests/send-all-destination-allot.num new file mode 100644 index 00000000..5932646e --- /dev/null +++ b/internal/interpreter/script-tests/send-all-destination-allot.num @@ -0,0 +1,7 @@ +send [USD/2 *] ( + source = @users:001 + destination = { + 1/3 to @d1 + 2/3 to @d2 + } +) diff --git a/internal/interpreter/script-tests/send-all-destination-allot.num.specs.json b/internal/interpreter/script-tests/send-all-destination-allot.num.specs.json new file mode 100644 index 00000000..7f468c32 --- /dev/null +++ b/internal/interpreter/script-tests/send-all-destination-allot.num.specs.json @@ -0,0 +1,25 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "sends all with destination allotment", + "balances": { + "users:001": { "USD/2": 30 } + }, + "expect.postings": [ + { + "asset": "USD/2", + "amount": 10, + "source": "users:001", + "destination": "d1" + }, + { + "asset": "USD/2", + "amount": 20, + "source": "users:001", + "destination": "d2" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/send-all-many-max-in-dest.num b/internal/interpreter/script-tests/send-all-many-max-in-dest.num new file mode 100644 index 00000000..0ceed18c --- /dev/null +++ b/internal/interpreter/script-tests/send-all-many-max-in-dest.num @@ -0,0 +1,8 @@ +send [USD/2 *] ( + source = @src + destination = { + max [USD/2 10] to @d1 + max [USD/2 20] to @d2 + remaining to @d3 + } +) diff --git a/internal/interpreter/script-tests/send-all-many-max-in-dest.num.specs.json b/internal/interpreter/script-tests/send-all-many-max-in-dest.num.specs.json new file mode 100644 index 00000000..6079e52d --- /dev/null +++ b/internal/interpreter/script-tests/send-all-many-max-in-dest.num.specs.json @@ -0,0 +1,25 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "sends all with many max in destination", + "balances": { + "src": { "USD/2": 15 } + }, + "expect.postings": [ + { + "asset": "USD/2", + "amount": 10, + "source": "src", + "destination": "d1" + }, + { + "asset": "USD/2", + "amount": 5, + "source": "src", + "destination": "d2" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/send-all-max-in-dest.num b/internal/interpreter/script-tests/send-all-max-in-dest.num new file mode 100644 index 00000000..3708fbe3 --- /dev/null +++ b/internal/interpreter/script-tests/send-all-max-in-dest.num @@ -0,0 +1,7 @@ +send [USD/2 *] ( + source = @src + destination = { + max [USD/2 10] to @d1 + remaining to @d2 + } +) diff --git a/internal/interpreter/script-tests/send-all-max-in-dest.num.specs.json b/internal/interpreter/script-tests/send-all-max-in-dest.num.specs.json new file mode 100644 index 00000000..dd6ae394 --- /dev/null +++ b/internal/interpreter/script-tests/send-all-max-in-dest.num.specs.json @@ -0,0 +1,25 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "sends all with max in destination", + "balances": { + "src": { "USD/2": 100 } + }, + "expect.postings": [ + { + "asset": "USD/2", + "amount": 10, + "source": "src", + "destination": "d1" + }, + { + "asset": "USD/2", + "amount": 90, + "source": "src", + "destination": "d2" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/send-all-max-in-src.num b/internal/interpreter/script-tests/send-all-max-in-src.num new file mode 100644 index 00000000..9196d1b1 --- /dev/null +++ b/internal/interpreter/script-tests/send-all-max-in-src.num @@ -0,0 +1,7 @@ +send [USD/2 *] ( + source = { + max [USD/2 5] from @src1 + @src2 + } + destination = @dest +) diff --git a/internal/interpreter/script-tests/send-all-max-in-src.num.specs.json b/internal/interpreter/script-tests/send-all-max-in-src.num.specs.json new file mode 100644 index 00000000..860d2602 --- /dev/null +++ b/internal/interpreter/script-tests/send-all-max-in-src.num.specs.json @@ -0,0 +1,26 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "sends all with max in source", + "balances": { + "src1": { "USD/2": 100 }, + "src2": { "USD/2": 200 } + }, + "expect.postings": [ + { + "asset": "USD/2", + "amount": 5, + "source": "src1", + "destination": "dest" + }, + { + "asset": "USD/2", + "amount": 200, + "source": "src2", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/send-all-max-when-no-amount.num b/internal/interpreter/script-tests/send-all-max-when-no-amount.num new file mode 100644 index 00000000..55b6e37e --- /dev/null +++ b/internal/interpreter/script-tests/send-all-max-when-no-amount.num @@ -0,0 +1,4 @@ +send [USD/2 *] ( + source = max [USD/2 5] from @src + destination = @dest +) diff --git a/internal/interpreter/script-tests/send-all-max-when-no-amount.num.specs.json b/internal/interpreter/script-tests/send-all-max-when-no-amount.num.specs.json new file mode 100644 index 00000000..9a3abd94 --- /dev/null +++ b/internal/interpreter/script-tests/send-all-max-when-no-amount.num.specs.json @@ -0,0 +1,12 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "sends all with max when no amount available", + "balances": { + "src1": { "USD/2": 0 } + }, + "expect.postings": [] + } + ] +} diff --git a/internal/interpreter/script-tests/send-all-multi.num b/internal/interpreter/script-tests/send-all-multi.num new file mode 100644 index 00000000..509ddb95 --- /dev/null +++ b/internal/interpreter/script-tests/send-all-multi.num @@ -0,0 +1,7 @@ +send [USD/2 *] ( + source = { + @users:001:wallet + @users:001:credit + } + destination = @platform +) diff --git a/internal/interpreter/script-tests/send-all-multi.num.specs.json b/internal/interpreter/script-tests/send-all-multi.num.specs.json new file mode 100644 index 00000000..b480aadf --- /dev/null +++ b/internal/interpreter/script-tests/send-all-multi.num.specs.json @@ -0,0 +1,26 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "sends all from multiple sources", + "balances": { + "users:001:wallet": { "USD/2": 19 }, + "users:001:credit": { "USD/2": 22 } + }, + "expect.postings": [ + { + "asset": "USD/2", + "amount": 19, + "source": "users:001:wallet", + "destination": "platform" + }, + { + "asset": "USD/2", + "amount": 22, + "source": "users:001:credit", + "destination": "platform" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/send-all-negative-with-overdraft.num b/internal/interpreter/script-tests/send-all-negative-with-overdraft.num new file mode 100644 index 00000000..2c991870 --- /dev/null +++ b/internal/interpreter/script-tests/send-all-negative-with-overdraft.num @@ -0,0 +1,4 @@ +send [USD/2 *] ( + source = @users:001 allowing overdraft up to [USD/2 150] + destination = @platform +) diff --git a/internal/interpreter/script-tests/send-all-negative-with-overdraft.num.specs.json b/internal/interpreter/script-tests/send-all-negative-with-overdraft.num.specs.json new file mode 100644 index 00000000..6b2c2fd1 --- /dev/null +++ b/internal/interpreter/script-tests/send-all-negative-with-overdraft.num.specs.json @@ -0,0 +1,19 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "sends all with overdraft when balance is negative", + "balances": { + "users:001": { "USD/2": -100 } + }, + "expect.postings": [ + { + "asset": "USD/2", + "amount": 50, + "source": "users:001", + "destination": "platform" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/send-all-variable.num b/internal/interpreter/script-tests/send-all-variable.num new file mode 100644 index 00000000..537be3ff --- /dev/null +++ b/internal/interpreter/script-tests/send-all-variable.num @@ -0,0 +1,9 @@ +vars { + account $src + account $dest +} + +send [USD/2 *] ( + source = $src + destination = $dest +) diff --git a/internal/interpreter/script-tests/send-all-variable.num.specs.json b/internal/interpreter/script-tests/send-all-variable.num.specs.json new file mode 100644 index 00000000..32e851ed --- /dev/null +++ b/internal/interpreter/script-tests/send-all-variable.num.specs.json @@ -0,0 +1,23 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "sends all with variables", + "balances": { + "users:001": { "USD/2": 17 } + }, + "vars": { + "src": "users:001", + "dest": "platform" + }, + "expect.postings": [ + { + "asset": "USD/2", + "amount": 17, + "source": "users:001", + "destination": "platform" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/send-all-when-negative.num b/internal/interpreter/script-tests/send-all-when-negative.num new file mode 100644 index 00000000..f0af6030 --- /dev/null +++ b/internal/interpreter/script-tests/send-all-when-negative.num @@ -0,0 +1,4 @@ +send [USD/2 *] ( + source = @users:001 + destination = @platform +) diff --git a/internal/interpreter/script-tests/send-all-when-negative.num.specs.json b/internal/interpreter/script-tests/send-all-when-negative.num.specs.json new file mode 100644 index 00000000..a2ad4933 --- /dev/null +++ b/internal/interpreter/script-tests/send-all-when-negative.num.specs.json @@ -0,0 +1,12 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "sends all when balance is negative (no postings)", + "balances": { + "users:001": { "USD/2": -100 } + }, + "expect.postings": [] + } + ] +} diff --git a/internal/interpreter/script-tests/send-all.num b/internal/interpreter/script-tests/send-all.num new file mode 100644 index 00000000..f0af6030 --- /dev/null +++ b/internal/interpreter/script-tests/send-all.num @@ -0,0 +1,4 @@ +send [USD/2 *] ( + source = @users:001 + destination = @platform +) diff --git a/internal/interpreter/script-tests/send-all.num.specs.json b/internal/interpreter/script-tests/send-all.num.specs.json new file mode 100644 index 00000000..f42d959a --- /dev/null +++ b/internal/interpreter/script-tests/send-all.num.specs.json @@ -0,0 +1,19 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "sends all available funds", + "balances": { + "users:001": { "USD/2": 17 } + }, + "expect.postings": [ + { + "asset": "USD/2", + "amount": 17, + "source": "users:001", + "destination": "platform" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/send-when-negative-balance.num b/internal/interpreter/script-tests/send-when-negative-balance.num new file mode 100644 index 00000000..db162439 --- /dev/null +++ b/internal/interpreter/script-tests/send-when-negative-balance.num @@ -0,0 +1,7 @@ +send [COIN 10] ( + source = { + @s + @world + } + destination = @dest +) diff --git a/internal/interpreter/script-tests/send-when-negative-balance.num.specs.json b/internal/interpreter/script-tests/send-when-negative-balance.num.specs.json new file mode 100644 index 00000000..502bae96 --- /dev/null +++ b/internal/interpreter/script-tests/send-when-negative-balance.num.specs.json @@ -0,0 +1,21 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "send when negative balance", + "balances": { + "s": { + "COIN": -5 + } + }, + "expect.postings": [ + { + "asset": "COIN", + "amount": 10, + "source": "world", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/send-zero.num b/internal/interpreter/script-tests/send-zero.num new file mode 100644 index 00000000..8ad51351 --- /dev/null +++ b/internal/interpreter/script-tests/send-zero.num @@ -0,0 +1,4 @@ +send [COIN 0] ( + source = @src + destination = @dest +) diff --git a/internal/interpreter/script-tests/send-zero.num.specs.json b/internal/interpreter/script-tests/send-zero.num.specs.json new file mode 100644 index 00000000..19ef5114 --- /dev/null +++ b/internal/interpreter/script-tests/send-zero.num.specs.json @@ -0,0 +1,9 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "handles sending zero amount", + "expect.postings": [] + } + ] +} diff --git a/internal/interpreter/script-tests/send.num b/internal/interpreter/script-tests/send.num new file mode 100644 index 00000000..ab0efb5b --- /dev/null +++ b/internal/interpreter/script-tests/send.num @@ -0,0 +1,4 @@ +send [EUR/2 100] ( + source=@alice + destination=@bob +) diff --git a/internal/interpreter/script-tests/send.num.specs.json b/internal/interpreter/script-tests/send.num.specs.json new file mode 100644 index 00000000..ea1294e2 --- /dev/null +++ b/internal/interpreter/script-tests/send.num.specs.json @@ -0,0 +1,19 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "sends money from alice to bob", + "balances": { + "alice": { "EUR/2": 100 } + }, + "expect.postings": [ + { + "asset": "EUR/2", + "amount": 100, + "source": "alice", + "destination": "bob" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/set-account-meta.num b/internal/interpreter/script-tests/set-account-meta.num new file mode 100644 index 00000000..e6d499da --- /dev/null +++ b/internal/interpreter/script-tests/set-account-meta.num @@ -0,0 +1,6 @@ +set_account_meta(@acc, "num", 42) +set_account_meta(@acc, "str", "abc") +set_account_meta(@acc, "asset", COIN) +set_account_meta(@acc, "account", @acc) +set_account_meta(@acc, "portion", 2/7) +set_account_meta(@acc, "portion-perc", 1%) diff --git a/internal/interpreter/script-tests/set-account-meta.num.specs.json b/internal/interpreter/script-tests/set-account-meta.num.specs.json new file mode 100644 index 00000000..ee12aa1f --- /dev/null +++ b/internal/interpreter/script-tests/set-account-meta.num.specs.json @@ -0,0 +1,18 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "outputs the metadata", + "expect.accountsMeta": { + "acc": { + "num": "42", + "str": "abc", + "asset": "COIN", + "account": "acc", + "portion": "2/7", + "portion-perc": "1/100" + } + } + } + ] +} diff --git a/internal/interpreter/script-tests/set-tx-meta.num b/internal/interpreter/script-tests/set-tx-meta.num new file mode 100644 index 00000000..f3bf0af1 --- /dev/null +++ b/internal/interpreter/script-tests/set-tx-meta.num @@ -0,0 +1,5 @@ +set_tx_meta("num", 42) +set_tx_meta("str", "abc") +set_tx_meta("asset", COIN) +set_tx_meta("account", @acc) +set_tx_meta("portion", 12%) diff --git a/internal/interpreter/script-tests/set-tx-meta.num.specs.json b/internal/interpreter/script-tests/set-tx-meta.num.specs.json new file mode 100644 index 00000000..184ac584 --- /dev/null +++ b/internal/interpreter/script-tests/set-tx-meta.num.specs.json @@ -0,0 +1,15 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "outputs the metadata", + "expect.txMeta": { + "num": "42", + "str": "abc", + "asset": "COIN", + "account": "acc", + "portion": "3/25" + } + } + ] +} diff --git a/internal/interpreter/script-tests/source-allotment-invalid-amt.num b/internal/interpreter/script-tests/source-allotment-invalid-amt.num new file mode 100644 index 00000000..ca2b39eb --- /dev/null +++ b/internal/interpreter/script-tests/source-allotment-invalid-amt.num @@ -0,0 +1,7 @@ +send [COIN 100] ( + source = { + 10% from @a + remaining from @world + } + destination = @d +) diff --git a/internal/interpreter/script-tests/source-allotment-invalid-amt.num.specs.json b/internal/interpreter/script-tests/source-allotment-invalid-amt.num.specs.json new file mode 100644 index 00000000..717bbd38 --- /dev/null +++ b/internal/interpreter/script-tests/source-allotment-invalid-amt.num.specs.json @@ -0,0 +1,12 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "handles source allotment with insufficient funds", + "balances": { + "a": { "COIN": 1 } + }, + "expect.missingFunds": true + } + ] +} diff --git a/internal/interpreter/script-tests/source-allotment.num b/internal/interpreter/script-tests/source-allotment.num new file mode 100644 index 00000000..a8709cbe --- /dev/null +++ b/internal/interpreter/script-tests/source-allotment.num @@ -0,0 +1,8 @@ +send [COIN 100] ( + source = { + 60% from @a + 35.5% from @b + 4.5% from @c + } + destination = @d +) diff --git a/internal/interpreter/script-tests/source-allotment.num.specs.json b/internal/interpreter/script-tests/source-allotment.num.specs.json new file mode 100644 index 00000000..b903d65a --- /dev/null +++ b/internal/interpreter/script-tests/source-allotment.num.specs.json @@ -0,0 +1,33 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "uses source allotment with percentages", + "balances": { + "a": { "COIN": 100 }, + "b": { "COIN": 100 }, + "c": { "COIN": 100 } + }, + "expect.postings": [ + { + "asset": "COIN", + "amount": 61, + "source": "a", + "destination": "d" + }, + { + "asset": "COIN", + "amount": 35, + "source": "b", + "destination": "d" + }, + { + "asset": "COIN", + "amount": 4, + "source": "c", + "destination": "d" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/source-complex.num b/internal/interpreter/script-tests/source-complex.num new file mode 100644 index 00000000..54a1c220 --- /dev/null +++ b/internal/interpreter/script-tests/source-complex.num @@ -0,0 +1,14 @@ +vars { + monetary $max +} +send [COIN 200] ( + source = { + 50% from { + max [COIN 4] from @a + @b + @c + } + remaining from max $max from @d + } + destination = @platform +) diff --git a/internal/interpreter/script-tests/source-complex.num.specs.json b/internal/interpreter/script-tests/source-complex.num.specs.json new file mode 100644 index 00000000..b5502e39 --- /dev/null +++ b/internal/interpreter/script-tests/source-complex.num.specs.json @@ -0,0 +1,43 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "handles complex source structure", + "balances": { + "a": { "COIN": 1000 }, + "b": { "COIN": 40 }, + "c": { "COIN": 1000 }, + "d": { "COIN": 1000 } + }, + "vars": { + "max": "COIN 120" + }, + "expect.postings": [ + { + "asset": "COIN", + "amount": 4, + "source": "a", + "destination": "platform" + }, + { + "asset": "COIN", + "amount": 40, + "source": "b", + "destination": "platform" + }, + { + "asset": "COIN", + "amount": 56, + "source": "c", + "destination": "platform" + }, + { + "asset": "COIN", + "amount": 100, + "source": "d", + "destination": "platform" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/source-overlapping.num b/internal/interpreter/script-tests/source-overlapping.num new file mode 100644 index 00000000..dceb1c90 --- /dev/null +++ b/internal/interpreter/script-tests/source-overlapping.num @@ -0,0 +1,11 @@ +send [COIN 99] ( + source = { + 15% from { + @b + @a + } + 30% from @a + remaining from @a + } + destination = @world +) diff --git a/internal/interpreter/script-tests/source-overlapping.num.specs.json b/internal/interpreter/script-tests/source-overlapping.num.specs.json new file mode 100644 index 00000000..7ad7f510 --- /dev/null +++ b/internal/interpreter/script-tests/source-overlapping.num.specs.json @@ -0,0 +1,26 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "handles overlapping source accounts", + "balances": { + "a": { "COIN": 99 }, + "b": { "COIN": 3 } + }, + "expect.postings": [ + { + "asset": "COIN", + "amount": 3, + "source": "b", + "destination": "world" + }, + { + "asset": "COIN", + "amount": 96, + "source": "a", + "destination": "world" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/source.num b/internal/interpreter/script-tests/source.num new file mode 100644 index 00000000..d3f605a7 --- /dev/null +++ b/internal/interpreter/script-tests/source.num @@ -0,0 +1,12 @@ +vars { + account $balance + account $payment + account $seller +} +send [GEM 15] ( + source = { + $balance + $payment + } + destination = $seller +) diff --git a/internal/interpreter/script-tests/source.num.specs.json b/internal/interpreter/script-tests/source.num.specs.json new file mode 100644 index 00000000..394af424 --- /dev/null +++ b/internal/interpreter/script-tests/source.num.specs.json @@ -0,0 +1,31 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "uses multiple sources", + "balances": { + "users:001": { "GEM": 3 }, + "payments:001": { "GEM": 12 } + }, + "vars": { + "balance": "users:001", + "payment": "payments:001", + "seller": "users:002" + }, + "expect.postings": [ + { + "asset": "GEM", + "amount": 3, + "source": "users:001", + "destination": "users:002" + }, + { + "asset": "GEM", + "amount": 12, + "source": "payments:001", + "destination": "users:002" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/sub-monetaries.num b/internal/interpreter/script-tests/sub-monetaries.num new file mode 100644 index 00000000..06d6957a --- /dev/null +++ b/internal/interpreter/script-tests/sub-monetaries.num @@ -0,0 +1 @@ +set_tx_meta("k", [USD/2 10] - [USD/2 3]) diff --git a/internal/interpreter/script-tests/sub-monetaries.num.specs.json b/internal/interpreter/script-tests/sub-monetaries.num.specs.json new file mode 100644 index 00000000..61749282 --- /dev/null +++ b/internal/interpreter/script-tests/sub-monetaries.num.specs.json @@ -0,0 +1,11 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "subtracts monetaries", + "expect.txMeta": { + "k": "USD/2 7" + } + } + ] +} diff --git a/internal/interpreter/script-tests/sub-numbers.num b/internal/interpreter/script-tests/sub-numbers.num new file mode 100644 index 00000000..a368c05f --- /dev/null +++ b/internal/interpreter/script-tests/sub-numbers.num @@ -0,0 +1 @@ +set_tx_meta("k", 10 - 1) diff --git a/internal/interpreter/script-tests/sub-numbers.num.specs.json b/internal/interpreter/script-tests/sub-numbers.num.specs.json new file mode 100644 index 00000000..6ff649c6 --- /dev/null +++ b/internal/interpreter/script-tests/sub-numbers.num.specs.json @@ -0,0 +1,11 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "subtracts numbers", + "expect.txMeta": { + "k": "9" + } + } + ] +} diff --git a/internal/interpreter/script-tests/track-balances-send-all.num b/internal/interpreter/script-tests/track-balances-send-all.num new file mode 100644 index 00000000..e7f90ffe --- /dev/null +++ b/internal/interpreter/script-tests/track-balances-send-all.num @@ -0,0 +1,8 @@ +send [COIN *] ( + source = @src + destination = @dest1 +) +send [COIN *] ( + source = @src + destination = @dest2 +) diff --git a/internal/interpreter/script-tests/track-balances-send-all.num.specs.json b/internal/interpreter/script-tests/track-balances-send-all.num.specs.json new file mode 100644 index 00000000..ccbe9fe0 --- /dev/null +++ b/internal/interpreter/script-tests/track-balances-send-all.num.specs.json @@ -0,0 +1,19 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "tracks balances with send all operations", + "balances": { + "src": { "COIN": 42 } + }, + "expect.postings": [ + { + "asset": "COIN", + "amount": 42, + "source": "src", + "destination": "dest1" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/track-balances-tricky.num b/internal/interpreter/script-tests/track-balances-tricky.num new file mode 100644 index 00000000..c00c9a98 --- /dev/null +++ b/internal/interpreter/script-tests/track-balances-tricky.num @@ -0,0 +1,7 @@ +send [COIN *] ( + source = @foo + destination = { + max [COIN 10] to @bar + remaining to @baz + } +) diff --git a/internal/interpreter/script-tests/track-balances-tricky.num.specs.json b/internal/interpreter/script-tests/track-balances-tricky.num.specs.json new file mode 100644 index 00000000..a4bc35f2 --- /dev/null +++ b/internal/interpreter/script-tests/track-balances-tricky.num.specs.json @@ -0,0 +1,27 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "handles track balances tricky", + "balances": { + "foo": { + "COIN": 100 + } + }, + "expect.postings": [ + { + "asset": "COIN", + "amount": 10, + "source": "foo", + "destination": "bar" + }, + { + "asset": "COIN", + "amount": 90, + "source": "foo", + "destination": "baz" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/track-balances.num b/internal/interpreter/script-tests/track-balances.num new file mode 100644 index 00000000..8eaf6387 --- /dev/null +++ b/internal/interpreter/script-tests/track-balances.num @@ -0,0 +1,8 @@ +send [COIN 50] ( + source = @world + destination = @a +) +send [COIN 100] ( + source = @a + destination = @b +) diff --git a/internal/interpreter/script-tests/track-balances.num.specs.json b/internal/interpreter/script-tests/track-balances.num.specs.json new file mode 100644 index 00000000..131338b3 --- /dev/null +++ b/internal/interpreter/script-tests/track-balances.num.specs.json @@ -0,0 +1,25 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "tracks balances across multiple sends", + "balances": { + "a": { "COIN": 50 } + }, + "expect.postings": [ + { + "asset": "COIN", + "amount": 50, + "source": "world", + "destination": "a" + }, + { + "asset": "COIN", + "amount": 100, + "source": "a", + "destination": "b" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/track-balances2.num b/internal/interpreter/script-tests/track-balances2.num new file mode 100644 index 00000000..a9b0e14f --- /dev/null +++ b/internal/interpreter/script-tests/track-balances2.num @@ -0,0 +1,8 @@ +send [COIN 50] ( + source = @a + destination = @z +) +send [COIN 50] ( + source = @a + destination = @z +) diff --git a/internal/interpreter/script-tests/track-balances2.num.specs.json b/internal/interpreter/script-tests/track-balances2.num.specs.json new file mode 100644 index 00000000..24c8a555 --- /dev/null +++ b/internal/interpreter/script-tests/track-balances2.num.specs.json @@ -0,0 +1,12 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "tracks balances with insufficient funds", + "balances": { + "a": { "COIN": 60 } + }, + "expect.missingFunds": true + } + ] +} diff --git a/internal/interpreter/script-tests/track-balances3.num b/internal/interpreter/script-tests/track-balances3.num new file mode 100644 index 00000000..c68f1a6f --- /dev/null +++ b/internal/interpreter/script-tests/track-balances3.num @@ -0,0 +1,11 @@ +send [COIN *] ( + source = @foo + destination = { + max [COIN 1000] to @bar + remaining kept + } +) +send [COIN *] ( + source = @foo + destination = @bar +) diff --git a/internal/interpreter/script-tests/track-balances3.num.specs.json b/internal/interpreter/script-tests/track-balances3.num.specs.json new file mode 100644 index 00000000..c8fd11b2 --- /dev/null +++ b/internal/interpreter/script-tests/track-balances3.num.specs.json @@ -0,0 +1,25 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "tracks balances with kept and remaining", + "balances": { + "foo": { "COIN": 2000 } + }, + "expect.postings": [ + { + "asset": "COIN", + "amount": 1000, + "source": "foo", + "destination": "bar" + }, + { + "asset": "COIN", + "amount": 1000, + "source": "foo", + "destination": "bar" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/unbounded-overdraft-when-not-enough-funds.num b/internal/interpreter/script-tests/unbounded-overdraft-when-not-enough-funds.num new file mode 100644 index 00000000..2e6a8d93 --- /dev/null +++ b/internal/interpreter/script-tests/unbounded-overdraft-when-not-enough-funds.num @@ -0,0 +1,4 @@ +send [USD/2 100] ( + source = @users:001 allowing unbounded overdraft + destination = @platform +) diff --git a/internal/interpreter/script-tests/unbounded-overdraft-when-not-enough-funds.num.specs.json b/internal/interpreter/script-tests/unbounded-overdraft-when-not-enough-funds.num.specs.json new file mode 100644 index 00000000..fdf92817 --- /dev/null +++ b/internal/interpreter/script-tests/unbounded-overdraft-when-not-enough-funds.num.specs.json @@ -0,0 +1,21 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "handles unbounded overdraft when not enough funds", + "balances": { + "users:001": { + "USD/2": 25 + } + }, + "expect.postings": [ + { + "asset": "USD/2", + "amount": 100, + "source": "users:001", + "destination": "platform" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/update-balances-with-oneof.num b/internal/interpreter/script-tests/update-balances-with-oneof.num new file mode 100644 index 00000000..3de9e46a --- /dev/null +++ b/internal/interpreter/script-tests/update-balances-with-oneof.num @@ -0,0 +1,7 @@ +send [USD 200] ( + source = oneof { + @alice + {@alice @world} + } + destination = @dest +) diff --git a/internal/interpreter/script-tests/update-balances-with-oneof.num.specs.json b/internal/interpreter/script-tests/update-balances-with-oneof.num.specs.json new file mode 100644 index 00000000..ad063d57 --- /dev/null +++ b/internal/interpreter/script-tests/update-balances-with-oneof.num.specs.json @@ -0,0 +1,28 @@ +{ + "$schema": "../../../specs.schema.json", + "featureFlags": ["experimental-oneof"], + "testCases": [ + { + "it": "update balances with oneof", + "balances": { + "alice": { + "USD": 100 + } + }, + "expect.postings": [ + { + "asset": "USD", + "amount": 100, + "source": "alice", + "destination": "dest" + }, + { + "asset": "USD", + "amount": 100, + "source": "world", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/update-balances.num b/internal/interpreter/script-tests/update-balances.num new file mode 100644 index 00000000..33961d71 --- /dev/null +++ b/internal/interpreter/script-tests/update-balances.num @@ -0,0 +1,8 @@ +send [USD 200] ( + source = { + @alice + @alice + @world + } + destination = @dest +) diff --git a/internal/interpreter/script-tests/update-balances.num.specs.json b/internal/interpreter/script-tests/update-balances.num.specs.json new file mode 100644 index 00000000..b457179a --- /dev/null +++ b/internal/interpreter/script-tests/update-balances.num.specs.json @@ -0,0 +1,27 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "update balances", + "balances": { + "alice": { + "USD": 100 + } + }, + "expect.postings": [ + { + "asset": "USD", + "amount": 100, + "source": "alice", + "destination": "dest" + }, + { + "asset": "USD", + "amount": 100, + "source": "world", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/use-balance-twice.num b/internal/interpreter/script-tests/use-balance-twice.num new file mode 100644 index 00000000..c85976c4 --- /dev/null +++ b/internal/interpreter/script-tests/use-balance-twice.num @@ -0,0 +1,6 @@ +vars { monetary $v = balance(@src, COIN) } + +send $v ( + source = @src + destination = @dest +) diff --git a/internal/interpreter/script-tests/use-balance-twice.num.specs.json b/internal/interpreter/script-tests/use-balance-twice.num.specs.json new file mode 100644 index 00000000..589504ff --- /dev/null +++ b/internal/interpreter/script-tests/use-balance-twice.num.specs.json @@ -0,0 +1,21 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "uses balance twice", + "balances": { + "src": { + "COIN": 50 + } + }, + "expect.postings": [ + { + "asset": "COIN", + "amount": 50, + "source": "src", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/use-different-assets-with-same-source-account.num b/internal/interpreter/script-tests/use-different-assets-with-same-source-account.num new file mode 100644 index 00000000..98844c5f --- /dev/null +++ b/internal/interpreter/script-tests/use-different-assets-with-same-source-account.num @@ -0,0 +1,11 @@ +vars { + account $a_account +} +send [A 100] ( + source = $a_account allowing unbounded overdraft + destination = @account1 +) +send [B 100] ( + source = @world + destination = @account2 +) diff --git a/internal/interpreter/script-tests/use-different-assets-with-same-source-account.num.specs.json b/internal/interpreter/script-tests/use-different-assets-with-same-source-account.num.specs.json new file mode 100644 index 00000000..3976c8ba --- /dev/null +++ b/internal/interpreter/script-tests/use-different-assets-with-same-source-account.num.specs.json @@ -0,0 +1,33 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "uses different assets with same source account", + "vars": { + "a_account": "world" + }, + "balances": { + "account1": { + "A": 100 + }, + "account2": { + "B": 100 + } + }, + "expect.postings": [ + { + "asset": "A", + "amount": 100, + "source": "world", + "destination": "account1" + }, + { + "asset": "B", + "amount": 100, + "source": "world", + "destination": "account2" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/variable-asset.num b/internal/interpreter/script-tests/variable-asset.num new file mode 100644 index 00000000..64965b27 --- /dev/null +++ b/internal/interpreter/script-tests/variable-asset.num @@ -0,0 +1,20 @@ +vars { + asset $ass + monetary $bal = balance(@alice, $ass) +} + +send [$ass 15] ( + source = { + @alice + @bob + } + destination = @swap +) + +send [$ass *] ( + source = @swap + destination = { + max $bal to @alice_2 + remaining to @bob_2 + } +) diff --git a/internal/interpreter/script-tests/variable-asset.num.specs.json b/internal/interpreter/script-tests/variable-asset.num.specs.json new file mode 100644 index 00000000..2f185733 --- /dev/null +++ b/internal/interpreter/script-tests/variable-asset.num.specs.json @@ -0,0 +1,45 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "handles variable asset", + "vars": { + "ass": "USD" + }, + "balances": { + "alice": { + "USD": 10 + }, + "bob": { + "USD": 10 + } + }, + "expect.postings": [ + { + "asset": "USD", + "amount": 10, + "source": "alice", + "destination": "swap" + }, + { + "asset": "USD", + "amount": 5, + "source": "bob", + "destination": "swap" + }, + { + "asset": "USD", + "amount": 10, + "source": "swap", + "destination": "alice_2" + }, + { + "asset": "USD", + "amount": 5, + "source": "swap", + "destination": "bob_2" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/variable-portion-part.num b/internal/interpreter/script-tests/variable-portion-part.num new file mode 100644 index 00000000..607e594d --- /dev/null +++ b/internal/interpreter/script-tests/variable-portion-part.num @@ -0,0 +1,12 @@ +vars { + number $num + number $den +} + +send [COIN 9] ( + source = @world + destination = { + $num/3 to @a + 2/$den to @b + } +) diff --git a/internal/interpreter/script-tests/variable-portion-part.num.specs.json b/internal/interpreter/script-tests/variable-portion-part.num.specs.json new file mode 100644 index 00000000..2fd40d46 --- /dev/null +++ b/internal/interpreter/script-tests/variable-portion-part.num.specs.json @@ -0,0 +1,26 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "uses variable portion parts", + "vars": { + "num": "1", + "den": "3" + }, + "expect.postings": [ + { + "asset": "COIN", + "amount": 3, + "source": "world", + "destination": "a" + }, + { + "asset": "COIN", + "amount": 6, + "source": "world", + "destination": "b" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/variables-json.num b/internal/interpreter/script-tests/variables-json.num new file mode 100644 index 00000000..b714cb85 --- /dev/null +++ b/internal/interpreter/script-tests/variables-json.num @@ -0,0 +1,15 @@ +vars { + account $rider + account $driver + string $description + number $nb + asset $ass + portion $por +} +send [$ass 999] ( + source=$rider + destination=$driver +) +set_tx_meta("description", $description) +set_tx_meta("ride", $nb) +set_tx_meta("por", $por) diff --git a/internal/interpreter/script-tests/variables-json.num.specs.json b/internal/interpreter/script-tests/variables-json.num.specs.json new file mode 100644 index 00000000..1cdee167 --- /dev/null +++ b/internal/interpreter/script-tests/variables-json.num.specs.json @@ -0,0 +1,32 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "handles variables from JSON", + "balances": { + "users:001": { "EUR/2": 1000 } + }, + "vars": { + "por": "42%", + "rider": "users:001", + "driver": "users:002", + "description": "midnight ride", + "nb": "1", + "ass": "EUR/2" + }, + "expect.postings": [ + { + "asset": "EUR/2", + "amount": 999, + "source": "users:001", + "destination": "users:002" + } + ], + "expect.txMeta": { + "description": "midnight ride", + "ride": "1", + "por": "21/50" + } + } + ] +} diff --git a/internal/interpreter/script-tests/variables.num b/internal/interpreter/script-tests/variables.num new file mode 100644 index 00000000..54926cfb --- /dev/null +++ b/internal/interpreter/script-tests/variables.num @@ -0,0 +1,14 @@ +vars { + account $rider + account $driver + string $description + number $nb + asset $ass +} +send [$ass 999] ( + source = $rider + destination = $driver +) + +set_tx_meta("description", $description) +set_tx_meta("ride", $nb) diff --git a/internal/interpreter/script-tests/variables.num.specs.json b/internal/interpreter/script-tests/variables.num.specs.json new file mode 100644 index 00000000..850d9afa --- /dev/null +++ b/internal/interpreter/script-tests/variables.num.specs.json @@ -0,0 +1,30 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "emit the correct postings", + "balances": { + "users:001": { "EUR/2": 1000 } + }, + "vars": { + "rider": "users:001", + "driver": "users:002", + "description": "midnight ride", + "nb": "1", + "ass": "EUR/2" + }, + "expect.txMeta": { + "description": "midnight ride", + "ride": "1" + }, + "expect.postings": [ + { + "asset": "EUR/2", + "amount": 999, + "source": "users:001", + "destination": "users:002" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/world-source.num b/internal/interpreter/script-tests/world-source.num new file mode 100644 index 00000000..e6c2fd05 --- /dev/null +++ b/internal/interpreter/script-tests/world-source.num @@ -0,0 +1,7 @@ +send [GEM 15] ( + source = { + @a + @world + } + destination = @b +) diff --git a/internal/interpreter/script-tests/world-source.num.specs.json b/internal/interpreter/script-tests/world-source.num.specs.json new file mode 100644 index 00000000..081d489c --- /dev/null +++ b/internal/interpreter/script-tests/world-source.num.specs.json @@ -0,0 +1,25 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "uses world as fallback source", + "balances": { + "a": { "GEM": 1 } + }, + "expect.postings": [ + { + "asset": "GEM", + "amount": 1, + "source": "a", + "destination": "b" + }, + { + "asset": "GEM", + "amount": 14, + "source": "world", + "destination": "b" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/zero-postings-destination.num b/internal/interpreter/script-tests/zero-postings-destination.num new file mode 100644 index 00000000..925eaa42 --- /dev/null +++ b/internal/interpreter/script-tests/zero-postings-destination.num @@ -0,0 +1,7 @@ +send [COIN 100] ( + source = @world + destination = { + max [COIN 0] to @d1 + remaining to @d2 + } +) diff --git a/internal/interpreter/script-tests/zero-postings-destination.num.specs.json b/internal/interpreter/script-tests/zero-postings-destination.num.specs.json new file mode 100644 index 00000000..08abe79c --- /dev/null +++ b/internal/interpreter/script-tests/zero-postings-destination.num.specs.json @@ -0,0 +1,16 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "zero postings destination", + "expect.postings": [ + { + "asset": "COIN", + "amount": 100, + "source": "world", + "destination": "d2" + } + ] + } + ] +} diff --git a/internal/interpreter/script-tests/zero-postings-explicit-allotment.num b/internal/interpreter/script-tests/zero-postings-explicit-allotment.num new file mode 100644 index 00000000..07650363 --- /dev/null +++ b/internal/interpreter/script-tests/zero-postings-explicit-allotment.num @@ -0,0 +1,7 @@ +send [COIN 0] ( + source = { + 1/2 from @a + 1/2 from @b + } + destination = @dest +) diff --git a/internal/interpreter/script-tests/zero-postings-explicit-allotment.num.specs.json b/internal/interpreter/script-tests/zero-postings-explicit-allotment.num.specs.json new file mode 100644 index 00000000..6907a24b --- /dev/null +++ b/internal/interpreter/script-tests/zero-postings-explicit-allotment.num.specs.json @@ -0,0 +1,9 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "handles zero postings explicit allotment", + "expect.postings": [] + } + ] +} diff --git a/internal/interpreter/script-tests/zero-postings-explicit-inorder.num b/internal/interpreter/script-tests/zero-postings-explicit-inorder.num new file mode 100644 index 00000000..6eae5076 --- /dev/null +++ b/internal/interpreter/script-tests/zero-postings-explicit-inorder.num @@ -0,0 +1,8 @@ +send [COIN 0] ( + source = { + @a + @b + @c + } + destination = @dest +) diff --git a/internal/interpreter/script-tests/zero-postings-explicit-inorder.num.specs.json b/internal/interpreter/script-tests/zero-postings-explicit-inorder.num.specs.json new file mode 100644 index 00000000..6f9a79b2 --- /dev/null +++ b/internal/interpreter/script-tests/zero-postings-explicit-inorder.num.specs.json @@ -0,0 +1,9 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "handles zero postings explicit inorder", + "expect.postings": [] + } + ] +} diff --git a/internal/interpreter/script-tests/zero-postings.num b/internal/interpreter/script-tests/zero-postings.num new file mode 100644 index 00000000..6ec2c08f --- /dev/null +++ b/internal/interpreter/script-tests/zero-postings.num @@ -0,0 +1,7 @@ +send [COIN 100] ( + source = { + @a + @world + } + destination = @dest +) diff --git a/internal/interpreter/script-tests/zero-postings.num.specs.json b/internal/interpreter/script-tests/zero-postings.num.specs.json new file mode 100644 index 00000000..1fbd7d27 --- /dev/null +++ b/internal/interpreter/script-tests/zero-postings.num.specs.json @@ -0,0 +1,16 @@ +{ + "$schema": "../../../specs.schema.json", + "testCases": [ + { + "it": "handles zero postings", + "expect.postings": [ + { + "asset": "COIN", + "amount": 100, + "source": "world", + "destination": "dest" + } + ] + } + ] +} diff --git a/internal/interpreter/utils.go b/internal/interpreter/utils.go deleted file mode 100644 index a32bf667..00000000 --- a/internal/interpreter/utils.go +++ /dev/null @@ -1,11 +0,0 @@ -package interpreter - -func defaultMapGet[T any](m map[string]T, key string, getDefault func() T) T { - lookup, ok := m[key] - if !ok { - default_ := getDefault() - m[key] = default_ - return default_ - } - return lookup -} diff --git a/internal/specs_format/index.go b/internal/specs_format/index.go new file mode 100644 index 00000000..e267d6c4 --- /dev/null +++ b/internal/specs_format/index.go @@ -0,0 +1,257 @@ +package specs_format + +import ( + "context" + "math/big" + "reflect" + + "github.com/formancehq/numscript/internal/interpreter" + "github.com/formancehq/numscript/internal/parser" + "github.com/formancehq/numscript/internal/utils" +) + +// --- Specs: +type Specs struct { + FeatureFlags []string `json:"featureFlags,omitempty"` + Balances interpreter.Balances `json:"balances,omitempty"` + Vars interpreter.VariablesMap `json:"vars,omitempty"` + Meta interpreter.AccountsMetadata `json:"accountsMeta,omitempty"` + TestCases []TestCase `json:"testCases,omitempty"` +} + +type TestCase struct { + It string `json:"it"` + Balances interpreter.Balances `json:"balances,omitempty"` + Vars interpreter.VariablesMap `json:"vars,omitempty"` + Meta interpreter.AccountsMetadata `json:"accountsMeta,omitempty"` + + // Expectations + ExpectMissingFunds bool `json:"expect.missingFunds,omitempty"` + ExpectPostings []interpreter.Posting `json:"expect.postings"` + ExpectTxMeta map[string]string `json:"expect.txMeta,omitempty"` + ExpectAccountsMeta interpreter.AccountsMetadata `json:"expect.accountsMeta,omitempty"` + ExpectVolumes interpreter.Balances `json:"expect.volumes,omitempty"` + ExpectMovements Movements `json:"expect.movements,omitempty"` +} + +type TestCaseResult struct { + It string `json:"it"` + Pass bool `json:"pass"` + Balances interpreter.Balances `json:"balances"` + Vars interpreter.VariablesMap `json:"vars"` + Meta interpreter.AccountsMetadata `json:"accountsMeta"` + + // Assertions + FailedAssertions []AssertionMismatch[any] `json:"failedAssertions"` +} + +type SpecsResult struct { + // Invariants: total==passing+failing + Total uint `json:"total"` + Passing uint `json:"passing"` + Failing uint `json:"failing"` + Cases []TestCaseResult +} + +func runAssertion[T any](failedAssertions []AssertionMismatch[any], assertion string, expected T, got T, cmp func(T, T) bool) []AssertionMismatch[any] { + eq := cmp(expected, got) + if !eq { + return append(failedAssertions, AssertionMismatch[any]{ + Assertion: assertion, + Expected: expected, + Got: got, + }) + } + + return failedAssertions +} + +func Check(program parser.Program, specs Specs) (SpecsResult, interpreter.InterpreterError) { + specsResult := SpecsResult{} + + for _, testCase := range specs.TestCases { + meta := mergeAccountsMeta(specs.Meta, testCase.Meta) + balances := mergeBalances(specs.Balances, testCase.Balances) + vars := mergeVars(specs.Vars, testCase.Vars) + + specsResult.Total += 1 + + featureFlags := make(map[string]struct{}) + for _, flag := range specs.FeatureFlags { + featureFlags[flag] = struct{}{} + } + + result, err := interpreter.RunProgram( + context.Background(), + program, + vars, + interpreter.StaticStore{ + Meta: meta, + Balances: balances, + }, + featureFlags, + ) + + var failedAssertions []AssertionMismatch[any] + + if err != nil { + _, ok := err.(interpreter.MissingFundsErr) + if !ok { + return SpecsResult{}, err + } + + if !testCase.ExpectMissingFunds { + failedAssertions = append(failedAssertions, AssertionMismatch[any]{ + Assertion: "expect.missingFunds", + Expected: false, + Got: true, + }) + } + + } else { + + if testCase.ExpectMissingFunds { + failedAssertions = append(failedAssertions, AssertionMismatch[any]{ + Assertion: "expect.missingFunds", + Expected: true, + Got: false, + }) + } + + if testCase.ExpectPostings != nil { + failedAssertions = runAssertion[any](failedAssertions, + "expect.postings", + testCase.ExpectPostings, + result.Postings, + reflect.DeepEqual, + ) + } + + if testCase.ExpectTxMeta != nil { + metadata := map[string]string{} + for k, v := range result.Metadata { + metadata[k] = v.String() + } + failedAssertions = runAssertion[any](failedAssertions, + "expect.txMeta", + testCase.ExpectTxMeta, + metadata, + reflect.DeepEqual, + ) + } + + if testCase.ExpectAccountsMeta != nil { + failedAssertions = runAssertion[any](failedAssertions, + "expect.accountsMeta", + testCase.ExpectAccountsMeta, + result.AccountsMetadata, + reflect.DeepEqual, + ) + } + + if testCase.ExpectVolumes != nil { + failedAssertions = runAssertion(failedAssertions, + "expect.volumes", + testCase.ExpectVolumes, + getVolumes(result.Postings, balances), + interpreter.CompareBalances, + ) + } + + if testCase.ExpectMovements != nil { + failedAssertions = runAssertion[any](failedAssertions, + "expect.movements", + testCase.ExpectMovements, + getMovements(result.Postings), + reflect.DeepEqual, + ) + } + + } + + pass := len(failedAssertions) == 0 + if pass { + specsResult.Passing += 1 + } else { + specsResult.Failing += 1 + } + + specsResult.Cases = append(specsResult.Cases, TestCaseResult{ + It: testCase.It, + Pass: pass, + Meta: meta, + Balances: balances, + Vars: vars, + FailedAssertions: failedAssertions, + }) + } + + return specsResult, nil +} + +func mergeVars(v1 interpreter.VariablesMap, v2 interpreter.VariablesMap) interpreter.VariablesMap { + out := interpreter.VariablesMap{} + for k, v := range v1 { + out[k] = v + } + for k, v := range v2 { + out[k] = v + } + return out +} + +func mergeAccountsMeta(m1 interpreter.AccountsMetadata, m2 interpreter.AccountsMetadata) interpreter.AccountsMetadata { + out := m1.DeepClone() + out.Merge(m2) + return out +} + +func mergeBalances(b1 interpreter.Balances, b2 interpreter.Balances) interpreter.Balances { + out := b1.DeepClone() + out.Merge(b2) + return out +} + +type AssertionMismatch[T any] struct { + Assertion string `json:"assertion"` + Expected T `json:"expected,omitempty"` + Got T `json:"got,omitempty"` +} + +// TODO test +type Movements = map[string]map[string]map[string]*big.Int + +func getMovements(postings []interpreter.Posting) Movements { + m := Movements{} + + for _, posting := range postings { + assetsMap := utils.NestedMapGetOrPutDefault(m, posting.Source, posting.Destination, func() map[string]*big.Int { + return map[string]*big.Int{} + }) + + amt := utils.MapGetOrPutDefault(assetsMap, posting.Asset, func() *big.Int { + return new(big.Int) + }) + + amt.Add(amt, posting.Amount) + } + + return m +} + +func getVolumes(postings []interpreter.Posting, initialBalances interpreter.Balances) interpreter.Balances { + balances := initialBalances.DeepClone() + for _, posting := range postings { + sourceBalance := utils.NestedMapGetOrPutDefault(balances, posting.Source, posting.Asset, func() *big.Int { + return new(big.Int) + }) + sourceBalance.Sub(sourceBalance, posting.Amount) + + destinationBalance := utils.NestedMapGetOrPutDefault(balances, posting.Destination, posting.Asset, func() *big.Int { + return new(big.Int) + }) + destinationBalance.Add(destinationBalance, posting.Amount) + } + + return balances +} diff --git a/internal/specs_format/parse_test.go b/internal/specs_format/parse_test.go new file mode 100644 index 00000000..e5b990bd --- /dev/null +++ b/internal/specs_format/parse_test.go @@ -0,0 +1,76 @@ +package specs_format_test + +import ( + "encoding/json" + "math/big" + "testing" + + "github.com/formancehq/numscript/internal/interpreter" + "github.com/formancehq/numscript/internal/specs_format" + "github.com/stretchr/testify/require" +) + +func TestParseSpecs(t *testing.T) { + + raw := ` +{ + "balances": { + "alice": { "EUR": 200 } + }, + "vars": { + "amt": "200" + }, + "testCases": [ + { + "it": "d1", + "balances": { + "bob": { "EUR": 42 } + }, + "expect.postings": [ + { + "source": "src", + "destination": "dest", + "asset": "EUR", + "amount": 100 + } + ] + } + ] +} + + ` + + var specs specs_format.Specs + err := json.Unmarshal([]byte(raw), &specs) + require.Nil(t, err) + + require.Equal(t, specs_format.Specs{ + Balances: interpreter.Balances{ + "alice": { + "EUR": big.NewInt(200), + }, + }, + Vars: interpreter.VariablesMap{ + "amt": "200", + }, + TestCases: []specs_format.TestCase{ + { + It: "d1", + Balances: interpreter.Balances{ + "bob": { + "EUR": big.NewInt(42), + }, + }, + ExpectPostings: []interpreter.Posting{ + { + Source: "src", + Destination: "dest", + Asset: "EUR", + Amount: big.NewInt(100), + }, + }, + }, + }, + }, specs) + +} diff --git a/internal/specs_format/run_test.go b/internal/specs_format/run_test.go new file mode 100644 index 00000000..4a4e060b --- /dev/null +++ b/internal/specs_format/run_test.go @@ -0,0 +1,308 @@ +package specs_format_test + +import ( + "encoding/json" + "math/big" + "testing" + + "github.com/formancehq/numscript/internal/interpreter" + "github.com/formancehq/numscript/internal/parser" + "github.com/formancehq/numscript/internal/specs_format" + "github.com/stretchr/testify/require" +) + +var exampleProgram = parser.Parse(` + vars { + account $source + number $amount + } + + send [USD $amount] ( + source = $source + destination = @dest + ) +`) + +func TestRunSpecsSimple(t *testing.T) { + j := `{ + "testCases": [ + { + "it": "t1", + "vars": { "source": "src", "amount": "42" }, + "balances": { "src": { "USD": 9999 } }, + "expect.postings": [ + { "source": "src", "destination": "dest", "asset": "USD", "amount": 42 } + ] + } + ] + }` + + var specs specs_format.Specs + err := json.Unmarshal([]byte(j), &specs) + require.Nil(t, err) + + out, err := specs_format.Check(exampleProgram.Value, specs) + require.Nil(t, err) + + require.Equal(t, specs_format.SpecsResult{ + Total: 1, + Failing: 0, + Passing: 1, + Cases: []specs_format.TestCaseResult{ + { + It: "t1", + Pass: true, + Vars: interpreter.VariablesMap{ + "source": "src", + "amount": "42", + }, + Balances: interpreter.Balances{ + "src": interpreter.AccountBalance{ + "USD": big.NewInt(9999), + }, + }, + Meta: interpreter.AccountsMetadata{}, + FailedAssertions: nil, + // ExpectedPostings: []interpreter.Posting{ + // { + // Source: "src", + // Destination: "dest", + // Asset: "USD", + // Amount: big.NewInt(42), + // }, + // }, + // ActualPostings: []interpreter.Posting{ + // { + // Source: "src", + // Destination: "dest", + // Asset: "USD", + // Amount: big.NewInt(42), + // }, + // }, + }, + }, + }, out) + +} + +func TestRunSpecsMergeOuter(t *testing.T) { + j := `{ + "vars": { "source": "src", "amount": "42" }, + "balances": { "src": { "USD": 10 } }, + "testCases": [ + { + "vars": { "amount": "1" }, + "balances": { + "src": { "EUR": 2 }, + "dest": { "USD": 1 } + }, + "it": "t1", + "expect.postings": [ + { "source": "src", "destination": "dest", "asset": "USD", "amount": 1 } + ] + } + ] + }` + + var specs specs_format.Specs + err := json.Unmarshal([]byte(j), &specs) + require.Nil(t, err) + + out, err := specs_format.Check(exampleProgram.Value, specs) + require.Nil(t, err) + + require.Equal(t, specs_format.SpecsResult{ + Total: 1, + Failing: 0, + Passing: 1, + Cases: []specs_format.TestCaseResult{ + { + It: "t1", + Pass: true, + Vars: interpreter.VariablesMap{ + "source": "src", + "amount": "1", + }, + Meta: interpreter.AccountsMetadata{}, + Balances: interpreter.Balances{ + "src": interpreter.AccountBalance{ + "USD": big.NewInt(10), + "EUR": big.NewInt(2), + }, + "dest": interpreter.AccountBalance{ + "USD": big.NewInt(1), + }, + }, + FailedAssertions: nil, + // ExpectedPostings: []interpreter.Posting{ + // { + // Source: "src", + // Destination: "dest", + // Asset: "USD", + // Amount: big.NewInt(1), + // }, + // }, + // ActualPostings: []interpreter.Posting{ + // { + // Source: "src", + // Destination: "dest", + // Asset: "USD", + // Amount: big.NewInt(1), + // }, + // }, + }, + }, + }, out) + +} + +func TestRunWithMissingBalance(t *testing.T) { + j := `{ + "testCases": [ + { + "it": "t1", + "vars": { "source": "src", "amount": "42" }, + "balances": { "src": { "USD": 1 } }, + "expect.missingFunds": false, + "expect.postings": null + } + ] + }` + + var specs specs_format.Specs + err := json.Unmarshal([]byte(j), &specs) + require.Nil(t, err) + + out, err := specs_format.Check(exampleProgram.Value, specs) + require.Nil(t, err) + + require.Equal(t, specs_format.SpecsResult{ + Total: 1, + Failing: 1, + Passing: 0, + Cases: []specs_format.TestCaseResult{ + { + It: "t1", + Pass: false, + Vars: interpreter.VariablesMap{ + "source": "src", + "amount": "42", + }, + Balances: interpreter.Balances{ + "src": interpreter.AccountBalance{ + "USD": big.NewInt(1), + }, + }, + Meta: interpreter.AccountsMetadata{}, + FailedAssertions: []specs_format.AssertionMismatch[any]{ + { + Assertion: "expect.missingFunds", + Expected: false, + Got: true, + }, + }, + // ExpectedPostings: nil, + // ActualPostings: nil, + }, + }, + }, out) + +} + +func TestRunWithMissingBalanceWhenExpectedPostings(t *testing.T) { + j := `{ + "testCases": [ + { + "it": "t1", + "vars": { "source": "src", "amount": "42" }, + "balances": { "src": { "USD": 1 } }, + "expect.postings": [ + { "source": "src", "destination": "dest", "asset": "USD", "amount": 1 } + ] + } + ] + }` + + var specs specs_format.Specs + err := json.Unmarshal([]byte(j), &specs) + require.Nil(t, err) + + out, err := specs_format.Check(exampleProgram.Value, specs) + require.Nil(t, err) + + require.Equal(t, specs_format.SpecsResult{ + Total: 1, + Failing: 1, + Passing: 0, + Cases: []specs_format.TestCaseResult{ + { + It: "t1", + Pass: false, + Vars: interpreter.VariablesMap{ + "source": "src", + "amount": "42", + }, + Balances: interpreter.Balances{ + "src": interpreter.AccountBalance{ + "USD": big.NewInt(1), + }, + }, + Meta: interpreter.AccountsMetadata{}, + FailedAssertions: []specs_format.AssertionMismatch[any]{ + { + Assertion: "expect.missingFunds", + Got: true, + Expected: false, + }, + }, + }, + }, + }, out) + +} + +func TestNullPostingsIsNoop(t *testing.T) { + exampleProgram := parser.Parse(``) + + j := `{ + "testCases": [ + { + "it": "t1", + "vars": { "source": "src", "amount": "42" }, + "balances": { "src": { "USD": 1 } }, + "expect.postings": null + } + ] + }` + + var specs specs_format.Specs + err := json.Unmarshal([]byte(j), &specs) + require.Nil(t, err) + + out, err := specs_format.Check(exampleProgram.Value, specs) + require.Nil(t, err) + + require.Equal(t, specs_format.SpecsResult{ + Total: 1, + Failing: 0, + Passing: 1, + Cases: []specs_format.TestCaseResult{ + { + It: "t1", + Pass: true, + Vars: interpreter.VariablesMap{ + "source": "src", + "amount": "42", + }, + Balances: interpreter.Balances{ + "src": interpreter.AccountBalance{ + "USD": big.NewInt(1), + }, + }, + Meta: interpreter.AccountsMetadata{}, + FailedAssertions: nil, + }, + }, + }, out) + +} diff --git a/internal/utils/__snapshots__/pretty_csv_test.snap b/internal/utils/__snapshots__/pretty_csv_test.snap new file mode 100755 index 00000000..aea68443 --- /dev/null +++ b/internal/utils/__snapshots__/pretty_csv_test.snap @@ -0,0 +1,14 @@ + +[TestPrettyCsv - 1] +| Account | Asset | Balance | +| alice | EUR/2 | 1 | +| alice | USD/1234 | 999999 | +| bob | BTC | 3 | +--- + +[TestPrettyCsvMap - 1] +| Name | Value | +| a | 0 | +| b | 12345 | +| very-very-very-long-key | | +--- diff --git a/internal/utils/pretty_csv.go b/internal/utils/pretty_csv.go new file mode 100644 index 00000000..41710f03 --- /dev/null +++ b/internal/utils/pretty_csv.go @@ -0,0 +1,83 @@ +package utils + +import ( + "fmt" + "slices" + "strings" + + "github.com/formancehq/numscript/internal/ansi" +) + +// Fails if the header is shorter than any of the rows +func CsvPretty( + header []string, + rows [][]string, + sortRows bool, +) string { + if sortRows { + slices.SortStableFunc(rows, func(x, y []string) int { + strX := strings.Join(x, "|") + strY := strings.Join(y, "|") + if strX == strY { + return 0 + } else if strX < strY { + return -1 + } else { + return 1 + } + }) + } + + // -- Find paddings + var maxLengths []int = make([]int, len(header)) + for fieldIndex, fieldName := range header { + maxLen := len(fieldName) + + for _, row := range rows { + // panics if row[fieldIndex] is out of bounds + // thus we must never have unlabeled cols + maxLen = max(maxLen, len(row[fieldIndex])) + } + + maxLengths[fieldIndex] = maxLen + } + + var allRows []string + + // -- Print header + { + var partialRow []string + for index, fieldName := range header { + partialRow = append(partialRow, fmt.Sprintf("| %-*s ", + maxLengths[index], + ansi.ColorCyan(fieldName), + )) + } + partialRow = append(partialRow, "|") + allRows = append(allRows, strings.Join(partialRow, "")) + } + + // -- Print rows + for _, row := range rows { + var partialRow []string + for index, fieldName := range row { + partialRow = append(partialRow, fmt.Sprintf("| %-*s ", + maxLengths[index], + fieldName, + )) + } + partialRow = append(partialRow, "|") + allRows = append(allRows, strings.Join(partialRow, "")) + } + + return strings.Join(allRows, "\n") +} + +func CsvPrettyMap(keyName string, valueName string, m map[string]string) string { + var rows [][]string + for k, v := range m { + rows = append(rows, []string{k, v}) + } + + return CsvPretty([]string{keyName, valueName}, rows, true) +} diff --git a/internal/utils/pretty_csv_test.go b/internal/utils/pretty_csv_test.go new file mode 100644 index 00000000..010456b7 --- /dev/null +++ b/internal/utils/pretty_csv_test.go @@ -0,0 +1,30 @@ +package utils_test + +import ( + "testing" + + "github.com/formancehq/numscript/internal/utils" + "github.com/gkampitakis/go-snaps/snaps" +) + +func TestPrettyCsv(t *testing.T) { + out := utils.CsvPretty([]string{ + "Account", "Asset", "Balance", + }, [][]string{ + {"alice", "EUR/2", "1"}, + {"alice", "USD/1234", "999999"}, + {"bob", "BTC", "3"}, + }, true) + + snaps.MatchSnapshot(t, out) +} + +func TestPrettyCsvMap(t *testing.T) { + out := utils.CsvPrettyMap("Name", "Value", map[string]string{ + "a": "0", + "b": "12345", + "very-very-very-long-key": "", + }) + + snaps.MatchSnapshot(t, out) +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 8a7bc7b4..40e96ca7 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -56,3 +56,51 @@ func Filter[T any](slice []T, predicate func(x T) bool) []T { } return ret } + +func Map[T any, U any](slice []T, f func(x T) U) []U { + // TODO make + var ret []U + for _, x := range slice { + ret = append(ret, f(x)) + } + return ret +} + +func MapGetOrPutDefault[T any](m map[string]T, key string, getDefault func() T) T { + lookup, ok := m[key] + if !ok { + default_ := getDefault() + m[key] = default_ + return default_ + } + return lookup +} + +func NestedMapGetOrPutDefault[T any](m map[string]map[string]T, key1 string, key2 string, getDefault func() T) T { + m1 := MapGetOrPutDefault(m, key1, func() map[string]T { + return map[string]T{} + }) + + return MapGetOrPutDefault(m1, key2, getDefault) +} + +func MapCmp[T any](m1, m2 map[string]T, cmp func(x1 T, x2 T) bool) bool { + if len(m1) != len(m2) { + return false + } + + for k1, v1 := range m1 { + v2, ok := m2[k1] + if !ok || !cmp(v1, v2) { + return false + } + } + + return true +} + +func Map2Cmp[T any](m1, m2 map[string]map[string]T, cmp func(x1 T, x2 T) bool) bool { + return MapCmp(m1, m2, func(nested1, nested2 map[string]T) bool { + return MapCmp(nested1, nested2, cmp) + }) +} diff --git a/specs.schema.json b/specs.schema.json new file mode 100644 index 00000000..2b65a9a0 --- /dev/null +++ b/specs.schema.json @@ -0,0 +1,155 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Specs", + "type": "object", + "additionalProperties": false, + "required": ["testCases"], + "properties": { + "$schema": { "type": "string" }, + "balances": { + "$ref": "#/definitions/Balances" + }, + "vars": { + "$ref": "#/definitions/VariablesMap" + }, + "accountsMeta": { + "$ref": "#/definitions/AccountsMetadata" + }, + "testCases": { + "type": "array", + "items": { "$ref": "#/definitions/TestCase" } + }, + "featureFlags": { + "type": "array", + "items": { "type": "string" } + } + }, + "definitions": { + "TestCase": { + "type": "object", + "required": ["it"], + "additionalProperties": false, + "properties": { + "it": { + "type": "string", + "description": "Test case description" + }, + "balances": { + "$ref": "#/definitions/Balances" + }, + "vars": { + "$ref": "#/definitions/VariablesMap" + }, + "accountsMeta": { + "$ref": "#/definitions/AccountsMetadata" + }, + "expect.postings": { + "type": "array", + "items": { "$ref": "#/definitions/Posting" } + }, + + "expect.volumes": { + "$ref": "#/definitions/Balances" + }, + + "expect.movements": { + "$ref": "#/definitions/Movements" + }, + + "expect.txMeta": { + "$ref": "#/definitions/TxMetadata" + }, + + "expect.accountsMeta": { + "$ref": "#/definitions/AccountsMetadata" + }, + + "expect.missingFunds": { + "type": "boolean" + } + } + }, + + "Balances": { + "type": "object", + "description": "Map of account names to asset balances", + "additionalProperties": false, + "patternProperties": { + "^([a-zA-Z0-9_-]+(:[a-zA-Z0-9_-]+)*)$": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^([A-Z]+(/[0-9]+)?)$": { + "type": "number" + } + } + } + } + }, + + "VariablesMap": { + "type": "object", + "description": "Map of variable name to variable stringified value", + "additionalProperties": false, + "patternProperties": { + "^[a-z_]+$": { "type": "string" } + } + }, + + "AccountsMetadata": { + "type": "object", + "description": "Map of an account metadata to the account's metadata", + "additionalProperties": false, + "patternProperties": { + "^([a-zA-Z0-9_-]+(:[a-zA-Z0-9_-]+)*)$": { + "type": "object", + "additionalProperties": { "type": "string" } + } + } + }, + + "TxMetadata": { + "type": "object", + "description": "Map from a metadata's key to the transaction's metadata stringied value", + "additionalProperties": { "type": "string" } + }, + + "Movements": { + "type": "object", + "description": "The funds sent from an account to another", + "additionalProperties": false, + "patternProperties": { + "^([a-zA-Z0-9_-]+(:[a-zA-Z0-9_-]+)*)$": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^([a-zA-Z0-9_-]+(:[a-zA-Z0-9_-]+)*)$": { + "type": "object", + "patternProperties": { + "^([A-Z]+(/[0-9]+)?)$": { + "type": "number" + } + } + } + } + } + } + }, + + "Posting": { + "type": "object", + "properties": { + "source": { "type": "string" }, + "destination": { "type": "string" }, + "asset": { + "type": "string", + "pattern": "^([A-Z]+(/[0-9]+)?)$" + }, + "amount": { + "type": "number" + } + }, + "required": ["source", "destination", "asset", "amount"] + } + } +}