mirror of
https://github.com/ccfos/nightingale.git
synced 2026-03-08 00:49:00 +00:00
Compare commits
5 Commits
fix-sql
...
event-trig
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aff584657c | ||
|
|
820286988f | ||
|
|
07a94e2eef | ||
|
|
8f2c5dea69 | ||
|
|
db58975575 |
@@ -6,19 +6,21 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/unit"
|
||||
"github.com/prometheus/common/model"
|
||||
)
|
||||
|
||||
type AnomalyPoint struct {
|
||||
Key string `json:"key"`
|
||||
Labels model.Metric `json:"labels"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Value float64 `json:"value"`
|
||||
Severity int `json:"severity"`
|
||||
Triggered bool `json:"triggered"`
|
||||
Query string `json:"query"`
|
||||
Values string `json:"values"`
|
||||
RecoverConfig models.RecoverConfig `json:"recover_config"`
|
||||
Key string `json:"key"`
|
||||
Labels model.Metric `json:"labels"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Value float64 `json:"value"`
|
||||
Severity int `json:"severity"`
|
||||
Triggered bool `json:"triggered"`
|
||||
Query string `json:"query"`
|
||||
Values string `json:"values"`
|
||||
ValuesUnit map[string]unit.FormattedValue `json:"values_unit"`
|
||||
RecoverConfig models.RecoverConfig `json:"recover_config"`
|
||||
}
|
||||
|
||||
func NewAnomalyPoint(key string, labels map[string]string, ts int64, value float64, severity int) AnomalyPoint {
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/ccfos/nightingale/v6/pkg/hash"
|
||||
"github.com/ccfos/nightingale/v6/pkg/parser"
|
||||
promsdk "github.com/ccfos/nightingale/v6/pkg/prom"
|
||||
"github.com/ccfos/nightingale/v6/pkg/unit"
|
||||
"github.com/ccfos/nightingale/v6/prom"
|
||||
"github.com/ccfos/nightingale/v6/tdengine"
|
||||
|
||||
@@ -247,7 +248,11 @@ func (arw *AlertRuleWorker) GetPromAnomalyPoint(ruleConfig string) ([]common.Ano
|
||||
for i := 0; i < len(points); i++ {
|
||||
points[i].Severity = query.Severity
|
||||
points[i].Query = promql
|
||||
points[i].ValuesUnit = map[string]unit.FormattedValue{
|
||||
"v": unit.ValueFormatter(query.Unit, 2, points[i].Value),
|
||||
}
|
||||
}
|
||||
|
||||
lst = append(lst, points...)
|
||||
}
|
||||
return lst, nil
|
||||
@@ -471,11 +476,22 @@ func GetAnomalyPoint(ruleId int64, ruleQuery models.RuleQuery, seriesTagIndexes
|
||||
return points, recoverPoints
|
||||
}
|
||||
|
||||
unitMap := make(map[string]string)
|
||||
for _, query := range ruleQuery.Queries {
|
||||
ref, unit, err := GetQueryRefAndUnit(query)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
unitMap[ref] = unit
|
||||
}
|
||||
|
||||
for _, trigger := range ruleQuery.Triggers {
|
||||
// seriesTagIndex 的 key 仅做分组使用,value 为每组 series 的 hash
|
||||
seriesTagIndex := ProcessJoins(ruleId, trigger, seriesTagIndexes, seriesStore)
|
||||
|
||||
for _, seriesHash := range seriesTagIndex {
|
||||
valuesUnitMap := make(map[string]unit.FormattedValue)
|
||||
|
||||
sort.Slice(seriesHash, func(i, j int) bool {
|
||||
return seriesHash[i] < seriesHash[j]
|
||||
})
|
||||
@@ -501,6 +517,10 @@ func GetAnomalyPoint(ruleId int64, ruleQuery models.RuleQuery, seriesTagIndexes
|
||||
continue
|
||||
}
|
||||
|
||||
if u, exists := unitMap[series.Ref]; exists {
|
||||
valuesUnitMap[series.Ref] = unit.ValueFormatter(u, 2, v)
|
||||
}
|
||||
|
||||
m["$"+series.Ref] = v
|
||||
m["$"+series.Ref+"."+series.MetricName()] = v
|
||||
ts = int64(t)
|
||||
@@ -529,6 +549,7 @@ func GetAnomalyPoint(ruleId int64, ruleQuery models.RuleQuery, seriesTagIndexes
|
||||
Triggered: isTriggered,
|
||||
Query: fmt.Sprintf("query:%+v trigger:%+v", ruleQuery.Queries, trigger),
|
||||
RecoverConfig: trigger.RecoverConfig,
|
||||
ValuesUnit: valuesUnitMap,
|
||||
}
|
||||
|
||||
if sample.Query != "" {
|
||||
@@ -815,3 +836,18 @@ func GetQueryRef(query interface{}) (string, error) {
|
||||
|
||||
return refField.String(), nil
|
||||
}
|
||||
|
||||
func GetQueryRefAndUnit(query interface{}) (string, string, error) {
|
||||
type Query struct {
|
||||
Ref string `json:"ref"`
|
||||
Unit string `json:"unit"`
|
||||
}
|
||||
|
||||
queryMap := Query{}
|
||||
queryBytes, err := json.Marshal(query)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
json.Unmarshal(queryBytes, &queryMap)
|
||||
return queryMap.Ref, queryMap.Unit, nil
|
||||
}
|
||||
|
||||
@@ -204,6 +204,7 @@ func (p *Processor) BuildEvent(anomalyPoint common.AnomalyPoint, from string, no
|
||||
event.TargetNote = p.targetNote
|
||||
event.TriggerValue = anomalyPoint.ReadableValue()
|
||||
event.TriggerValues = anomalyPoint.Values
|
||||
event.TriggerValuesJson = models.EventTriggerValues{ValuesWithUnit: anomalyPoint.ValuesUnit}
|
||||
event.TagsJSON = p.tagsArr
|
||||
event.Tags = strings.Join(p.tagsArr, ",,")
|
||||
event.IsRecovered = false
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/poster"
|
||||
"github.com/ccfos/nightingale/v6/pkg/tplx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/unit"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
@@ -49,6 +50,7 @@ type AlertCurEvent struct {
|
||||
TriggerTime int64 `json:"trigger_time"`
|
||||
TriggerValue string `json:"trigger_value"`
|
||||
TriggerValues string `json:"trigger_values" gorm:"-"`
|
||||
TriggerValuesJson EventTriggerValues `json:"trigger_values_json" gorm:"-"`
|
||||
Tags string `json:"-"` // for db
|
||||
TagsJSON []string `json:"tags" gorm:"-"` // for fe
|
||||
TagsMap map[string]string `json:"tags_map" gorm:"-"` // for internal usage
|
||||
@@ -73,6 +75,10 @@ type AlertCurEvent struct {
|
||||
ExtraInfoMap []map[string]string `json:"extra_info_map" gorm:"-"`
|
||||
}
|
||||
|
||||
type EventTriggerValues struct {
|
||||
ValuesWithUnit map[string]unit.FormattedValue `json:"values_with_unit"`
|
||||
}
|
||||
|
||||
func (e *AlertCurEvent) TableName() string {
|
||||
return "alert_cur_event"
|
||||
}
|
||||
|
||||
@@ -155,6 +155,7 @@ type PromQuery struct {
|
||||
PromQl string `json:"prom_ql"`
|
||||
Severity int `json:"severity"`
|
||||
RecoverConfig RecoverConfig `json:"recover_config"`
|
||||
Unit string `json:"unit"`
|
||||
}
|
||||
|
||||
type HostTrigger struct {
|
||||
|
||||
320
pkg/unit/unit_convert.go
Normal file
320
pkg/unit/unit_convert.go
Normal file
@@ -0,0 +1,320 @@
|
||||
package unit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// FormattedValue 格式化后的值的结构
|
||||
type FormattedValue struct {
|
||||
Value float64 `json:"value"`
|
||||
Unit string `json:"unit"`
|
||||
Text string `json:"text"`
|
||||
Stat float64 `json:"stat"`
|
||||
}
|
||||
|
||||
// FormatOptions 格式化选项
|
||||
type FormatOptions struct {
|
||||
Type string // "si" 或 "iec"
|
||||
Base string // "bits" 或 "bytes"
|
||||
Decimals int // 小数位数
|
||||
Postfix string // 后缀
|
||||
}
|
||||
|
||||
// 时间相关常量
|
||||
const (
|
||||
NanosecondVal = 0.000000001
|
||||
MicrosecondVal = 0.000001
|
||||
MillisecondVal = 0.001
|
||||
SecondVal = 1
|
||||
MinuteVal = 60
|
||||
HourVal = 3600
|
||||
DayVal = 86400
|
||||
WeekVal = 86400 * 7
|
||||
YearVal = 86400 * 365
|
||||
)
|
||||
|
||||
var (
|
||||
valueMap = []struct {
|
||||
Exp int
|
||||
Si string
|
||||
Iec string
|
||||
IecExp int
|
||||
}{
|
||||
{0, "", "", 1},
|
||||
{3, "k", "Ki", 10},
|
||||
{6, "M", "Mi", 20},
|
||||
{9, "G", "Gi", 30},
|
||||
{12, "T", "Ti", 40},
|
||||
{15, "P", "Pi", 50},
|
||||
{18, "E", "Ei", 60},
|
||||
{21, "Z", "Zi", 70},
|
||||
{24, "Y", "Yi", 80},
|
||||
}
|
||||
|
||||
baseUtilMap = map[string]string{
|
||||
"bits": "b",
|
||||
"bytes": "B",
|
||||
}
|
||||
)
|
||||
|
||||
// ValueFormatter 格式化入口函数
|
||||
func ValueFormatter(unit string, decimals int, value float64) FormattedValue {
|
||||
if math.IsNaN(value) {
|
||||
return FormattedValue{
|
||||
Value: 0,
|
||||
Unit: "",
|
||||
Text: "NaN",
|
||||
Stat: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// 处理时间单位
|
||||
switch unit {
|
||||
case "none":
|
||||
return formatNone(value, decimals)
|
||||
case "ns", "nanoseconds":
|
||||
return formatDuration(value, "ns", decimals)
|
||||
case "µs", "microseconds":
|
||||
return formatDuration(value, "µs", decimals)
|
||||
case "ms", "milliseconds":
|
||||
return formatDuration(value, "ms", decimals)
|
||||
case "s", "seconds":
|
||||
return formatDuration(value, "s", decimals)
|
||||
case "min", "h", "d", "w":
|
||||
return formatDuration(value, unit, decimals)
|
||||
case "percent":
|
||||
return formatPercent(value, decimals, false)
|
||||
case "percentUnit":
|
||||
return formatPercent(value, decimals, true)
|
||||
case "bytesIEC", "bytes(IEC)", "bitsIEC", "bits(IEC)":
|
||||
base := unit
|
||||
base = strings.TrimSuffix(base, "(IEC)")
|
||||
base = strings.TrimSuffix(base, "IEC")
|
||||
base = strings.TrimSuffix(base, "s")
|
||||
opts := FormatOptions{
|
||||
Type: "iec",
|
||||
Base: base,
|
||||
Decimals: decimals,
|
||||
}
|
||||
return formatBytes(value, opts)
|
||||
case "bytesSI", "bytes(SI)", "bitsSI", "bits(SI)", "default", "sishort":
|
||||
base := unit
|
||||
base = strings.TrimSuffix(base, "(SI)")
|
||||
base = strings.TrimSuffix(base, "SI")
|
||||
base = strings.TrimSuffix(base, "s")
|
||||
opts := FormatOptions{
|
||||
Type: "si",
|
||||
Base: base,
|
||||
Decimals: decimals,
|
||||
}
|
||||
return formatBytes(value, opts)
|
||||
case "bytesSecIEC":
|
||||
opts := FormatOptions{
|
||||
Type: "iec",
|
||||
Base: "bytes",
|
||||
Decimals: decimals,
|
||||
Postfix: "/s",
|
||||
}
|
||||
return formatBytes(value, opts)
|
||||
case "bitsSecIEC":
|
||||
opts := FormatOptions{
|
||||
Type: "iec",
|
||||
Base: "bits",
|
||||
Decimals: decimals,
|
||||
Postfix: "/s",
|
||||
}
|
||||
return formatBytes(value, opts)
|
||||
case "bytesSecSI":
|
||||
opts := FormatOptions{
|
||||
Type: "si",
|
||||
Base: "bytes",
|
||||
Decimals: decimals,
|
||||
Postfix: "/s",
|
||||
}
|
||||
return formatBytes(value, opts)
|
||||
case "bitsSecSI":
|
||||
opts := FormatOptions{
|
||||
Type: "si",
|
||||
Base: "bits",
|
||||
Decimals: decimals,
|
||||
Postfix: "/s",
|
||||
}
|
||||
return formatBytes(value, opts)
|
||||
case "datetimeSeconds", "datetimeMilliseconds":
|
||||
return formatDateTime(unit, value)
|
||||
default:
|
||||
return formatNone(value, decimals)
|
||||
}
|
||||
}
|
||||
|
||||
// formatDuration 处理时间单位的转换
|
||||
func formatDuration(originValue float64, unit string, decimals int) FormattedValue {
|
||||
var converted float64
|
||||
var targetUnit string
|
||||
value := originValue
|
||||
// 标准化到秒
|
||||
switch unit {
|
||||
case "ns":
|
||||
value *= NanosecondVal
|
||||
case "µs":
|
||||
value *= MicrosecondVal
|
||||
case "ms":
|
||||
value *= MillisecondVal
|
||||
case "min":
|
||||
value *= MinuteVal
|
||||
case "h":
|
||||
value *= HourVal
|
||||
case "d":
|
||||
value *= DayVal
|
||||
case "w":
|
||||
value *= WeekVal
|
||||
}
|
||||
|
||||
// 选择合适的单位
|
||||
switch {
|
||||
case value >= YearVal:
|
||||
converted = value / YearVal
|
||||
targetUnit = "y"
|
||||
case value >= WeekVal:
|
||||
converted = value / WeekVal
|
||||
targetUnit = "w"
|
||||
case value >= DayVal:
|
||||
converted = value / DayVal
|
||||
targetUnit = "d"
|
||||
case value >= HourVal:
|
||||
converted = value / HourVal
|
||||
targetUnit = "h"
|
||||
case value >= MinuteVal:
|
||||
converted = value / MinuteVal
|
||||
targetUnit = "min"
|
||||
case value >= SecondVal:
|
||||
converted = value
|
||||
targetUnit = "s"
|
||||
case value >= MillisecondVal:
|
||||
converted = value / MillisecondVal
|
||||
targetUnit = "ms"
|
||||
case value >= MicrosecondVal:
|
||||
converted = value / MicrosecondVal
|
||||
targetUnit = "µs"
|
||||
default:
|
||||
converted = value / NanosecondVal
|
||||
targetUnit = "ns"
|
||||
}
|
||||
|
||||
return FormattedValue{
|
||||
Value: roundFloat(converted, decimals),
|
||||
Unit: targetUnit,
|
||||
Text: fmt.Sprintf("%.*f %s", decimals, converted, targetUnit),
|
||||
Stat: originValue,
|
||||
}
|
||||
}
|
||||
|
||||
// formatBytes 处理字节相关的转换
|
||||
func formatBytes(value float64, opts FormatOptions) FormattedValue {
|
||||
if value == 0 {
|
||||
baseUtil := baseUtilMap[opts.Base]
|
||||
return FormattedValue{
|
||||
Value: 0,
|
||||
Unit: baseUtil + opts.Postfix,
|
||||
Text: fmt.Sprintf("0%s%s", baseUtil, opts.Postfix),
|
||||
Stat: 0,
|
||||
}
|
||||
}
|
||||
|
||||
baseUtil := baseUtilMap[opts.Base]
|
||||
threshold := 1000.0
|
||||
if opts.Type == "iec" {
|
||||
threshold = 1024.0
|
||||
}
|
||||
|
||||
if math.Abs(value) < threshold {
|
||||
return FormattedValue{
|
||||
Value: roundFloat(value, opts.Decimals),
|
||||
Unit: baseUtil + opts.Postfix,
|
||||
Text: fmt.Sprintf("%.*f%s%s", opts.Decimals, value, baseUtil, opts.Postfix),
|
||||
Stat: value,
|
||||
}
|
||||
}
|
||||
|
||||
// 计算指数
|
||||
exp := int(math.Floor(math.Log10(math.Abs(value))/3.0)) * 3
|
||||
if exp > 24 {
|
||||
exp = 24
|
||||
}
|
||||
|
||||
var unit string
|
||||
var divider float64
|
||||
|
||||
// 查找对应的单位
|
||||
for _, v := range valueMap {
|
||||
if v.Exp == exp {
|
||||
if opts.Type == "iec" {
|
||||
unit = v.Iec
|
||||
divider = math.Pow(2, float64(v.IecExp))
|
||||
} else {
|
||||
unit = v.Si
|
||||
divider = math.Pow(10, float64(v.Exp))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
newValue := value / divider
|
||||
return FormattedValue{
|
||||
Value: roundFloat(newValue, opts.Decimals),
|
||||
Unit: unit + baseUtil + opts.Postfix,
|
||||
Text: fmt.Sprintf("%.*f%s%s%s", opts.Decimals, newValue, unit, baseUtil, opts.Postfix),
|
||||
Stat: value,
|
||||
}
|
||||
}
|
||||
|
||||
// formatPercent 处理百分比格式化
|
||||
func formatPercent(value float64, decimals int, isUnit bool) FormattedValue {
|
||||
if isUnit {
|
||||
value = value * 100
|
||||
}
|
||||
return FormattedValue{
|
||||
Value: roundFloat(value, decimals),
|
||||
Unit: "%",
|
||||
Text: fmt.Sprintf("%.*f%%", decimals, value),
|
||||
Stat: value,
|
||||
}
|
||||
}
|
||||
|
||||
// formatNone 处理无单位格式化
|
||||
func formatNone(value float64, decimals int) FormattedValue {
|
||||
return FormattedValue{
|
||||
Value: value,
|
||||
Unit: "",
|
||||
Text: fmt.Sprintf("%.*f", decimals, value),
|
||||
Stat: value,
|
||||
}
|
||||
}
|
||||
|
||||
// formatDateTime 处理时间戳格式化
|
||||
func formatDateTime(uint string, value float64) FormattedValue {
|
||||
var t time.Time
|
||||
switch uint {
|
||||
case "datetimeSeconds":
|
||||
t = time.Unix(int64(value), 0)
|
||||
case "datetimeMilliseconds":
|
||||
t = time.Unix(0, int64(value)*int64(time.Millisecond))
|
||||
}
|
||||
|
||||
text := t.Format("2006-01-02 15:04:05")
|
||||
return FormattedValue{
|
||||
Value: value,
|
||||
Unit: "",
|
||||
Text: text,
|
||||
Stat: value,
|
||||
}
|
||||
}
|
||||
|
||||
// roundFloat 四舍五入到指定小数位
|
||||
func roundFloat(val float64, precision int) float64 {
|
||||
ratio := math.Pow(10, float64(precision))
|
||||
return math.Round(val*ratio) / ratio
|
||||
}
|
||||
318
pkg/unit/unit_convert_test.go
Normal file
318
pkg/unit/unit_convert_test.go
Normal file
@@ -0,0 +1,318 @@
|
||||
package unit
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValueFormatter(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
unit string
|
||||
decimals int
|
||||
value float64
|
||||
want FormattedValue
|
||||
}{
|
||||
// 字节测试
|
||||
{
|
||||
name: "IEC字节测试",
|
||||
unit: "bytes(IEC)",
|
||||
decimals: 2,
|
||||
value: 1024 * 1024,
|
||||
want: FormattedValue{Value: 1, Unit: "Mi", Text: "1.00Mi", Stat: 1024 * 1024},
|
||||
},
|
||||
{
|
||||
name: "SI字节测试",
|
||||
unit: "bytes(SI)",
|
||||
decimals: 2,
|
||||
value: 1000 * 1000,
|
||||
want: FormattedValue{Value: 1, Unit: "M", Text: "1.00M", Stat: 1000 * 1000},
|
||||
},
|
||||
// 时间单位测试
|
||||
{
|
||||
name: "毫秒转秒",
|
||||
unit: "ms",
|
||||
decimals: 2,
|
||||
value: 1500,
|
||||
want: FormattedValue{
|
||||
Value: 1.50,
|
||||
Unit: "s",
|
||||
Text: "1.50 s",
|
||||
Stat: 1500,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "秒转分钟",
|
||||
unit: "s",
|
||||
decimals: 1,
|
||||
value: 150,
|
||||
want: FormattedValue{
|
||||
Value: 2.5,
|
||||
Unit: "min",
|
||||
Text: "2.5 min",
|
||||
Stat: 150,
|
||||
},
|
||||
},
|
||||
// 百分比测试
|
||||
{
|
||||
name: "百分比",
|
||||
unit: "percent",
|
||||
decimals: 2,
|
||||
value: 0.9555,
|
||||
want: FormattedValue{
|
||||
Value: 0.96,
|
||||
Unit: "%",
|
||||
Text: "0.96%",
|
||||
Stat: 0.9555,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "百分比单位",
|
||||
unit: "percentUnit",
|
||||
decimals: 1,
|
||||
value: 0.95,
|
||||
want: FormattedValue{
|
||||
Value: 95.0,
|
||||
Unit: "%",
|
||||
Text: "95.0%",
|
||||
Stat: 95.0,
|
||||
},
|
||||
},
|
||||
// SI格式测试
|
||||
{
|
||||
name: "SI格式",
|
||||
unit: "sishort",
|
||||
decimals: 2,
|
||||
value: 1500,
|
||||
want: FormattedValue{
|
||||
Value: 1.50,
|
||||
Unit: "k",
|
||||
Text: "1.50k",
|
||||
Stat: 1500,
|
||||
},
|
||||
},
|
||||
// 时间戳测试
|
||||
{
|
||||
name: "时间戳 s",
|
||||
unit: "datetimeSeconds",
|
||||
decimals: 0,
|
||||
value: 1683518400,
|
||||
want: FormattedValue{
|
||||
Value: 1683518400,
|
||||
Unit: "",
|
||||
Text: "2023-05-08 12:00:00",
|
||||
Stat: 1683518400,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "时间戳 ms",
|
||||
unit: "datetimeMilliseconds",
|
||||
decimals: 0,
|
||||
value: 1683518400000,
|
||||
want: FormattedValue{
|
||||
Value: 1683518400000,
|
||||
Unit: "",
|
||||
Text: "2023-05-08 12:00:00",
|
||||
Stat: 1683518400000,
|
||||
},
|
||||
},
|
||||
// 补充时间单位测试
|
||||
{
|
||||
name: "纳秒测试",
|
||||
unit: "ns",
|
||||
decimals: 2,
|
||||
value: 1500,
|
||||
want: FormattedValue{
|
||||
Value: 1.50,
|
||||
Unit: "µs",
|
||||
Text: "1.50 µs",
|
||||
Stat: 1500,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "微秒测试",
|
||||
unit: "µs",
|
||||
decimals: 2,
|
||||
value: 1500,
|
||||
want: FormattedValue{
|
||||
Value: 1.50,
|
||||
Unit: "ms",
|
||||
Text: "1.50 ms",
|
||||
Stat: 1500,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "小时测试",
|
||||
unit: "h",
|
||||
decimals: 1,
|
||||
value: 2.5,
|
||||
want: FormattedValue{
|
||||
Value: 2.5,
|
||||
Unit: "h",
|
||||
Text: "2.5 h",
|
||||
Stat: 2.5,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "天数测试",
|
||||
unit: "d",
|
||||
decimals: 1,
|
||||
value: 1.5,
|
||||
want: FormattedValue{
|
||||
Value: 1.5,
|
||||
Unit: "d",
|
||||
Text: "1.5 d",
|
||||
Stat: 1.5,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "周数测试",
|
||||
unit: "w",
|
||||
decimals: 1,
|
||||
value: 1.5,
|
||||
want: FormattedValue{
|
||||
Value: 1.5,
|
||||
Unit: "w",
|
||||
Text: "1.5 w",
|
||||
Stat: 1.5,
|
||||
},
|
||||
},
|
||||
// 补充字节速率测试
|
||||
{
|
||||
name: "IEC字节每秒",
|
||||
unit: "bytesSecIEC",
|
||||
decimals: 2,
|
||||
value: 1024 * 1024,
|
||||
want: FormattedValue{
|
||||
Value: 1,
|
||||
Unit: "MiB/s",
|
||||
Text: "1.00MiB/s",
|
||||
Stat: 1024 * 1024,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "IEC比特每秒",
|
||||
unit: "bitsSecIEC",
|
||||
decimals: 2,
|
||||
value: 1024 * 1024,
|
||||
want: FormattedValue{
|
||||
Value: 1,
|
||||
Unit: "Mib/s",
|
||||
Text: "1.00Mib/s",
|
||||
Stat: 1024 * 1024,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SI字节每秒",
|
||||
unit: "bytesSecSI",
|
||||
decimals: 2,
|
||||
value: 1000 * 1000,
|
||||
want: FormattedValue{
|
||||
Value: 1,
|
||||
Unit: "MB/s",
|
||||
Text: "1.00MB/s",
|
||||
Stat: 1000 * 1000,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SI比特每秒",
|
||||
unit: "bitsSecSI",
|
||||
decimals: 2,
|
||||
value: 1000 * 1000,
|
||||
want: FormattedValue{
|
||||
Value: 1,
|
||||
Unit: "Mb/s",
|
||||
Text: "1.00Mb/s",
|
||||
Stat: 1000 * 1000,
|
||||
},
|
||||
},
|
||||
// none 类型测试
|
||||
{
|
||||
name: "无单位测试",
|
||||
unit: "none",
|
||||
decimals: 2,
|
||||
value: 1234.5678,
|
||||
want: FormattedValue{
|
||||
Value: 1234.5678,
|
||||
Unit: "",
|
||||
Text: "1234.57",
|
||||
Stat: 1234.5678,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := ValueFormatter(tt.unit, tt.decimals, tt.value)
|
||||
if !compareFormattedValues(got, tt.want) {
|
||||
t.Errorf("ValueFormatter() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
unit string
|
||||
decimals int
|
||||
value float64
|
||||
wantNil bool
|
||||
}{
|
||||
{
|
||||
name: "NaN值",
|
||||
unit: "bytes",
|
||||
decimals: 2,
|
||||
value: math.NaN(),
|
||||
wantNil: false,
|
||||
},
|
||||
{
|
||||
name: "零值",
|
||||
unit: "bytes",
|
||||
decimals: 2,
|
||||
value: 0,
|
||||
wantNil: false,
|
||||
},
|
||||
{
|
||||
name: "极小值",
|
||||
unit: "bytes",
|
||||
decimals: 2,
|
||||
value: 0.0000001,
|
||||
wantNil: false,
|
||||
},
|
||||
{
|
||||
name: "极大值",
|
||||
unit: "bytes",
|
||||
decimals: 2,
|
||||
value: 1e30,
|
||||
wantNil: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := ValueFormatter(tt.unit, tt.decimals, tt.value)
|
||||
if (got == FormattedValue{}) == !tt.wantNil {
|
||||
t.Errorf("ValueFormatter() got = %v, wantNil = %v", got, tt.wantNil)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// compareFormattedValues 比较两个FormattedValue是否相等
|
||||
func compareFormattedValues(a, b FormattedValue) bool {
|
||||
const epsilon = 0.0001
|
||||
if math.Abs(a.Value-b.Value) > epsilon {
|
||||
return false
|
||||
}
|
||||
if math.Abs(a.Stat-b.Stat) > epsilon {
|
||||
return false
|
||||
}
|
||||
if a.Unit != b.Unit {
|
||||
return false
|
||||
}
|
||||
if a.Text != b.Text {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user