package internal import ( "testing" "github.com/shopspring/decimal" ) func TestFillerQueue(t *testing.T) { var recCount int newRecord := func() Record { recCount++ return testRecord{ id: recCount, } } var rq FillerQueue if rq.Len() != 0 { t.Fatalf("zero value should have zero length") } _, ok := rq.Pop() if ok { t.Fatalf("Pop() should return (_,false) on a zero value") } _, ok = rq.Peek() if ok { t.Fatalf("Peek() should return (_,false) on a zero value") } rq.Push(nil) if rq.Len() != 0 { t.Fatalf("pushing nil should be a no-op") } rq.Push(NewFiller(newRecord())) if rq.Len() != 1 { t.Fatalf("pushing 1st record should result in length of 1") } rq.Push(NewFiller(newRecord())) if rq.Len() != 2 { t.Fatalf("pushing 2nd record should result in length of 2") } peekFiller, ok := rq.Peek() if !ok { t.Fatalf("Peek() should return (_,true) when the list is not empty") } if rec, ok := peekFiller.Record.(testRecord); ok { if rec.id != 1 { t.Fatalf("Peek() should return the 1st record pushed but returned %d", rec.id) } } else { t.Fatalf("Peek() should return the original record type") } if rq.Len() != 2 { t.Fatalf("Peek() should not affect the list length") } popFiller, ok := rq.Pop() if !ok { t.Fatalf("Pop() should return (_,true) when the list is not empty") } if rec, ok := popFiller.Record.(testRecord); ok { if rec.id != 1 { t.Fatalf("Pop() should return the first record pushed but returned %d", rec.id) } } else { t.Fatalf("Pop() should return the original record") } if rq.Len() != 1 { t.Fatalf("Pop() should remove an element from the list") } } func TestFillerQueueNilReceiver(t *testing.T) { var rq *FillerQueue if rq.Len() > 0 { t.Fatalf("nil receiver should have zero length") } _, ok := rq.Peek() if ok { t.Fatalf("Peek() on a nil receiver should return (_,false)") } _, ok = rq.Pop() if ok { t.Fatalf("Pop() on a nil receiver should return (_,false)") } rq.Push(nil) if rq.Len() != 0 { t.Fatalf("Push(nil) on a nil receiver should be a no-op") } defer func() { r := recover() if r == nil { t.Fatalf("expected a panic but got nothing") } expMsg := "Push to nil FillerQueue" if msg, ok := r.(string); !ok || msg != expMsg { t.Fatalf(`want panic message %q but got "%v"`, expMsg, r) } }() rq.Push(NewFiller(&testRecord{})) } type testRecord struct { Record id int quantity decimal.Decimal price decimal.Decimal } func (tr testRecord) Quantity() decimal.Decimal { return tr.quantity } func (tr testRecord) Price() decimal.Decimal { return tr.price } func TestFiller_Fill(t *testing.T) { tests := []struct { name string r Record quantity decimal.Decimal want decimal.Decimal wantBool bool }{ { name: "fills 0 of zero quantity", r: &testRecord{quantity: decimal.NewFromFloat(0.0)}, quantity: decimal.Decimal{}, want: decimal.Decimal{}, wantBool: true, }, { name: "fills 0 of positive quantity", r: &testRecord{quantity: decimal.NewFromFloat(100.0)}, quantity: decimal.Decimal{}, want: decimal.Decimal{}, wantBool: false, }, { name: "fills 10 out of 100 and no previous fills", r: &testRecord{quantity: decimal.NewFromFloat(100.0)}, quantity: decimal.NewFromFloat(10), want: decimal.NewFromFloat(10), wantBool: false, }, { name: "fills 10 out of 10 and no previous fills", r: &testRecord{quantity: decimal.NewFromFloat(10.0)}, quantity: decimal.NewFromFloat(10), want: decimal.NewFromFloat(10), wantBool: true, }, { name: "filling 100 fills 10 out of 10 and no previous fills", r: &testRecord{quantity: decimal.NewFromFloat(10.0)}, quantity: decimal.NewFromFloat(100), want: decimal.NewFromFloat(10), wantBool: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { f := NewFiller(tt.r) got, gotBool := f.Fill(tt.quantity) if !tt.want.Equal(got) { t.Errorf("want 1st return value to be %v but got %v", tt.want, got) } if tt.wantBool != gotBool { t.Errorf("want 2nd return value to be %v but got %v", tt.wantBool, gotBool) } }) } } func TestFiller_ApplySplit(t *testing.T) { tests := []struct { name string qty float64 price float64 prefilled float64 ratio float64 wantQty float64 wantPrice float64 wantFilled float64 wantCostBasis float64 }{ { name: "5:1 split on unfilled lot preserves cost basis", qty: 10, price: 100, prefilled: 0, ratio: 5, wantQty: 50, wantPrice: 20, wantFilled: 0, wantCostBasis: 1000, }, { name: "5:1 split on partially filled lot", qty: 10, price: 100, prefilled: 4, ratio: 5, wantQty: 50, wantPrice: 20, wantFilled: 20, wantCostBasis: 1000, }, { name: "1:2 reverse split on unfilled lot preserves cost basis", qty: 10, price: 100, prefilled: 0, ratio: 0.5, wantQty: 5, wantPrice: 200, wantFilled: 0, wantCostBasis: 1000, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { f := NewFiller(&testRecord{ quantity: decimal.NewFromFloat(tt.qty), price: decimal.NewFromFloat(tt.price), }) if tt.prefilled > 0 { f.Fill(decimal.NewFromFloat(tt.prefilled)) } f.ApplySplit(decimal.NewFromFloat(tt.ratio)) if !f.Quantity().Equal(decimal.NewFromFloat(tt.wantQty)) { t.Errorf("want quantity %v but got %v", tt.wantQty, f.Quantity()) } if !f.Price().Equal(decimal.NewFromFloat(tt.wantPrice)) { t.Errorf("want price %v but got %v", tt.wantPrice, f.Price()) } if !f.filled.Equal(decimal.NewFromFloat(tt.wantFilled)) { t.Errorf("want filled %v but got %v", tt.wantFilled, f.filled) } costBasis := f.Quantity().Mul(f.Price()) if !costBasis.Equal(decimal.NewFromFloat(tt.wantCostBasis)) { t.Errorf("want cost basis %v but got %v", tt.wantCostBasis, costBasis) } }) } } func TestFillerQueue_AdjustForSplit(t *testing.T) { var fq FillerQueue fq.Push(NewFiller(&testRecord{quantity: decimal.NewFromFloat(10), price: decimal.NewFromFloat(100)})) fq.Push(NewFiller(&testRecord{quantity: decimal.NewFromFloat(5), price: decimal.NewFromFloat(200)})) fq.AdjustForSplit(decimal.NewFromFloat(5)) lot1, _ := fq.Pop() if !lot1.Quantity().Equal(decimal.NewFromFloat(50)) { t.Errorf("lot1: want quantity 50 but got %v", lot1.Quantity()) } if !lot1.Price().Equal(decimal.NewFromFloat(20)) { t.Errorf("lot1: want price 20 but got %v", lot1.Price()) } lot2, _ := fq.Pop() if !lot2.Quantity().Equal(decimal.NewFromFloat(25)) { t.Errorf("lot2: want quantity 25 but got %v", lot2.Quantity()) } if !lot2.Price().Equal(decimal.NewFromFloat(40)) { t.Errorf("lot2: want price 40 but got %v", lot2.Price()) } } func TestFillerQueue_AdjustForSplit_NilReceiver(t *testing.T) { var fq *FillerQueue fq.AdjustForSplit(decimal.NewFromFloat(5)) // must not panic }