diff --git a/cdrc/.cdrc.go.swp b/cdrc/.cdrc.go.swp index 4916112cb..e9b63cc96 100644 Binary files a/cdrc/.cdrc.go.swp and b/cdrc/.cdrc.go.swp differ diff --git a/cdrc/.cdrc_test.go.swp b/cdrc/.cdrc_test.go.swp deleted file mode 100644 index e8ddb1e84..000000000 Binary files a/cdrc/.cdrc_test.go.swp and /dev/null differ diff --git a/cdrc/cdrc.go b/cdrc/cdrc.go index c426d616a..8c0601308 100644 --- a/cdrc/cdrc.go +++ b/cdrc/cdrc.go @@ -24,30 +24,66 @@ import ( "github.com/howeyc/fsnotify" "os" "path" + "io/ioutil" "net/http" "net/url" "strconv" "strings" "bufio" "encoding/csv" + "time" "github.com/cgrates/cgrates/config" "github.com/cgrates/cgrates/utils" "github.com/cgrates/cgrates/engine" ) +func NewCdrc(config *config.CGRConfig) (*Cdrc, error) { + cdrc := &Cdrc{cgrCfg: config} + // Before processing, make sure in and out folders exist + for _, dir := range []string{ cdrc.cgrCfg.CdrcCdrInDir, cdrc.cgrCfg.CdrcCdrOutDir } { + if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) { + return nil, fmt.Errorf("Folder %s does not exist", dir) + } + } + if err := cdrc.parseFieldIndexesFromConfig(); err != nil { + return nil, err + } + return cdrc, nil +} + + type Cdrc struct { cgrCfg *config.CGRConfig fieldIndxes map[string]int // Key is the name of the field, int is the position in the csv file httpClient *http.Client } +// When called fires up folder monitoring, either automated via inotify or manual by sleeping between processing +func (self *Cdrc) Run() error { + if self.cgrCfg.CdrcRunDelay == time.Duration(0) { // Automated via inotify + return self.trackCDRFiles() + } + // No automated, process and sleep approach + for { + engine.Logger.Info(fmt.Sprintf("Parsing folder %s for CDR files.", self.cgrCfg.CdrcCdrInDir)) + filesInDir,_ := ioutil.ReadDir(self.cgrCfg.CdrcCdrInDir) + for _, file := range filesInDir { + if err := self.processFile(file.Name()); err != nil { + return err + } + } + time.Sleep(self.cgrCfg.CdrcRunDelay) + } + return nil +} + // Parses fieldIndex strings into fieldIndex integers needed func (self *Cdrc) parseFieldIndexesFromConfig() error { var err error // Add main fields here self.fieldIndxes = make(map[string]int) - // PrimaryCdrFields []string = []string{ACCID, CDRHOST, REQTYPE, DIRECTION, TENANT, TOR, ACCOUNT, SUBJECT, DESTINATION, ANSWER_TIME, DURATION} + // PrimaryCdrFields []string = []string{ACCID, CDRHOST, CDRSOURCE, REQTYPE, DIRECTION, TENANT, TOR, ACCOUNT, SUBJECT, DESTINATION, ANSWER_TIME, DURATION} fieldKeys := []string{utils.ACCID, utils.REQTYPE, utils.DIRECTION, utils.TENANT, utils.TOR, utils.ACCOUNT, utils.SUBJECT, utils.DESTINATION, utils.ANSWER_TIME, utils.DURATION} fieldIdxStrs := []string{self.cgrCfg.CdrcAccIdField, self.cgrCfg.CdrcReqTypeField, self.cgrCfg.CdrcDirectionField, self.cgrCfg.CdrcTenantField, self.cgrCfg.CdrcTorField, self.cgrCfg.CdrcAccountField, self.cgrCfg.CdrcSubjectField, self.cgrCfg.CdrcDestinationField, self.cgrCfg.CdrcAnswerTimeField, self.cgrCfg.CdrcDurationField} @@ -75,6 +111,7 @@ func (self *Cdrc) parseFieldIndexesFromConfig() error { // Takes the record out of csv and turns it into http form which can be posted func (self *Cdrc) cdrAsHttpForm(record []string) (url.Values, error) { v := url.Values{} + v.Set(utils.CDRSOURCE, self.cgrCfg.CdrcSourceId) for fldName, idx := range self.fieldIndxes { if len(record) <= idx { return nil, fmt.Errorf("Ignoring record: %v - cannot extract field %s", record, fldName) @@ -100,7 +137,6 @@ func (self *Cdrc) trackCDRFiles() (err error) { select { case ev := <-watcher.Event: if ev.IsCreate() && path.Ext(ev.Name) != ".csv" { - engine.Logger.Info(fmt.Sprintf("Parsing: %s", ev.Name)) if err = self.processFile(ev.Name); err != nil { return err } @@ -114,6 +150,8 @@ func (self *Cdrc) trackCDRFiles() (err error) { // Processe file at filePath and posts the valid cdr rows out of it func (self *Cdrc) processFile(filePath string) error { + _, fn := path.Split(filePath) + engine.Logger.Info(fmt.Sprintf("Parsing: %s", filePath)) file, err := os.Open(filePath) defer file.Close() if err != nil { @@ -133,6 +171,5 @@ func (self *Cdrc) processFile(filePath string) error { } } // Finished with file, move it to processed folder - _, fn := path.Split(filePath) return os.Rename(filePath, path.Join(self.cgrCfg.CdrcCdrOutDir, fn)) } diff --git a/cdrc/cdrc_test.go b/cdrc/cdrc_test.go index 4f3b4fb1b..6bd815433 100644 --- a/cdrc/cdrc_test.go +++ b/cdrc/cdrc_test.go @@ -32,6 +32,9 @@ func TestCdrAsHttpForm(t *testing.T) { if err != nil { t.Error("Failed to parse CDR in form", err) } + if cdrAsForm.Get(utils.CDRSOURCE) != cgrConfig.CdrcSourceId { + t.Error("Unexpected cdrsource received", cdrAsForm.Get(utils.CDRSOURCE)) + } if cdrAsForm.Get(utils.REQTYPE) != "prepaid" { t.Error("Unexpected CDR value received", cdrAsForm.Get(utils.REQTYPE)) } diff --git a/cdrs/cgrcdr.go b/cdrs/cgrcdr.go index 69e141984..26054a0f5 100644 --- a/cdrs/cgrcdr.go +++ b/cdrs/cgrcdr.go @@ -41,48 +41,48 @@ func NewCgrCdrFromHttpReq(req *http.Request) (CgrCdr, error) { type CgrCdr map[string]string func (cgrCdr CgrCdr) GetCgrId() string { - return utils.FSCgrId(cgrCdr[ACCID]) + return utils.FSCgrId(cgrCdr[utils.ACCID]) } func (cgrCdr CgrCdr) GetAccId() string { - return cgrCdr[ACCID] + return cgrCdr[utils.ACCID] } func (cgrCdr CgrCdr) GetCdrHost() string { - return cgrCdr[CDRHOST] + return cgrCdr[utils.CDRHOST] } func (cgrCdr CgrCdr) GetCdrSource() string { - return cgrCdr[CDRSOURCE] + return cgrCdr[utils.CDRSOURCE] } func (cgrCdr CgrCdr) GetDirection() string { //TODO: implement direction return "*out" } func (cgrCdr CgrCdr) GetOrigId() string { - return cgrCdr[CDRHOST] + return cgrCdr[utils.CDRHOST] } func (cgrCdr CgrCdr) GetSubject() string { - return cgrCdr[SUBJECT] + return cgrCdr[utils.SUBJECT] } func (cgrCdr CgrCdr) GetAccount() string { - return cgrCdr[ACCOUNT] + return cgrCdr[utils.ACCOUNT] } // Charging destination number func (cgrCdr CgrCdr) GetDestination() string { - return cgrCdr[DESTINATION] + return cgrCdr[utils.DESTINATION] } func (cgrCdr CgrCdr) GetTOR() string { - return cgrCdr[TOR] + return cgrCdr[utils.TOR] } func (cgrCdr CgrCdr) GetTenant() string { - return cgrCdr[TENANT] + return cgrCdr[utils.TENANT] } func (cgrCdr CgrCdr) GetReqType() string { - return cgrCdr[REQTYPE] + return cgrCdr[utils.REQTYPE] } func (cgrCdr CgrCdr) GetExtraFields() map[string]string { extraFields := make(map[string]string) @@ -94,11 +94,11 @@ func (cgrCdr CgrCdr) GetExtraFields() map[string]string { return extraFields } func (cgrCdr CgrCdr) GetAnswerTime() (t time.Time, err error) { - return utils.ParseDate(cgrCdr[TIME_ANSWER]) + return utils.ParseDate(cgrCdr[utils.ANSWER_TIME]) } // Extracts duration as considered by the telecom switch func (cgrCdr CgrCdr) GetDuration() int64 { - dur, _ := strconv.ParseInt(cgrCdr[DURATION], 0, 64) + dur, _ := strconv.ParseInt(cgrCdr[utils.DURATION], 0, 64) return dur } diff --git a/cdrs/cgrcdr_test.go b/cdrs/cgrcdr_test.go index 4b9f46b53..b16a42de7 100644 --- a/cdrs/cgrcdr_test.go +++ b/cdrs/cgrcdr_test.go @@ -32,7 +32,7 @@ curl --data "accid=asbfdsaf&cdrhost=192.168.1.1&reqtype=rated&direction=*out&ten func TestCgrCdrFields(t *testing.T) { cfg, _ = config.NewDefaultCGRConfig() cgrCdr := CgrCdr{"accid": "dsafdsaf", "cdrhost": "192.168.1.1", "reqtype": "rated", "direction": "*out", "tenant": "cgrates.org", "tor": "call", - "account": "1001", "subject": "1001", "destination": "1002", "time_answer": "1383813746", "duration": "10", "field_extr1": "val_extr1", "fieldextr2": "valextr2"} + "account": "1001", "subject": "1001", "destination": "1002", "answer_time": "1383813746", "duration": "10", "field_extr1": "val_extr1", "fieldextr2": "valextr2"} if cgrCdr.GetCgrId() != utils.FSCgrId("dsafdsaf") { t.Error("Error parsing cdr: ", cgrCdr) } diff --git a/cmd/cgr-engine/cgr-engine.go b/cmd/cgr-engine/cgr-engine.go index a301a7935..e559088b3 100644 --- a/cmd/cgr-engine/cgr-engine.go +++ b/cmd/cgr-engine/cgr-engine.go @@ -32,6 +32,7 @@ import ( "github.com/cgrates/cgrates/apier/v1" "github.com/cgrates/cgrates/balancer2go" "github.com/cgrates/cgrates/cdrs" + "github.com/cgrates/cgrates/cdrc" "github.com/cgrates/cgrates/config" "github.com/cgrates/cgrates/engine" "github.com/cgrates/cgrates/history" @@ -133,10 +134,19 @@ func startMediator(responder *engine.Responder, loggerDb engine.LogStorage, cdrD engine.Logger.Crit(fmt.Sprintf("Mediator config parsing error: %v", err)) exitChan <- true } +} - if cfg.MediatorCDRType == utils.FSCDR_FILE_CSV { //Mediator as standalone service for file CDRs - medi.TrackCDRFiles() +func startCdrc() { + cdrc, err := cdrc.NewCdrc(cfg) + if err != nil { + engine.Logger.Crit(fmt.Sprintf("Cdrc config parsing error: %s", err.Error())) + exitChan <- true + return } + if err := cdrc.Run(); err != nil { + engine.Logger.Crit(fmt.Sprintf("Cdrc run error: %s", err.Error())) + } + exitChan <- true // If run stopped, something is bad, stop the application } func startSessionManager(responder *engine.Responder, loggerDb engine.LogStorage) { @@ -416,6 +426,10 @@ func main() { engine.Logger.Info("Starting History Service.") go startHistoryScribe() } + if cfg.CdrcEnabled { + engine.Logger.Info("Starting CGRateS CDR Client.") + go startCdrc() + } <-exitChan engine.Logger.Info("Stopped all components. CGRateS shutdown!") } diff --git a/config/config.go b/config/config.go index ccd03bb92..6749306e4 100644 --- a/config/config.go +++ b/config/config.go @@ -21,6 +21,7 @@ package config import ( "errors" "fmt" + "time" "code.google.com/p/goconf/conf" "github.com/cgrates/cgrates/utils" @@ -81,7 +82,7 @@ type CGRConfig struct { CdrcEnabled bool // Enable CDR client functionality CdrcCdrs string // Address where to reach CDR server CdrcCdrsMethod string // Mechanism to use when posting CDRs on server - CdrcRunDelay int // Sleep interval in seconds between consecutive runs, 0 to use automation via inotify + CdrcRunDelay time.Duration // Sleep interval between consecutive runs, if time unit missing, defaults to seconds, 0 to use automation via inotify CdrcCdrType string // CDR file format . CdrcCdrInDir string // Absolute path towards the directory where the CDRs are stored. CdrcCdrOutDir string // Absolute path towards the directory where processed CDRs will be moved. @@ -169,7 +170,7 @@ func (self *CGRConfig) setDefaults() error { self.CdrcEnabled = false self.CdrcCdrs = "127.0.0.1:2022" self.CdrcCdrsMethod = "http_cgr" - self.CdrcRunDelay = 0 + self.CdrcRunDelay = time.Duration(0) self.CdrcCdrType = "csv" self.CdrcCdrInDir = "/var/log/cgrates/cdr/in/csv" self.CdrcCdrOutDir = "/var/log/cgrates/cdr/out/csv" @@ -372,7 +373,10 @@ func loadConfig(c *conf.ConfigFile) (*CGRConfig, error) { cfg.CdrcCdrsMethod, _ = c.GetString("cdrc", "cdrs_method") } if hasOpt = c.HasOption("cdrc", "run_delay"); hasOpt { - cfg.CdrcRunDelay, _ = c.GetInt("cdrc", "run_delay") + durStr,_ := c.GetString("cdrc", "run_delay") + if cfg.CdrcRunDelay, errParse = utils.ParseDurationWithSecs(durStr); errParse != nil { + return nil, errParse + } } if hasOpt = c.HasOption("cdrc", "cdr_type"); hasOpt { cfg.CdrcCdrType, _ = c.GetString("cdrc", "cdr_type") diff --git a/config/config_test.go b/config/config_test.go index 06e8d3f0b..cc3f7716c 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -22,6 +22,7 @@ import ( "fmt" "reflect" "testing" + "time" "github.com/cgrates/cgrates/utils" ) @@ -73,7 +74,7 @@ func TestDefaults(t *testing.T) { eCfg.CdrcEnabled = false eCfg.CdrcCdrs = "127.0.0.1:2022" eCfg.CdrcCdrsMethod = "http_cgr" - eCfg.CdrcRunDelay = 0 + eCfg.CdrcRunDelay = time.Duration(0) eCfg.CdrcCdrType = "csv" eCfg.CdrcCdrInDir = "/var/log/cgrates/cdr/in/csv" eCfg.CdrcCdrOutDir = "/var/log/cgrates/cdr/out/csv" @@ -197,7 +198,7 @@ func TestConfigFromFile(t *testing.T) { eCfg.CdrcEnabled = true eCfg.CdrcCdrs = "test" eCfg.CdrcCdrsMethod = "test" - eCfg.CdrcRunDelay = 99 + eCfg.CdrcRunDelay = time.Duration(99)*time.Second eCfg.CdrcCdrType = "test" eCfg.CdrcCdrInDir = "test" eCfg.CdrcCdrOutDir = "test" diff --git a/console/get_cost.go b/console/get_cost.go index 1d0a691fa..a745b1e9d 100644 --- a/console/get_cost.go +++ b/console/get_cost.go @@ -21,7 +21,6 @@ package console import ( "fmt" "time" - "strconv" "github.com/cgrates/cgrates/engine" "github.com/cgrates/cgrates/utils" ) @@ -71,11 +70,7 @@ func (self *CmdGetCost) FromArgs(args []string) error { return fmt.Errorf(self.Usage("")) } } - if _, err := strconv.Atoi(args[7]); err == nil { // No suffix, default to seconds - fmt.Println("\n*duration* needs suffix, defaulting to *s* (valid suffixes: ns, us/µs, ms, s, m, h)\n") - args[7] += "s" - } - callDur, err := time.ParseDuration(args[7]) + callDur, err := utils.ParseDurationWithSecs(args[7]) if err != nil { fmt.Println("\n\tExample durations: 60s for 60 seconds, 25m for 25minutes, 1m25s for one minute and 25 seconds\n") return fmt.Errorf(self.Usage("")) diff --git a/mediator/mediator.go b/mediator/mediator.go index a86ccfd98..3e5755c23 100644 --- a/mediator/mediator.go +++ b/mediator/mediator.go @@ -19,18 +19,12 @@ along with this program. If not, see package mediator import ( - "bufio" - "encoding/csv" "errors" - "flag" "fmt" "github.com/cgrates/cgrates/config" "github.com/cgrates/cgrates/engine" "github.com/cgrates/cgrates/utils" - "os" - "path" "strconv" - "strings" "time" ) @@ -41,11 +35,9 @@ func NewMediator(connector engine.Connector, logDb engine.LogStorage, cdrDb engi cdrDb: cdrDb, cgrCfg: cfg, } - m.fieldNames = make(map[string][]string) - m.fieldIdxs = make(map[string][]int) - // Load config fields - if errLoad := m.loadConfig(); errLoad != nil { - return nil, errLoad + // Parse config + if err := m.parseConfig(); err != nil { + return nil, err } return m, nil } @@ -55,16 +47,10 @@ type Mediator struct { logDb engine.LogStorage cdrDb engine.CdrStorage cgrCfg *config.CGRConfig - cdrInDir, cdrOutDir string - accIdField string - accIdIdx int // Populated only for csv files where we have no names but indexes for the fields - fieldNames map[string][]string - fieldIdxs map[string][]int // Populated only for csv files where we have no names but indexes for the fields } /* -Responsible for loading configuration fields out of CGRConfig instance, doing the necessary pre-checks. -@param fieldKeys: stores the keys which will be referenced inside fieldNames/fieldIdxs +Responsible for parsing configuration fields out of CGRConfig instance, doing the necessary pre-checks. @param cfgVals: keep ordered references to configuration fields from CGRConfig instance. fieldKeys and cfgVals are directly related through index. Method logic: @@ -73,14 +59,12 @@ Method logic: * Accounting id field should not be empty. * If we run mediation on csv file: * Make sure cdrInDir and cdrOutDir are valid paths. - * Populate accIdIdx by converting accIdField into integer. * Populate fieldIdxs by converting fieldNames into integers */ -func (self *Mediator) loadConfig() error { - fieldKeys := []string{"subject", "reqtype", "direction", "tenant", "tor", "account", "destination", "time_start", "duration"} +func (self *Mediator) parseConfig() error { cfgVals := [][]string{self.cgrCfg.MediatorSubjectFields, self.cgrCfg.MediatorReqTypeFields, self.cgrCfg.MediatorDirectionFields, self.cgrCfg.MediatorTenantFields, self.cgrCfg.MediatorTORFields, self.cgrCfg.MediatorAccountFields, self.cgrCfg.MediatorDestFields, - self.cgrCfg.MediatorTimeAnswerFields, self.cgrCfg.MediatorDurationFields} + self.cgrCfg.MediatorAnswerTimeFields, self.cgrCfg.MediatorDurationFields} if len(self.cgrCfg.MediatorRunIds) == 0 { return errors.New("Unconfigured mediator run_ids") @@ -93,44 +77,6 @@ func (self *Mediator) loadConfig() error { } } - // AccIdField has no special requirements, should just exist - if len(self.cgrCfg.MediatorAccIdField) == 0 { - return errors.New("Undefined mediator accid field") - } - self.accIdField = self.cgrCfg.MediatorAccIdField - - var errConv error - // Specific settings of CSV style CDRS - if self.cgrCfg.MediatorCDRType == utils.FSCDR_FILE_CSV { - // Check paths to be valid before adding as configuration - if _, err := os.Stat(self.cgrCfg.MediatorCDRInDir); err != nil { - return fmt.Errorf("The input path for mediator does not exist: %v", self.cgrCfg.MediatorCDRInDir) - } else { - self.cdrInDir = self.cgrCfg.MediatorCDRInDir - } - if _, err := os.Stat(self.cgrCfg.MediatorCDROutDir); err != nil { - return fmt.Errorf("The output path for mediator does not exist: %v", self.cgrCfg.MediatorCDROutDir) - } else { - self.cdrOutDir = self.cgrCfg.MediatorCDROutDir - } - if self.accIdIdx, errConv = strconv.Atoi(self.cgrCfg.MediatorAccIdField); errConv != nil { - return errors.New("AccIdIndex must be integer.") - } - } - - // Load here field names and convert to integers in case of unamed cdrs like CSV - for idx, key := range fieldKeys { - self.fieldNames[key] = cfgVals[idx] - if self.cgrCfg.MediatorCDRType == utils.FSCDR_FILE_CSV { // Special case when field names represent indexes of their location in file - self.fieldIdxs[key] = make([]int, len(cfgVals[idx])) - for iStr, cfgStr := range cfgVals[idx] { - if self.fieldIdxs[key][iStr], errConv = strconv.Atoi(cfgStr); errConv != nil { - return fmt.Errorf("All mediator index members (%s) must be ints", key) - } - } - } - } - return nil } diff --git a/test.sh b/test.sh index 4355e005d..19add0394 100755 --- a/test.sh +++ b/test.sh @@ -8,6 +8,7 @@ go test -i github.com/cgrates/cgrates/mediator go test -i github.com/cgrates/fsock go test -i github.com/cgrates/cgrates/cache2go go test -i github.com/cgrates/cgrates/cdrs +go test -i github.com/cgrates/cgrates/cdrc go test -i github.com/cgrates/cgrates/utils go test -i github.com/cgrates/cgrates/history go test -i github.com/cgrates/cgrates/cdrexporter @@ -23,7 +24,9 @@ cr=$? go test github.com/cgrates/cgrates/mediator md=$? go test github.com/cgrates/cgrates/cdrs -cdr=$? +cdrs=$? +go test github.com/cgrates/cgrates/cdrc +cdrcs=$? go test github.com/cgrates/cgrates/utils ut=$? go test github.com/cgrates/fsock @@ -36,4 +39,4 @@ go test github.com/cgrates/cgrates/cdrexporter cdre=$? -exit $en && $sm && $cfg && $bl && $cr && $md && $cdr && $fs && $ut && $hs && $c2g && $cdre +exit $en && $sm && $cfg && $bl && $cr && $md && $cdrs && $cdrc && $fs && $ut && $hs && $c2g && $cdre diff --git a/utils/cdr.go b/utils/cdr.go index dc1f179aa..1dda1e7b3 100644 --- a/utils/cdr.go +++ b/utils/cdr.go @@ -22,12 +22,13 @@ import ( "time" ) -var PrimaryCdrFields []string = []string{ACCID, CDRHOST, REQTYPE, DIRECTION, TENANT, TOR, ACCOUNT, SUBJECT, DESTINATION, ANSWER_TIME, DURATION} +var PrimaryCdrFields []string = []string{ACCID, CDRHOST, CDRSOURCE, REQTYPE, DIRECTION, TENANT, TOR, ACCOUNT, SUBJECT, DESTINATION, ANSWER_TIME, DURATION} type CDR interface { GetCgrId() string GetAccId() string GetCdrHost() string + GetCdrSource() string GetDirection() string GetSubject() string GetAccount() string diff --git a/utils/coreutils.go b/utils/coreutils.go index d1407ff10..d88de4767 100644 --- a/utils/coreutils.go +++ b/utils/coreutils.go @@ -148,3 +148,11 @@ func CopyHour(src, dest time.Time) time.Time { } return time.Date(dest.Year(), dest.Month(), dest.Day(), src.Hour(), src.Minute(), src.Second(), src.Nanosecond(), src.Location()) } + +// Parses duration, considers s as time unit if not provided +func ParseDurationWithSecs(durStr string) (time.Duration, error) { + if _, err := strconv.Atoi(durStr); err == nil { // No suffix, default to seconds + durStr += "s" + } + return time.ParseDuration(durStr) +} diff --git a/utils/ratedcdr.go b/utils/ratedcdr.go index 94801062f..67f2228f0 100644 --- a/utils/ratedcdr.go +++ b/utils/ratedcdr.go @@ -27,6 +27,7 @@ type RatedCDR struct { CgrId string AccId string CdrHost string + CdrSource string ReqType string Direction string Tenant string @@ -52,6 +53,10 @@ func (ratedCdr *RatedCDR) GetCdrHost() string { return ratedCdr.CdrHost } +func (ratedCdr *RatedCDR) GetCdrSource() string { + return ratedCdr.CdrSource +} + func (ratedCdr *RatedCDR) GetDirection() string { return ratedCdr.Direction } diff --git a/utils/utils_test.go b/utils/utils_test.go index d642bac42..b6ee70cd8 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -247,3 +247,28 @@ func TestSplitPrefixEmpty(t *testing.T) { t.Error("Error splitting prefix: ", a) } } + +func TestParseDurationWithSecs(t *testing.T) { + durStr := "2" + durExpected := time.Duration(2)*time.Second + if parsed, err := ParseDurationWithSecs(durStr); err != nil { + t.Error(err) + } else if parsed != durExpected { + t.Error("Parsed different than expected") + } + durStr = "2s" + if parsed, err := ParseDurationWithSecs(durStr); err != nil { + t.Error(err) + } else if parsed != durExpected { + t.Error("Parsed different than expected") + } + durStr = "2ms" + durExpected = time.Duration(2)*time.Millisecond + if parsed, err := ParseDurationWithSecs(durStr); err != nil { + t.Error(err) + } else if parsed != durExpected { + t.Error("Parsed different than expected") + } +} + +