From c02e49cbd1007ba7f4be9dd7fa7132874cb2f8a3 Mon Sep 17 00:00:00 2001 From: DanB Date: Mon, 21 Aug 2017 20:49:18 +0200 Subject: [PATCH] EventCost fixes, more testing --- engine/eventcost.go | 136 +++++++++++---------- engine/eventcost_test.go | 247 +++++++++++++++++++++++++++++---------- 2 files changed, 259 insertions(+), 124 deletions(-) diff --git a/engine/eventcost.go b/engine/eventcost.go index 1a4031f1c..6295df25b 100644 --- a/engine/eventcost.go +++ b/engine/eventcost.go @@ -19,6 +19,7 @@ package engine import ( "errors" + "fmt" "time" "github.com/cgrates/cgrates/utils" @@ -264,6 +265,7 @@ func (ec *EventCost) AsCallCost() *CallCost { ts.TimeEnd = ts.TimeStart.Add( time.Duration(cIl.Usage().Nanoseconds() * int64(cIl.CompressFactor))) if cIl.RatingID != "" { + //fmt.Printf("Checking RatingID: <%s>\n", cIl.RatingID) if ec.Rating[cIl.RatingID].RatingFiltersID != "" { rfs := ec.RatingFilters[ec.Rating[cIl.RatingID].RatingFiltersID] ts.MatchedSubject = rfs["Subject"].(string) @@ -460,6 +462,7 @@ func (ec *EventCost) Trim(atUsage time.Duration) (srplusEC *EventCost, err error if ec.Usage == nil { ec.GetUsage() } + fmt.Printf("Trim, atUsage: %v, eventCostUsage: %v\n", atUsage, ec.Usage) origECUsage := ec.GetUsage() if atUsage >= *ec.Usage { return // no trim @@ -480,7 +483,7 @@ func (ec *EventCost) Trim(atUsage time.Duration) (srplusEC *EventCost, err error srplusEC.StartTime = ec.StartTime srplusEC.AccountSummary = ec.AccountSummary.Clone() - var lastActiveCIlIdx *int // marks last index which should stay with ec + var lastActiveCIlIdx *int // mark last index which should stay with ec for i, cIl := range ec.Charges { if cIl.ecUsageIdx == nil { ec.ComputeEventCostUsageIndexes() @@ -504,12 +507,14 @@ func (ec *EventCost) Trim(atUsage time.Duration) (srplusEC *EventCost, err error } srplusEC.Charges = ec.Charges[*lastActiveCIlIdx+1:] ec.Charges = ec.Charges[:*lastActiveCIlIdx+1] - - if lastActiveCIl.CompressFactor != 1 { // Split based on compress factor if needed + ec.Usage = nil + ec.Cost = nil + if lastActiveCIl.CompressFactor != 1 && + *lastActiveCIl.ecUsageIdx+*lastActiveCIl.TotalUsage() > atUsage { // Split based on compress factor if needed var laCF int for ciCnt := 1; ciCnt <= lastActiveCIl.CompressFactor; ciCnt++ { if *lastActiveCIl.ecUsageIdx+ - time.Duration(lastActiveCIl.usage.Nanoseconds()*int64(ciCnt)) > atUsage { + time.Duration(lastActiveCIl.usage.Nanoseconds()*int64(ciCnt)) >= atUsage { laCF = ciCnt break } @@ -523,72 +528,83 @@ func (ec *EventCost) Trim(atUsage time.Duration) (srplusEC *EventCost, err error srplsCIl.CompressFactor = lastActiveCIl.CompressFactor - laCF srplusEC.Charges = append([]*ChargingInterval{srplsCIl}, srplusEC.Charges...) // prepend surplus CIl lastActiveCIl.CompressFactor = laCF // correct compress factor + ec.Usage = nil + ec.Cost = nil } } - atUsage = atUsage - time.Duration(lastActiveCIl.ecUsageIdx.Nanoseconds()*int64(lastActiveCIl.CompressFactor)) // remaining duration to cover in increments - - // find out last increment covering duration - var lastActiveCItIdx *int - var incrementsUsage time.Duration - for i, cIt := range lastActiveCIl.Increments { - incrementsUsage += cIt.TotalUsage() - if incrementsUsage >= atUsage { - lastActiveCItIdx = utils.IntPointer(i) - break - } - } - if lastActiveCItIdx == nil { // bug in increments - return nil, errors.New("no active increment found") - } - lastActiveCIts := lastActiveCIl.Increments // so we can modify the reference in case we have surplus - lastIncrement := lastActiveCIts[*lastActiveCItIdx] - - if lastIncrement.CompressFactor == 0 { - return nil, errors.New("empty compress factor in increment") - } - - var srplsIncrements []*ChargingIncrement - if *lastActiveCItIdx < len(lastActiveCIl.Increments)-1 { // less that complete increments, have surplus - srplsIncrements = lastActiveCIts[*lastActiveCItIdx+1:] - lastActiveCIts = lastActiveCIts[:*lastActiveCItIdx+1] - } - var laItCF int - if lastIncrement.CompressFactor != 1 { // detect the increment covering the last part of usage - incrementsUsage -= lastIncrement.TotalUsage() - for cnt := 1; cnt <= lastIncrement.CompressFactor; cnt++ { - incrementsUsage += lastIncrement.Usage + if atUsage != ec.GetUsage()+*lastActiveCIl.TotalUsage() { // lastInterval covering more than needed, need split + atUsage -= (ec.GetUsage() - *lastActiveCIl.TotalUsage()) // remaining duration to cover in increments of the last charging interval + // find out last increment covering duration + var lastActiveCItIdx *int + var incrementsUsage time.Duration + for i, cIt := range lastActiveCIl.Increments { + incrementsUsage += cIt.TotalUsage() if incrementsUsage >= atUsage { - laItCF = cnt + lastActiveCItIdx = utils.IntPointer(i) break } } - if laItCF == 0 { - return nil, errors.New("cannot detect last active CompressFactor in ChargingIncrement") + if lastActiveCItIdx == nil { // bug in increments + return nil, errors.New("no active increment found") } - if laItCF != lastIncrement.CompressFactor { - srplsIncrement := lastIncrement.Clone() - srplsIncrement.CompressFactor = srplsIncrement.CompressFactor - laItCF - srplsIncrements = append([]*ChargingIncrement{srplsIncrement}, srplsIncrements...) // prepend the surplus out of compress + lastActiveCIts := lastActiveCIl.Increments // so we can modify the reference in case we have surplus + lastIncrement := lastActiveCIts[*lastActiveCItIdx] + if lastIncrement.CompressFactor == 0 { + return nil, errors.New("empty compress factor in increment") } - } + var srplsIncrements []*ChargingIncrement + if *lastActiveCItIdx < len(lastActiveCIl.Increments)-1 { // less that complete increments, have surplus + srplsIncrements = lastActiveCIts[*lastActiveCItIdx+1:] + lastActiveCIts = lastActiveCIts[:*lastActiveCItIdx+1] + ec.Usage = nil + ec.Cost = nil + } + var laItCF int + if lastIncrement.CompressFactor != 1 && atUsage != incrementsUsage { + // last increment compress factor is higher that we need to cover + incrementsUsage -= lastIncrement.TotalUsage() + for cnt := 1; cnt <= lastIncrement.CompressFactor; cnt++ { + incrementsUsage += lastIncrement.Usage + if incrementsUsage >= atUsage { + laItCF = cnt + break + } + } + if laItCF == 0 { + return nil, errors.New("cannot detect last active CompressFactor in ChargingIncrement") + } + if laItCF != lastIncrement.CompressFactor { + srplsIncrement := lastIncrement.Clone() + srplsIncrement.CompressFactor = srplsIncrement.CompressFactor - laItCF + srplsIncrements = append([]*ChargingIncrement{srplsIncrement}, srplsIncrements...) // prepend the surplus out of compress + lastIncrement.CompressFactor = laItCF + ec.Usage = nil + ec.Cost = nil - if len(srplsIncrements) != 0 { // partially covering, need trim + } + } - if lastActiveCIl.CompressFactor > 1 { // ChargingInterval not covering in full, need to split it - lastActiveCIl.CompressFactor -= 1 - ec.Charges = append(ec.Charges, lastActiveCIl.Clone()) - lastActiveCIl = ec.Charges[len(ec.Charges)-1] - lastActiveCIl.CompressFactor = 1 - } - srplsCIl := lastActiveCIl.Clone() - srplsCIl.Increments = srplsIncrements - srplusEC.Charges = append([]*ChargingInterval{srplsCIl}, srplusEC.Charges...) - lastActiveCIl.Increments = make([]*ChargingIncrement, len(lastActiveCIts)) - for i, incr := range lastActiveCIts { - lastActiveCIl.Increments[i] = incr.Clone() // avoid pointer references to the other interval - } - if laItCF != 0 { - lastActiveCIl.Increments[len(lastActiveCIl.Increments)-1].CompressFactor = laItCF // correct the compressFactor for the last increment + if len(srplsIncrements) != 0 { // partially covering, need trim + if lastActiveCIl.CompressFactor > 1 { // ChargingInterval not covering in full, need to split it + lastActiveCIl.CompressFactor -= 1 + ec.Charges = append(ec.Charges, lastActiveCIl.Clone()) + lastActiveCIl = ec.Charges[len(ec.Charges)-1] + lastActiveCIl.CompressFactor = 1 + ec.Usage = nil + ec.Cost = nil + } + srplsCIl := lastActiveCIl.Clone() + srplsCIl.Increments = srplsIncrements + srplusEC.Charges = append([]*ChargingInterval{srplsCIl}, srplusEC.Charges...) + lastActiveCIl.Increments = make([]*ChargingIncrement, len(lastActiveCIts)) + for i, incr := range lastActiveCIts { + lastActiveCIl.Increments[i] = incr.Clone() // avoid pointer references to the other interval + } + if laItCF != 0 { + lastActiveCIl.Increments[len(lastActiveCIl.Increments)-1].CompressFactor = laItCF // correct the compressFactor for the last increment + ec.Usage = nil + ec.Cost = nil + } } } ec.ResetCounters() diff --git a/engine/eventcost_test.go b/engine/eventcost_test.go index 9f6d91b72..badab8e31 100644 --- a/engine/eventcost_test.go +++ b/engine/eventcost_test.go @@ -18,6 +18,7 @@ along with this program. If not, see package engine import ( + "fmt" "reflect" "testing" "time" @@ -73,6 +74,30 @@ var testEC = &EventCost{ }, CompressFactor: 4, }, + &ChargingInterval{ + RatingID: "c1a5ab9", + Increments: []*ChargingIncrement{ + &ChargingIncrement{ + Usage: time.Duration(1 * time.Second), + Cost: 0, + AccountingID: "3455b83", + CompressFactor: 10, + }, + &ChargingIncrement{ + Usage: time.Duration(10 * time.Second), + Cost: 0.01, + AccountingID: "a012888", + CompressFactor: 2, + }, + &ChargingIncrement{ + Usage: time.Duration(1 * time.Second), + Cost: 0.005, + AccountingID: "44d6c02", + CompressFactor: 30, + }, + }, + CompressFactor: 5, + }, }, AccountSummary: &AccountSummary{ Tenant: "cgrates.org", @@ -185,22 +210,79 @@ func TestECClone(t *testing.T) { t.Errorf("Expecting: %s, received: %s", utils.ToJSON(testEC), utils.ToJSON(ec)) } + // making sure we don't influence the original values + ec.Usage = utils.DurationPointer(time.Duration(1 * time.Second)) + if testEC.Usage != nil { + t.Error("Usage is not nil") + } + ec.Cost = utils.Float64Pointer(1.0) + if testEC.Cost != nil { + t.Error("Cost is not nil") + } + ec.Charges[0].Increments[0].Cost = 1.0 + if testEC.Charges[0].Increments[0].Cost == 1.0 { + t.Error("Cost is 1.0") + } + ec.AccountSummary.Disabled = true + if testEC.AccountSummary.Disabled { + t.Error("Account is disabled") + } + ec.AccountSummary.BalanceSummaries[0].Value = 5.0 + if testEC.AccountSummary.BalanceSummaries[0].Value == 5.0 { + t.Error("Wrong balance summary") + } + ec.Rates["ec1a177"][0].Value = 5.0 + if testEC.Rates["ec1a177"][0].Value == 5.0 { + t.Error("Wrong Value") + } + delete(ec.Rates, "ec1a177") + if _, has := testEC.Rates["ec1a177"]; !has { + t.Error("Key removed from testEC") + } + ec.Timings["7f324ab"].StartTime = "10:00:00" + if testEC.Timings["7f324ab"].StartTime == "10:00:00" { + t.Error("Wrong StartTime") + } + delete(ec.Timings, "7f324ab") + if _, has := testEC.Timings["7f324ab"]; !has { + t.Error("Key removed from testEC") + } + ec.RatingFilters["43e77dc"]["DestinationID"] = "GERMANY_MOBILE" + if testEC.RatingFilters["43e77dc"]["DestinationID"] == "GERMANY_MOBILE" { + t.Error("Wrong DestinationID") + } + delete(ec.RatingFilters, "43e77dc") + if _, has := testEC.RatingFilters["43e77dc"]; !has { + t.Error("Key removed from testEC") + } + ec.Accounting["a012888"].Units = 5.0 + if testEC.Accounting["a012888"].Units == 5.0 { + t.Error("Wrong Units") + } + delete(ec.Accounting, "a012888") + if _, has := testEC.Accounting["a012888"]; !has { + t.Error("Key removed from testEC") + } + } func TestECComputeAndReset(t *testing.T) { ec := testEC.Clone() eEc := testEC.Clone() - eEc.Usage = utils.DurationPointer(time.Duration(5 * time.Minute)) - eEc.Cost = utils.Float64Pointer(2.67) + eEc.Usage = utils.DurationPointer(time.Duration(10 * time.Minute)) + eEc.Cost = utils.Float64Pointer(3.52) eEc.Charges[0].ecUsageIdx = utils.DurationPointer(time.Duration(0)) eEc.Charges[0].usage = utils.DurationPointer(time.Duration(1 * time.Minute)) eEc.Charges[0].cost = utils.Float64Pointer(0.27) eEc.Charges[1].ecUsageIdx = utils.DurationPointer(time.Duration(1 * time.Minute)) eEc.Charges[1].usage = utils.DurationPointer(time.Duration(1 * time.Minute)) eEc.Charges[1].cost = utils.Float64Pointer(0.6) + eEc.Charges[2].ecUsageIdx = utils.DurationPointer(time.Duration(5 * time.Minute)) + eEc.Charges[2].usage = utils.DurationPointer(time.Duration(1 * time.Minute)) + eEc.Charges[2].cost = utils.Float64Pointer(0.17) ec.Compute() if !reflect.DeepEqual(eEc, ec) { - t.Errorf("Expecting: %+v, received: %+v", eEc, ec) + t.Errorf("Expecting: %s\n, received: %s", utils.ToJSON(eEc), utils.ToJSON(ec)) } ec.ResetCounters() if !reflect.DeepEqual(testEC, ec) { @@ -903,15 +985,16 @@ func TestECAsCallCost(t *testing.T) { func TestECTrimZeroAndFull(t *testing.T) { ec := testEC.Clone() - if srplsEC, err := ec.Trim(time.Duration(5 * time.Minute)); err != nil { + if srplsEC, err := ec.Trim(time.Duration(10 * time.Minute)); err != nil { t.Error(err) } else if srplsEC != nil { t.Errorf("Expecting nil, got: %+v", srplsEC) } eFullSrpls := testEC.Clone() - eFullSrpls.Usage = utils.DurationPointer(time.Duration(5 * time.Minute)) + eFullSrpls.Usage = utils.DurationPointer(time.Duration(10 * time.Minute)) eFullSrpls.Charges[0].usage = utils.DurationPointer(time.Duration(1 * time.Minute)) eFullSrpls.Charges[1].usage = utils.DurationPointer(time.Duration(1 * time.Minute)) + eFullSrpls.Charges[2].usage = utils.DurationPointer(time.Duration(1 * time.Minute)) if srplsEC, err := ec.Trim(time.Duration(0)); err != nil { t.Error(err) } else if !reflect.DeepEqual(eFullSrpls, srplsEC) { @@ -981,6 +1064,7 @@ func TestECTrimMiddle1(t *testing.T) { }, } eSrplsEC := testEC.Clone() + eSrplsEC.StartTime = time.Date(2017, 1, 9, 16, 21, 31, 0, time.UTC) eSrplsEC.Charges = []*ChargingInterval{ &ChargingInterval{ RatingID: "c1a5ab9", @@ -1006,86 +1090,120 @@ func TestECTrimMiddle1(t *testing.T) { }, CompressFactor: 1, }, + &ChargingInterval{ + RatingID: "c1a5ab9", + Increments: []*ChargingIncrement{ + &ChargingIncrement{ + Usage: time.Duration(1 * time.Second), + Cost: 0, + AccountingID: "3455b83", + CompressFactor: 10, + }, + &ChargingIncrement{ + Usage: time.Duration(10 * time.Second), + Cost: 0.01, + AccountingID: "a012888", + CompressFactor: 2, + }, + &ChargingIncrement{ + Usage: time.Duration(1 * time.Second), + Cost: 0.005, + AccountingID: "44d6c02", + CompressFactor: 30, + }, + }, + CompressFactor: 5, + }, } reqDuration := time.Duration(190 * time.Second) - initDur := ec.GetUsage() srplsEC, err := ec.Trim(reqDuration) if err != nil { t.Error(err) } if reqDuration != *ec.Usage { - t.Logf("\teEC: %s\n\tEC: %s\n\torigEC: %s\n", utils.ToJSON(eEC), utils.ToJSON(ec), utils.ToJSON(testEC)) t.Errorf("Expecting request duration: %v, received: %v", reqDuration, *ec.Usage) } - if srplsUsage := srplsEC.GetUsage(); srplsUsage != time.Duration(110*time.Second) { - t.Errorf("Expecting surplus duration: %v, received: %v", initDur-reqDuration, srplsUsage) + eSrplsDur := time.Duration(410 * time.Second) + if srplsUsage := srplsEC.GetUsage(); srplsUsage != eSrplsDur { + t.Errorf("Expecting surplus duration: %v, received: %v", eSrplsDur, srplsUsage) } + ec.ResetCounters() + srplsEC.ResetCounters() if !reflect.DeepEqual(eEC, ec) { - //t.Errorf("Expecting: %s, received: %s", utils.ToJSON(eEC), utils.ToJSON(ec)) - } else if !reflect.DeepEqual(eSrplsEC, srplsEC) { - //t.Errorf("Expecting: %s, received: %s", utils.ToJSON(eSrplsEC), utils.ToJSON(srplsEC)) + t.Errorf("Expecting: %s\n, received: %s", utils.ToIJSON(eEC), utils.ToIJSON(ec)) + } + // test surplus, which is not easy to estimate due it's different item ids + if !eSrplsEC.StartTime.Equal(srplsEC.StartTime) || + len(eSrplsEC.Charges) != len(srplsEC.Charges) { + t.Errorf("Expecting: \n%s, received: \n%s", utils.ToIJSON(eSrplsEC), utils.ToIJSON(srplsEC)) } } // TestECTrimMUsage is targeting simpler testing of the durations trimmed/remainders +// using subtests so we can cover the tests with less code func TestECTrimMUsage(t *testing.T) { - ec := testEC.Clone() - atUsage := time.Duration(5 * time.Second) - srplsEC, _ := ec.Trim(atUsage) - if ec.GetUsage() != atUsage { - t.Errorf("Wrongly trimmed EC: %s", utils.ToJSON(ec)) + // each subtest will trim at some usage duration + testCases := []struct { + atUsage time.Duration + ecUsage time.Duration + ecCost float64 + srplsUsage time.Duration + srplsCost float64 + }{ + {time.Duration(5 * time.Second), time.Duration(5 * time.Second), 0.1, + time.Duration(595 * time.Second), 3.42}, + {time.Duration(10 * time.Second), time.Duration(10 * time.Second), 0.1, + time.Duration(590 * time.Second), 3.42}, + {time.Duration(15 * time.Second), time.Duration(20 * time.Second), 0.11, + time.Duration(580 * time.Second), 3.41}, + {time.Duration(20 * time.Second), time.Duration(20 * time.Second), 0.11, + time.Duration(580 * time.Second), 3.41}, + {time.Duration(25 * time.Second), time.Duration(30 * time.Second), 0.12, + time.Duration(570 * time.Second), 3.40}, + {time.Duration(38 * time.Second), time.Duration(38 * time.Second), 0.16, + time.Duration(562 * time.Second), 3.36}, + {time.Duration(60 * time.Second), time.Duration(60 * time.Second), 0.27, + time.Duration(540 * time.Second), 3.25}, + {time.Duration(62 * time.Second), time.Duration(62 * time.Second), 0.29, + time.Duration(538 * time.Second), 3.23}, + {time.Duration(120 * time.Second), time.Duration(120 * time.Second), 0.87, + time.Duration(480 * time.Second), 2.65}, } - if srplsEC.GetUsage() != time.Duration(295*time.Second) { - t.Errorf("Wrong surplusEC: %s", utils.ToJSON(ec)) - } - ec = testEC.Clone() - atUsage = time.Duration(10 * time.Second) - srplsEC, _ = ec.Trim(atUsage) - if ec.GetUsage() != atUsage { - t.Errorf("Wrongly trimmed EC: %s", utils.ToJSON(ec)) - } - if srplsEC.GetUsage() != time.Duration(290*time.Second) { - t.Errorf("Wrong surplusEC: %s", utils.ToJSON(srplsEC)) - } - ec = testEC.Clone() - atUsage = time.Duration(15 * time.Second) - srplsEC, _ = ec.Trim(atUsage) - if ec.GetUsage() != time.Duration(20*time.Second) { - t.Errorf("Wrongly trimmed EC: %s", utils.ToJSON(ec)) - } - if srplsEC.GetUsage() != time.Duration(280*time.Second) { - t.Errorf("Wrong surplusEC: %s", utils.ToJSON(srplsEC)) - } - ec = testEC.Clone() - atUsage = time.Duration(25 * time.Second) - srplsEC, _ = ec.Trim(atUsage) - if ec.GetUsage() != time.Duration(30*time.Second) { - t.Errorf("Wrongly trimmed EC: %s", utils.ToJSON(ec)) - } - if srplsEC.GetUsage() != time.Duration(270*time.Second) { - t.Errorf("Wrong surplusEC: %s", utils.ToJSON(srplsEC)) - } - ec = testEC.Clone() - atUsage = time.Duration(38 * time.Second) - srplsEC, _ = ec.Trim(atUsage) - if ec.GetUsage() != atUsage { - t.Errorf("Wrongly trimmed EC: %s", utils.ToJSON(ec)) - } - if srplsEC.GetUsage() != time.Duration(262*time.Second) { - t.Errorf("Wrong surplusEC: %s", utils.ToJSON(srplsEC)) - } - ec = testEC.Clone() - atUsage = time.Duration(61 * time.Second) - srplsEC, _ = ec.Trim(atUsage) - if ec.GetUsage() != atUsage { - t.Errorf("Wrongly trimmed EC: %s", utils.ToJSON(ec)) - } - if srplsEC.GetUsage() != time.Duration(239*time.Second) { - t.Errorf("Wrong surplusEC: %s", utils.ToJSON(srplsEC)) + for _, tC := range testCases { + t.Run(fmt.Sprintf("AtUsage:%s", tC.atUsage), func(t *testing.T) { + var ec, srplsEC *EventCost + ec = testEC.Clone() + if srplsEC, err = ec.Trim(tC.atUsage); err != nil { + t.Error(err) + } + if ec.GetUsage() != tC.ecUsage { + t.Errorf("Wrongly trimmed EC: %s", utils.ToIJSON(ec)) + } else if ec.GetCost() != tC.ecCost { + t.Errorf("Wrong cost for event: %s", utils.ToIJSON(ec)) + } + if srplsEC.GetUsage() != tC.srplsUsage { + t.Errorf("Wrong usage: %v for surplusEC: %s", srplsEC.GetUsage(), utils.ToIJSON(srplsEC)) + } else if srplsEC.GetCost() != tC.srplsCost { + t.Errorf("Wrong cost: %f in surplus: %s", srplsEC.GetCost(), utils.ToIJSON(srplsEC)) + } + }) } + /* + + ec = testEC.Clone() + atUsage = time.Duration(61 * time.Second) + srplsEC, _ = ec.Trim(atUsage) + if ec.GetUsage() != atUsage { + t.Errorf("Wrongly trimmed EC: %s", utils.ToJSON(ec)) + } + if srplsEC.GetUsage() != time.Duration(239*time.Second) { + t.Errorf("Wrong surplusEC: %s", utils.ToJSON(srplsEC)) + } + */ } +/* func TestECMerge(t *testing.T) { ec := NewBareEventCost() ec.CGRID = testEC.CGRID @@ -1135,3 +1253,4 @@ func TestECMerge(t *testing.T) { t.Errorf("Unexpected EC after merge: %s", utils.ToJSON(ec)) } } +*/