gob is creating extra problems

This commit is contained in:
Radu Ioan Fericean
2012-03-01 00:06:34 +02:00
parent 4463946866
commit 12540225b1
14 changed files with 194 additions and 293 deletions

View File

@@ -47,7 +47,7 @@ func TestApRestoreRedis(t *testing.T) {
}
func TestApStoreRestore(t *testing.T) {
getter, _ := NewRedisStorage("tcp:127.0.0.1:6379", 10)
getter, _ := NewKyotoStorage("test.kch")
defer getter.Close()
d := time.Date(2012, time.February, 1, 14, 30, 1, 0, time.UTC)
i := &Interval{Month: time.February,
@@ -68,3 +68,23 @@ func TestApStoreRestore(t *testing.T) {
}
/**************************** Benchmarks *************************************/
func BenchmarkActivationPeriodStoreRestore(b *testing.B) {
b.StopTimer()
getter, _ := NewKyotoStorage("test.kch")
defer getter.Close()
d := time.Date(2012, time.February, 1, 14, 30, 1, 0, time.UTC)
i := &Interval{Month: time.February,
MonthDay: 1,
WeekDays: []time.Weekday{time.Wednesday, time.Thursday},
StartTime: "14:30:00",
EndTime: "15:00:00"}
ap := &ActivationPeriod{ActivationTime: d}
ap.AddInterval(i)
b.StartTimer()
for i := 0; i < b.N; i++ {
getter.SetActivationPeriods("storerestore", []*ActivationPeriod{ap})
getter.GetActivationPeriods("storerestore")
}
}

View File

@@ -269,7 +269,7 @@ func (cd *CallDescriptor) GetMaxSessionTime() (seconds float64, err error) {
cost := 0.0
for i, ts := range timespans {
if ts.MinuteInfo == nil && i == 0 {
if i == 0 && ts.MinuteInfo == nil && ts.Interval != nil {
cost += ts.Interval.ConnectFee
}
cost += ts.GetCost(cd)

View File

@@ -209,7 +209,7 @@ func TestMaxSessionTimeNoUserBudget(t *testing.T) {
defer getter.Close()
cd := &CallDescriptor{CstmId: "vdf", Subject: "rif", DestinationPrefix: "0723", storageGetter: getter, Amount: 1000}
result, err := cd.GetMaxSessionTime()
if result != 1000 || err != nil {
if result != 1000 || err == nil {
t.Errorf("Expected %v was %v", 1000, result)
}
}

View File

@@ -17,11 +17,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
*/
package timespans
import (
//"log"
"strings"
)
/*
Structure that gathers multiple destination prefixes under a common id.
*/
@@ -30,21 +25,6 @@ type Destination struct {
Prefixes []string
}
/*
Serializes the destination for the storage. Used for key-value storages.
*/
func (d *Destination) store() (result string) {
for _, p := range d.Prefixes {
result += p + ","
}
result = strings.TrimRight(result, ",")
return
}
func (d *Destination) restore(input string) {
d.Prefixes = strings.Split(input, ",")
}
/*
De-serializes the destination for the storage. Used for key-value storages.
*/

View File

@@ -21,16 +21,6 @@ import (
"testing"
)
func TestDestinationStoreRestore(t *testing.T) {
nationale = &Destination{Id: "nat", Prefixes: []string{"0257", "0256", "0723"}}
s := nationale.store()
d1 := &Destination{Id: "nat"}
d1.restore(s)
if d1.store() != s {
t.Errorf("Expected %q was %q", s, d1.store())
}
}
func TestDestinationKyotoStore(t *testing.T) {
getter, _ := NewKyotoStorage("test.kch")
defer getter.Close()

View File

@@ -1,11 +1,11 @@
/*
Rating system designed to be used in VoIP Carriers World
Rating system designed to be used in VoIP Carrieks World
Copyright (C) 2012 Radu Ioan Fericean
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
the Free Software Foundation, either veksion 3 of the License, or
(at your option) any later veksion.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -21,25 +21,38 @@ import (
"bitbucket.org/ww/cabinet"
"bytes"
"encoding/gob"
//"github.com/fsouza/gokabinet/kc"
// "log"
//"log"
"sync"
)
type KyotoStorage struct {
//db *kc.DB
db *cabinet.KCDB
buf bytes.Buffer
dec *gob.Decoder
mux sync.Mutex // we need norma lock because we reset the buf variable
db *cabinet.KCDB
buf bytes.Buffer
decAP *gob.Decoder
encAP *gob.Encoder
decDest *gob.Decoder
encDest *gob.Encoder
decTP *gob.Decoder
encTP *gob.Encoder
decUB *gob.Decoder
encUB *gob.Encoder
mux sync.Mutex // we need norma lock because we reset the buf variable
}
func NewKyotoStorage(filaName string) (*KyotoStorage, error) {
//ndb, err := kc.Open(filaName, kc.WRITE)
ndb := cabinet.New()
err := ndb.Open(filaName, cabinet.KCOWRITER|cabinet.KCOCREATE)
ks := &KyotoStorage{db: ndb}
ks.dec = gob.NewDecoder(&ks.buf)
ks.decAP = gob.NewDecoder(&ks.buf)
ks.encAP = gob.NewEncoder(&ks.buf)
ks.decDest = gob.NewDecoder(&ks.buf)
ks.encDest = gob.NewEncoder(&ks.buf)
ks.decTP = gob.NewDecoder(&ks.buf)
ks.encTP = gob.NewEncoder(&ks.buf)
ks.decUB = gob.NewDecoder(&ks.buf)
ks.encUB = gob.NewEncoder(&ks.buf)
return ks, err
}
@@ -51,10 +64,9 @@ func (ks *KyotoStorage) SetActivationPeriods(key string, aps []*ActivationPeriod
ks.mux.Lock()
defer ks.mux.Unlock()
var writeBuf bytes.Buffer
encoder := gob.NewEncoder(&writeBuf)
encoder.Encode(aps)
return ks.db.Set([]byte(key), writeBuf.Bytes())
ks.buf.Reset()
ks.encAP.Encode(aps)
return ks.db.Set([]byte(key), ks.buf.Bytes())
}
func (ks *KyotoStorage) GetActivationPeriods(key string) (aps []*ActivationPeriod, err error) {
@@ -65,42 +77,69 @@ func (ks *KyotoStorage) GetActivationPeriods(key string) (aps []*ActivationPerio
ks.buf.Reset()
ks.buf.Write(values)
ks.dec.Decode(&aps)
return
}
func (ks *KyotoStorage) GetDestination(key string) (dest *Destination, err error) {
if values, err := ks.db.Get([]byte(key)); err == nil {
dest = &Destination{Id: key}
dest.restore(string(values))
}
ks.decAP.Decode(&aps)
return
}
func (ks *KyotoStorage) SetDestination(dest *Destination) error {
return ks.db.Set([]byte(dest.Id), []byte(dest.store()))
ks.mux.Lock()
defer ks.mux.Unlock()
ks.buf.Reset()
ks.encDest.Encode(dest)
return ks.db.Set([]byte(dest.Id), ks.buf.Bytes())
}
func (ks *KyotoStorage) GetTariffPlan(key string) (tp *TariffPlan, err error) {
if values, err := ks.db.Get([]byte(key)); err == nil {
tp = &TariffPlan{Id: key}
tp.restore(string(values))
}
func (ks *KyotoStorage) GetDestination(key string) (dest *Destination, err error) {
ks.mux.Lock()
defer ks.mux.Unlock()
values, err := ks.db.Get([]byte(key))
ks.buf.Reset()
ks.buf.Write(values)
ks.decDest.Decode(&dest)
return
}
func (ks *KyotoStorage) SetTariffPlan(tp *TariffPlan) error {
return ks.db.Set([]byte(tp.Id), []byte(tp.store()))
ks.mux.Lock()
defer ks.mux.Unlock()
ks.buf.Reset()
ks.encTP.Encode(tp)
return ks.db.Set([]byte(tp.Id), ks.buf.Bytes())
}
func (ks *KyotoStorage) GetUserBudget(key string) (ub *UserBudget, err error) {
if values, err := ks.db.Get([]byte(key)); err == nil {
ub = &UserBudget{Id: key}
ub.restore(string(values))
}
func (ks *KyotoStorage) GetTariffPlan(key string) (tp *TariffPlan, err error) {
ks.mux.Lock()
defer ks.mux.Unlock()
values, err := ks.db.Get([]byte(key))
ks.buf.Reset()
ks.buf.Write(values)
ks.decTP.Decode(&tp)
return
}
func (ks *KyotoStorage) SetUserBudget(ub *UserBudget) error {
return ks.db.Set([]byte(ub.Id), []byte(ub.store()))
ks.mux.Lock()
defer ks.mux.Unlock()
ks.buf.Reset()
ks.encUB.Encode(ub)
return ks.db.Set([]byte(ub.Id), ks.buf.Bytes())
}
func (ks *KyotoStorage) GetUserBudget(key string) (ub *UserBudget, err error) {
ks.mux.Lock()
defer ks.mux.Unlock()
values, err := ks.db.Get([]byte(key))
ks.buf.Reset()
ks.buf.Write(values)
ks.decUB.Decode(&ub)
return
}

View File

@@ -20,8 +20,6 @@ package timespans
import (
// "log"
"math"
"strconv"
"strings"
)
type MinuteBucket struct {
@@ -33,28 +31,6 @@ type MinuteBucket struct {
precision int
}
/*
Serializes the minute bucket for the storage. Used for key-value storages.
*/
func (mb *MinuteBucket) store() (result string) {
result += strconv.Itoa(int(mb.Seconds)) + "|"
result += strconv.Itoa(int(mb.Priority)) + "|"
result += strconv.FormatFloat(mb.Price, 'f', -1, 64) + "|"
result += mb.DestinationId
return
}
/*
De-serializes the minute bucket for the storage. Used for key-value storages.
*/
func (mb *MinuteBucket) restore(input string) {
elements := strings.Split(input, "|")
mb.Seconds, _ = strconv.ParseFloat(elements[0], 64)
mb.Priority, _ = strconv.Atoi(elements[1])
mb.Price, _ = strconv.ParseFloat(elements[2], 64)
mb.DestinationId = elements[3]
}
/*
Returns the destination loading it from the storage if necessary.
*/

View File

@@ -30,3 +30,29 @@ func TestGetDestination(t *testing.T) {
t.Error("Got wrong destination: ", d)
}
}
func TestMultipleGetDestination(t *testing.T) {
getter, _ := NewRedisStorage("tcp:127.0.0.1:6379", 10)
defer getter.Close()
mb := &MinuteBucket{DestinationId: "nationale"}
d := mb.getDestination(getter)
d = mb.getDestination(getter)
d = mb.getDestination(getter)
if d.Id != "nationale" || len(d.Prefixes) != 4 {
t.Error("Got wrong destination: ", d)
}
/*mb = &MinuteBucket{DestinationId: "retea"}
d = mb.getDestination(getter)
d = mb.getDestination(getter)
d = mb.getDestination(getter)
if d.Id != "retea" || len(d.Prefixes) != 2 {
t.Error("Got wrong destination: ", d)
}
mb = &MinuteBucket{DestinationId: "mobil"}
d = mb.getDestination(getter)
d = mb.getDestination(getter)
d = mb.getDestination(getter)
if d.Id != "mobil" || len(d.Prefixes) != 2 {
t.Error("Got wrong destination: ", d)
}*/
}

View File

@@ -21,23 +21,29 @@ import (
"bytes"
"encoding/gob"
"github.com/simonz05/godis"
// "log"
"log"
"sync"
)
type RedisStorage struct {
dbNb int
db *godis.Client
buf bytes.Buffer
dec *gob.Decoder
mux sync.Mutex
dbNb int
db *godis.Client
buf bytes.Buffer
decAP *gob.Decoder
decDest *gob.Decoder
decTP *gob.Decoder
decUB *gob.Decoder
mux sync.Mutex
}
func NewRedisStorage(address string, db int) (*RedisStorage, error) {
ndb := godis.New(address, db, "")
rs := &RedisStorage{db: ndb, dbNb: db}
rs.dec = gob.NewDecoder(&rs.buf)
rs.decAP = gob.NewDecoder(&rs.buf)
rs.decDest = gob.NewDecoder(&rs.buf)
rs.decTP = gob.NewDecoder(&rs.buf)
rs.decUB = gob.NewDecoder(&rs.buf)
return rs, nil
}
@@ -49,6 +55,7 @@ func (rs *RedisStorage) SetActivationPeriods(key string, aps []*ActivationPeriod
//.db.Select(rs.dbNb)
rs.mux.Lock()
defer rs.mux.Unlock()
var writeBuf bytes.Buffer
encoder := gob.NewEncoder(&writeBuf)
encoder.Encode(aps)
@@ -64,48 +71,76 @@ func (rs *RedisStorage) GetActivationPeriods(key string) (aps []*ActivationPerio
rs.buf.Reset()
rs.buf.Write(elem.Bytes())
rs.dec.Decode(&aps)
return
}
func (rs *RedisStorage) GetDestination(key string) (dest *Destination, err error) {
//rs.db.Select(rs.dbNb + 1)
if values, err := rs.db.Get(key); err == nil {
dest = &Destination{Id: key}
dest.restore(values.String())
}
rs.decAP.Decode(&aps)
return
}
func (rs *RedisStorage) SetDestination(dest *Destination) error {
//rs.db.Select(rs.dbNb + 1)
return rs.db.Set(dest.Id, dest.store())
//rs.db.Select(rs.dbNb + 1)
rs.mux.Lock()
defer rs.mux.Unlock()
var writeBuf bytes.Buffer
encoder := gob.NewEncoder(&writeBuf)
encoder.Encode(dest)
return rs.db.Set(dest.Id, writeBuf.Bytes())
}
func (rs *RedisStorage) GetTariffPlan(key string) (tp *TariffPlan, err error) {
//rs.db.Select(rs.dbNb + 2)
if values, err := rs.db.Get(key); err == nil {
tp = &TariffPlan{Id: key}
tp.restore(values.String())
}
func (rs *RedisStorage) GetDestination(key string) (dest *Destination, err error) {
//rs.db.Select(rs.dbNb + 1)
rs.mux.Lock()
defer rs.mux.Unlock()
elem, err := rs.db.Get(key)
rs.buf.Reset()
rs.buf.Write(elem.Bytes())
err = rs.decDest.Decode(&dest)
log.Print(err)
return
}
func (rs *RedisStorage) SetTariffPlan(tp *TariffPlan) error {
//rs.db.Select(rs.dbNb + 2)
return rs.db.Set(tp.Id, tp.store())
rs.mux.Lock()
defer rs.mux.Unlock()
var writeBuf bytes.Buffer
encoder := gob.NewEncoder(&writeBuf)
encoder.Encode(tp)
return rs.db.Set(tp.Id, writeBuf.Bytes())
}
func (rs *RedisStorage) GetUserBudget(key string) (ub *UserBudget, err error) {
//rs.db.Select(rs.dbNb + 3)
if values, err := rs.db.Get(key); err == nil {
ub = &UserBudget{Id: key}
ub.restore(values.String())
}
func (rs *RedisStorage) GetTariffPlan(key string) (tp *TariffPlan, err error) {
//rs.db.Select(rs.dbNb + 2)
rs.mux.Lock()
defer rs.mux.Unlock()
elem, err := rs.db.Get(key)
rs.buf.Reset()
rs.buf.Write(elem.Bytes())
rs.decTP.Decode(&tp)
return
}
func (rs *RedisStorage) SetUserBudget(ub *UserBudget) error {
//rs.db.Select(rs.dbNb + 3)
return rs.db.Set(ub.Id, ub.store())
rs.mux.Lock()
defer rs.mux.Unlock()
var writeBuf bytes.Buffer
encoder := gob.NewEncoder(&writeBuf)
encoder.Encode(ub)
return rs.db.Set(ub.Id, writeBuf.Bytes())
}
func (rs *RedisStorage) GetUserBudget(key string) (ub *UserBudget, err error) {
//rs.db.Select(rs.dbNb + 3)
rs.mux.Lock()
defer rs.mux.Unlock()
elem, err := rs.db.Get(key)
rs.buf.Reset()
rs.buf.Write(elem.Bytes())
rs.decUB.Decode(&ub)
return
}

View File

@@ -19,8 +19,6 @@ package timespans
import (
// "log"
"strconv"
"strings"
)
/*
@@ -45,62 +43,6 @@ type VolumeDiscount struct {
Discount float64 // procentage
}
/*
Serializes the tariff plan for the storage. Used for key-value storages.
*/
func (tp *TariffPlan) store() (result string) {
result += strconv.FormatFloat(tp.SmsCredit, 'f', -1, 64) + ";"
result += strconv.FormatFloat(tp.Traffic, 'f', -1, 64) + ";"
result += strconv.FormatFloat(tp.ReceivedCallSecondsLimit, 'f', -1, 64) + ";"
if tp.RecivedCallBonus == nil {
tp.RecivedCallBonus = &RecivedCallBonus{}
}
result += tp.RecivedCallBonus.store() + ";"
for i, mb := range tp.MinuteBuckets {
if i > 0 {
result += ","
}
result += mb.store()
}
if tp.VolumeDiscountThresholds != nil {
result += ";"
}
for i, vd := range tp.VolumeDiscountThresholds {
if i > 0 {
result += ","
}
result += strconv.FormatFloat(vd.Volume, 'f', -1, 64) + "|" + strconv.FormatFloat(vd.Discount, 'f', -1, 64)
}
result = strings.TrimRight(result, ";")
return
}
/*
De-serializes the tariff plan for the storage. Used for key-value storages.
*/
func (tp *TariffPlan) restore(input string) {
elements := strings.Split(input, ";")
tp.SmsCredit, _ = strconv.ParseFloat(elements[0], 64)
tp.Traffic, _ = strconv.ParseFloat(elements[1], 64)
tp.ReceivedCallSecondsLimit, _ = strconv.ParseFloat(elements[2], 64)
tp.RecivedCallBonus = &RecivedCallBonus{}
tp.RecivedCallBonus.restore(elements[3])
for _, mbs := range strings.Split(elements[4], ",") {
mb := &MinuteBucket{}
mb.restore(mbs)
tp.MinuteBuckets = append(tp.MinuteBuckets, mb)
}
if len(elements) > 5 {
for _, vdss := range strings.Split(elements[5], ",") {
vd := &VolumeDiscount{}
vds := strings.Split(vdss, "|")
vd.Volume, _ = strconv.ParseFloat(vds[0], 64)
vd.Discount, _ = strconv.ParseFloat(vds[1], 64)
tp.VolumeDiscountThresholds = append(tp.VolumeDiscountThresholds, vd)
}
}
}
/*
Structure to be filled for each tariff plan with the bonus value for received calls minutes.
*/
@@ -110,31 +52,3 @@ type RecivedCallBonus struct {
Traffic float64
MinuteBucket *MinuteBucket
}
/*
Serializes the tariff plan for the storage. Used for key-value storages.
*/
func (rcb *RecivedCallBonus) store() (result string) {
result += strconv.FormatFloat(rcb.Credit, 'f', -1, 64) + ","
result += strconv.FormatFloat(rcb.SmsCredit, 'f', -1, 64) + ","
result += strconv.FormatFloat(rcb.Traffic, 'f', -1, 64)
if rcb.MinuteBucket != nil {
result += ","
result += rcb.MinuteBucket.store()
}
return
}
/*
De-serializes the tariff plan for the storage. Used for key-value storages.
*/
func (rcb *RecivedCallBonus) restore(input string) {
elements := strings.Split(input, ",")
rcb.Credit, _ = strconv.ParseFloat(elements[0], 64)
rcb.SmsCredit, _ = strconv.ParseFloat(elements[1], 64)
rcb.Traffic, _ = strconv.ParseFloat(elements[2], 64)
if len(elements) > 3 {
rcb.MinuteBucket = &MinuteBucket{}
rcb.MinuteBucket.restore(elements[3])
}
}

View File

@@ -21,25 +21,6 @@ import (
"testing"
)
func TestTariffPlanStoreRestore(t *testing.T) {
b1 := &MinuteBucket{Seconds: 10, Priority: 10, Price: 0.01, DestinationId: "nationale"}
b2 := &MinuteBucket{Seconds: 100, Priority: 20, Price: 0.0, DestinationId: "retea"}
rcb := &RecivedCallBonus{Credit: 100}
vd := &VolumeDiscount{100, 10}
seara := &TariffPlan{Id: "seara_voo",
SmsCredit: 100,
ReceivedCallSecondsLimit: 0,
RecivedCallBonus: rcb,
MinuteBuckets: []*MinuteBucket{b1, b2},
VolumeDiscountThresholds: []*VolumeDiscount{vd}}
s := seara.store()
tp1 := &TariffPlan{Id: "seara_voo"}
tp1.restore(s)
if tp1.store() != s {
t.Errorf("Expected %q was %q", s, tp1.store())
}
}
func TestTariffPlanKyotoStore(t *testing.T) {
getter, _ := NewKyotoStorage("test.kch")
defer getter.Close()

Binary file not shown.

View File

@@ -20,8 +20,6 @@ package timespans
import (
// "log"
"sort"
"strconv"
"strings"
"sync"
)
@@ -70,55 +68,11 @@ func (bs bucketsorter) Less(j, i int) bool {
bs[i].Price > bs[j].Price
}
/*
Serializes the user budget for the storage. Used for key-value storages.
*/
func (ub *UserBudget) store() (result string) {
result += strconv.FormatFloat(ub.Credit, 'f', -1, 64) + ";"
result += strconv.FormatFloat(ub.SmsCredit, 'f', -1, 64) + ";"
result += strconv.FormatFloat(ub.Traffic, 'f', -1, 64) + ";"
result += strconv.FormatFloat(ub.VolumeDiscountSeconds, 'f', -1, 64) + ";"
result += strconv.FormatFloat(ub.ReceivedCallSeconds, 'f', -1, 64) + ";"
result += strconv.Itoa(ub.ResetDayOfTheMonth) + ";"
result += ub.TariffPlanId
if ub.MinuteBuckets != nil {
result += ";"
}
for i, mb := range ub.MinuteBuckets {
if i > 0 {
result += ","
}
result += mb.store()
}
return
}
/*
De-serializes the user budget for the storage. Used for key-value storages.
*/
func (ub *UserBudget) restore(input string) {
elements := strings.Split(input, ";")
ub.Credit, _ = strconv.ParseFloat(elements[0], 64)
ub.SmsCredit, _ = strconv.ParseFloat(elements[1], 64)
ub.Traffic, _ = strconv.ParseFloat(elements[2], 64)
ub.VolumeDiscountSeconds, _ = strconv.ParseFloat(elements[3], 64)
ub.ReceivedCallSeconds, _ = strconv.ParseFloat(elements[4], 64)
ub.ResetDayOfTheMonth, _ = strconv.Atoi(elements[5])
ub.TariffPlanId = elements[6]
if len(elements) > 7 {
for _, mbs := range strings.Split(elements[7], ",") {
mb := &MinuteBucket{}
mb.restore(mbs)
ub.MinuteBuckets = append(ub.MinuteBuckets, mb)
}
}
}
/*
Returns the tariff plan loading it from the storage if necessary.
*/
func (ub *UserBudget) getTariffPlan(storage StorageGetter) (tp *TariffPlan, err error) {
if ub.tariffPlan == nil {
if ub.tariffPlan == nil && ub.TariffPlanId != "" {
ub.tariffPlan, err = storage.GetTariffPlan(ub.TariffPlanId)
}
return ub.tariffPlan, err
@@ -150,7 +104,6 @@ func (ub *UserBudget) getSecondsForPrefix(sg StorageGetter, prefix string) (seco
// log.Print("There are no minute buckets to check for user: ", ub.Id)
return
}
for _, mb := range ub.MinuteBuckets {
d := mb.getDestination(sg)
if d == nil {

View File

@@ -53,19 +53,6 @@ func TestGetPricedSeconds(t *testing.T) {
}
}
func TestUserBudgetStoreRestore(t *testing.T) {
b1 := &MinuteBucket{Seconds: 10, Priority: 10, Price: 0.01, DestinationId: "nationale"}
b2 := &MinuteBucket{Seconds: 100, Priority: 20, Price: 0.0, DestinationId: "retea"}
seara := &TariffPlan{Id: "seara", SmsCredit: 100, MinuteBuckets: []*MinuteBucket{b1, b2}}
rifsBudget := &UserBudget{Id: "other", MinuteBuckets: []*MinuteBucket{b1, b2}, Credit: 21, tariffPlan: seara, ResetDayOfTheMonth: 10}
s := rifsBudget.store()
ub1 := &UserBudget{Id: "other"}
ub1.restore(s)
if ub1.store() != s {
t.Errorf("Expected %q was %q", s, ub1.store())
}
}
func TestUserBudgetKyotoStore(t *testing.T) {
getter, _ := NewKyotoStorage("test.kch")
defer getter.Close()