diff --git a/internal/entrypoint/entrypoint_test.go b/internal/entrypoint/entrypoint_test.go index 0b5261f..eb98f44 100644 --- a/internal/entrypoint/entrypoint_test.go +++ b/internal/entrypoint/entrypoint_test.go @@ -11,14 +11,12 @@ import ( var ep = NewEntrypoint() -func addRoute(alias string) *route.ReveseProxyRoute { - r := &route.ReveseProxyRoute{ +func addRoute(alias string) { + routes.HTTP.Add(&route.ReveseProxyRoute{ Route: &route.Route{ Alias: alias, }, - } - routes.HTTP.Add(r) - return r + }) } func run(t *testing.T, match []string, noMatch []string) { @@ -28,10 +26,9 @@ func run(t *testing.T, match []string, noMatch []string) { for _, test := range match { t.Run(test, func(t *testing.T) { - r := addRoute(test) found, err := ep.findRouteFunc(test) expect.NoError(t, err) - expect.True(t, found == r) + expect.NotNil(t, found) }) } @@ -112,7 +109,7 @@ func TestFindRouteByDomainsExactMatch(t *testing.T) { ".sub.domain.com", }) - addRoute("app1") + addRoute("app1.foo.bar") tests := []string{ "app1.foo.bar", // exact match diff --git a/internal/metrics/systeminfo/system_info.go b/internal/metrics/systeminfo/system_info.go index 1d44c50..2a68c7d 100644 --- a/internal/metrics/systeminfo/system_info.go +++ b/internal/metrics/systeminfo/system_info.go @@ -1,7 +1,6 @@ package systeminfo import ( - "bytes" "context" "errors" "fmt" @@ -295,48 +294,46 @@ func (s *SystemInfo) collectSensorsInfo(ctx context.Context) error { } // explicitly implement MarshalJSON to avoid reflection -func (s *SystemInfo) MarshalJSONTo(buf []byte) []byte { - b := bytes.NewBuffer(buf) - - b.WriteRune('{') +func (s *SystemInfo) MarshalJSONTo(b []byte) []byte { + b = append(b, '{') // timestamp - b.WriteString(`"timestamp":`) - b.WriteString(strconv.FormatInt(s.Timestamp, 10)) + b = append(b, `"timestamp":`...) + b = strconv.AppendInt(b, s.Timestamp, 10) // cpu_average - b.WriteString(`,"cpu_average":`) + b = append(b, `,"cpu_average":`...) if s.CPUAverage != nil { - b.WriteString(strconv.FormatFloat(*s.CPUAverage, 'f', 2, 64)) + b = strconv.AppendFloat(b, *s.CPUAverage, 'f', 2, 64) } else { - b.WriteString("null") + b = append(b, "null"...) } // memory - b.WriteString(`,"memory":`) + b = append(b, `,"memory":`...) if s.Memory != nil { - b.Write(fmt.Appendf(nil, + b = fmt.Appendf(b, `{"total":%d,"available":%d,"used":%d,"used_percent":%s}`, s.Memory.Total, s.Memory.Available, s.Memory.Used, strconv.FormatFloat(s.Memory.UsedPercent, 'f', 2, 64), - )) + ) } else { - b.WriteString("null") + b = append(b, "null"...) } // disk - b.WriteString(`,"disks":`) + b = append(b, `,"disks":`...) if len(s.Disks) > 0 { - b.WriteRune('{') + b = append(b, '{') first := true for device, disk := range s.Disks { if !first { - b.WriteRune(',') + b = append(b, ',') } - b.Write(fmt.Appendf(nil, - `"%s":{"device":%q,"path":%q,"fstype":%q,"total":%d,"free":%d,"used":%d,"used_percent":%s}`, + b = fmt.Appendf(b, + `"%s":{"device":%q,"path":%q,"fstype":%q,"total":%d,"free":%d,"used":%d,"used_percent":%.2f}`, device, device, disk.Path, @@ -344,81 +341,81 @@ func (s *SystemInfo) MarshalJSONTo(buf []byte) []byte { disk.Total, disk.Free, disk.Used, - strconv.FormatFloat(float64(disk.UsedPercent), 'f', 2, 32), - )) + disk.UsedPercent, + ) first = false } - b.WriteRune('}') + b = append(b, '}') } else { - b.WriteString("null") + b = append(b, "null"...) } // disks_io - b.WriteString(`,"disks_io":`) + b = append(b, `,"disks_io":`...) if len(s.DisksIO) > 0 { - b.WriteString("{") + b = append(b, '{') first := true for name, usage := range s.DisksIO { if !first { - b.WriteRune(',') + b = append(b, ',') } - b.Write(fmt.Appendf(nil, - `"%s":{"name":%q,"read_bytes":%d,"write_bytes":%d,"read_speed":%s,"write_speed":%s,"iops":%d}`, + b = fmt.Appendf(b, + `"%s":{"name":%q,"read_bytes":%d,"write_bytes":%d,"read_speed":%.2f,"write_speed":%.2f,"iops":%d}`, name, name, usage.ReadBytes, usage.WriteBytes, - strconv.FormatFloat(usage.ReadSpeed, 'f', 2, 64), - strconv.FormatFloat(usage.WriteSpeed, 'f', 2, 64), + usage.ReadSpeed, + usage.WriteSpeed, usage.Iops, - )) + ) first = false } - b.WriteRune('}') + b = append(b, '}') } else { - b.WriteString("null") + b = append(b, "null"...) } // network - b.WriteString(`,"network":`) + b = append(b, `,"network":`...) if s.Network != nil { - b.Write(fmt.Appendf(nil, - `{"bytes_sent":%d,"bytes_recv":%d,"upload_speed":%s,"download_speed":%s}`, + b = fmt.Appendf(b, + `{"bytes_sent":%d,"bytes_recv":%d,"upload_speed":%.2f,"download_speed":%.2f}`, s.Network.BytesSent, s.Network.BytesRecv, - strconv.FormatFloat(s.Network.UploadSpeed, 'f', 2, 64), - strconv.FormatFloat(s.Network.DownloadSpeed, 'f', 2, 64), - )) + s.Network.UploadSpeed, + s.Network.DownloadSpeed, + ) } else { - b.WriteString("null") + b = append(b, "null"...) } // sensors - b.WriteString(`,"sensors":`) + b = append(b, `,"sensors":`...) if len(s.Sensors) > 0 { - b.WriteRune('{') + b = append(b, '{') first := true for _, sensor := range s.Sensors { if !first { - b.WriteRune(',') + b = append(b, ',') } - b.Write(fmt.Appendf(nil, - `%q:{"name":%q,"temperature":%s,"high":%s,"critical":%s}`, + b = fmt.Appendf(b, + `"%s":{"name":%q,"temperature":%.2f,"high":%.2f,"critical":%.2f}`, sensor.SensorKey, sensor.SensorKey, - strconv.FormatFloat(float64(sensor.Temperature), 'f', 2, 32), - strconv.FormatFloat(float64(sensor.High), 'f', 2, 32), - strconv.FormatFloat(float64(sensor.Critical), 'f', 2, 32), - )) + sensor.Temperature, + sensor.High, + sensor.Critical, + ) first = false } - b.WriteRune('}') + b = append(b, '}') } else { - b.WriteString("null") + b = append(b, "null"...) } - b.WriteRune('}') - return b.Bytes() + b = append(b, '}') + return b } func (s *Sensors) UnmarshalJSON(data []byte) error { diff --git a/internal/route/provider/docker.go b/internal/route/provider/docker.go index 2658593..0bd1b02 100755 --- a/internal/route/provider/docker.go +++ b/internal/route/provider/docker.go @@ -49,6 +49,9 @@ func (p *DockerProvider) ShortName() string { } func (p *DockerProvider) IsExplicitOnly() bool { + if p.name == "" { // tests + return false + } return p.name[len(p.name)-1] == '!' } diff --git a/internal/route/provider/docker_labels_test.go b/internal/route/provider/docker_labels_test.go index ad864b1..531b9b6 100644 --- a/internal/route/provider/docker_labels_test.go +++ b/internal/route/provider/docker_labels_test.go @@ -15,7 +15,10 @@ import ( var testDockerLabelsYAML []byte func TestParseDockerLabels(t *testing.T) { - var provider DockerProvider + provider := &DockerProvider{ + name: "test", + dockerHost: "unix:///var/run/docker.sock", + } labels := make(map[string]string) expect.NoError(t, yaml.Unmarshal(testDockerLabelsYAML, &labels)) diff --git a/internal/route/provider/docker_test.go b/internal/route/provider/docker_test.go index 929df17..63e539b 100644 --- a/internal/route/provider/docker_test.go +++ b/internal/route/provider/docker_test.go @@ -22,6 +22,7 @@ const ( func makeRoutes(cont *container.Summary, dockerHostIP ...string) route.Routes { var p DockerProvider + var host string if len(dockerHostIP) > 0 { host = "tcp://" + dockerHostIP[0] + ":2375" @@ -30,6 +31,7 @@ func makeRoutes(cont *container.Summary, dockerHostIP ...string) route.Routes { } cont.ID = "test" p.name = "test" + p.dockerHost = host routes := expect.Must(p.routesFromContainerLabels(D.FromDocker(cont, host))) for _, r := range routes { r.Finalize() @@ -67,7 +69,7 @@ func TestApplyLabel(t *testing.T) { Names: dummyNames, Labels: map[string]string{ D.LabelAliases: "a,b", - D.LabelIdleTimeout: "", + D.LabelIdleTimeout: "10s", D.LabelStopMethod: "stop", D.LabelStopSignal: "SIGTERM", D.LabelStopTimeout: "1h", @@ -109,8 +111,13 @@ func TestApplyLabel(t *testing.T) { expect.Equal(t, a.Middlewares, middlewaresExpect) expect.Equal(t, len(b.Middlewares), 0) - expect.Equal(t, a.Container.IdlewatcherConfig.IdleTimeout, 0) - expect.Equal(t, b.Container.IdlewatcherConfig.IdleTimeout, 0) + expect.NotNil(t, a.Container) + expect.NotNil(t, b.Container) + expect.NotNil(t, a.Container.IdlewatcherConfig) + expect.NotNil(t, b.Container.IdlewatcherConfig) + + expect.Equal(t, a.Container.IdlewatcherConfig.IdleTimeout, 10*time.Second) + expect.Equal(t, b.Container.IdlewatcherConfig.IdleTimeout, 10*time.Second) expect.Equal(t, a.Container.IdlewatcherConfig.StopTimeout, time.Hour) expect.Equal(t, b.Container.IdlewatcherConfig.StopTimeout, time.Hour) expect.Equal(t, a.Container.IdlewatcherConfig.StopMethod, "stop") diff --git a/internal/route/rules/do_test.go b/internal/route/rules/do_test.go index 8c8b9c5..b47b14a 100644 --- a/internal/route/rules/do_test.go +++ b/internal/route/rules/do_test.go @@ -1,12 +1,14 @@ package rules import ( + "os" "testing" expect "github.com/yusing/go-proxy/internal/utils/testing" ) func TestParseCommands(t *testing.T) { + tmpDir := t.TempDir() tests := []struct { name string input string @@ -42,7 +44,7 @@ func TestParseCommands(t *testing.T) { // serve tests { name: "serve_valid", - input: "serve /var/www", + input: "serve " + tmpDir, wantErr: nil, }, { @@ -55,6 +57,11 @@ func TestParseCommands(t *testing.T) { input: "serve / / /", wantErr: ErrInvalidArguments, }, + { + name: "serve_non_existent_path", + input: "serve " + tmpDir + "/non-existent", + wantErr: os.ErrNotExist, + }, // redirect tests { name: "redirect_valid", diff --git a/internal/utils/serialization_test.go b/internal/utils/serialization_test.go index c9ba144..0875d22 100644 --- a/internal/utils/serialization_test.go +++ b/internal/utils/serialization_test.go @@ -45,7 +45,7 @@ func TestUnmarshal(t *testing.T) { var s2 S err := MapUnmarshalValidate(testStructSerialized, &s2) expect.NoError(t, err) - expect.Values(t, s2, testStruct) + expect.Equal(t, s2, testStruct) }) } @@ -65,15 +65,15 @@ func TestUnmarshalAnonymousField(t *testing.T) { // t.Fatalf("anon %v, all %v", anon, all) err := MapUnmarshalValidate(map[string]any{"a": 1, "b": 2, "c": 3}, &s) expect.NoError(t, err) - expect.Values(t, s.A, 1) - expect.Values(t, s.B, 2) - expect.Values(t, s.C, 3) + expect.Equal(t, s.A, 1) + expect.Equal(t, s.B, 2) + expect.Equal(t, s.C, 3) err = MapUnmarshalValidate(map[string]any{"a": 1, "b": 2, "c": 3}, &s2) expect.NoError(t, err) - expect.Values(t, s2.A, 1) - expect.Values(t, s2.B, 2) - expect.Values(t, s2.C, 3) + expect.Equal(t, s2.A, 1) + expect.Equal(t, s2.B, 2) + expect.Equal(t, s2.C, 3) } func TestStringIntConvert(t *testing.T) { @@ -95,11 +95,11 @@ func TestStringIntConvert(t *testing.T) { ok, err := ConvertString("127", field) expect.True(t, ok) expect.NoError(t, err) - expect.Values(t, field.Interface(), 127) + expect.Equal(t, field.Interface(), 127) err = Convert(reflect.ValueOf(uint8(64)), field) expect.NoError(t, err) - expect.Values(t, field.Interface(), 64) + expect.Equal(t, field.Interface(), 64) }) } } @@ -125,19 +125,19 @@ func TestConvertor(t *testing.T) { m := new(testModel) expect.NoError(t, MapUnmarshalValidate(map[string]any{"Test": "123"}, m)) - expect.Values(t, m.Test.foo, 123) - expect.Values(t, m.Test.bar, "123") + expect.Equal(t, m.Test.foo, 123) + expect.Equal(t, m.Test.bar, "123") }) t.Run("int_to_string", func(t *testing.T) { m := new(testModel) expect.NoError(t, MapUnmarshalValidate(map[string]any{"Test": "123"}, m)) - expect.Values(t, m.Test.foo, 123) - expect.Values(t, m.Test.bar, "123") + expect.Equal(t, m.Test.foo, 123) + expect.Equal(t, m.Test.bar, "123") expect.NoError(t, MapUnmarshalValidate(map[string]any{"Baz": 456}, m)) - expect.Values(t, m.Baz, "456") + expect.Equal(t, m.Baz, "456") }) t.Run("invalid", func(t *testing.T) { @@ -152,21 +152,21 @@ func TestStringToSlice(t *testing.T) { convertible, err := ConvertString("a,b,c", reflect.ValueOf(&dst)) expect.True(t, convertible) expect.NoError(t, err) - expect.Values(t, dst, []string{"a", "b", "c"}) + expect.Equal(t, dst, []string{"a", "b", "c"}) }) t.Run("yaml-like", func(t *testing.T) { dst := make([]string, 0) convertible, err := ConvertString("- a\n- b\n- c", reflect.ValueOf(&dst)) expect.True(t, convertible) expect.NoError(t, err) - expect.Values(t, dst, []string{"a", "b", "c"}) + expect.Equal(t, dst, []string{"a", "b", "c"}) }) t.Run("single-line-yaml-like", func(t *testing.T) { dst := make([]string, 0) convertible, err := ConvertString("- a", reflect.ValueOf(&dst)) expect.True(t, convertible) expect.NoError(t, err) - expect.Values(t, dst, []string{"a"}) + expect.Equal(t, dst, []string{"a"}) }) } @@ -190,7 +190,7 @@ func TestStringToMap(t *testing.T) { convertible, err := ConvertString(" a: b\n c: d", reflect.ValueOf(&dst)) expect.True(t, convertible) expect.NoError(t, err) - expect.Values(t, dst, map[string]string{"a": "b", "c": "d"}) + expect.Equal(t, dst, map[string]string{"a": "b", "c": "d"}) }) } @@ -218,8 +218,8 @@ func TestStringToStruct(t *testing.T) { convertible, err := ConvertString(" A: a\n B: 123", reflect.ValueOf(&dst)) expect.True(t, convertible) expect.NoError(t, err) - expect.Values(t, dst.A, "a") - expect.Values(t, dst.B, 123) + expect.Equal(t, dst.A, "a") + expect.Equal(t, dst.B, 123) }) type T2 struct { diff --git a/internal/utils/strutils/format.go b/internal/utils/strutils/format.go index 8ed1f65..4bb8750 100644 --- a/internal/utils/strutils/format.go +++ b/internal/utils/strutils/format.go @@ -9,6 +9,13 @@ import ( "github.com/yusing/go-proxy/internal/utils/strutils/ansi" ) +// AppendDuration appends a duration to a buffer with the following format: +// - 1 ns +// - 1 ms +// - 1 seconds +// - 1 minutes and 1 seconds +// - 1 hours, 1 minutes and 1 seconds +// - 1 days, 1 hours and 1 minutes (ignore seconds if days >= 1) func AppendDuration(d time.Duration, buf []byte) []byte { if d < 0 { buf = append(buf, '-') @@ -39,23 +46,37 @@ func AppendDuration(d time.Duration, buf []byte) []byte { minutes := (totalSeconds % 3600) / 60 seconds := totalSeconds % 60 + idxPartBeg := 0 if days > 0 { buf = strconv.AppendInt(buf, days, 10) - buf = fmt.Appendf(buf, "day%s, ", Pluralize(days)) + buf = fmt.Appendf(buf, " day%s, ", Pluralize(days)) } if hours > 0 { + idxPartBeg = len(buf) - 2 buf = strconv.AppendInt(buf, hours, 10) - buf = fmt.Appendf(buf, "hour%s, ", Pluralize(hours)) + buf = fmt.Appendf(buf, " hour%s, ", Pluralize(hours)) } if minutes > 0 { + idxPartBeg = len(buf) - 2 buf = strconv.AppendInt(buf, minutes, 10) - buf = fmt.Appendf(buf, "minute%s, ", Pluralize(minutes)) + buf = fmt.Appendf(buf, " minute%s, ", Pluralize(minutes)) } if seconds > 0 && totalSeconds < 3600 { + idxPartBeg = len(buf) - 2 buf = strconv.AppendInt(buf, seconds, 10) - buf = fmt.Appendf(buf, "second%s, ", Pluralize(seconds)) + buf = fmt.Appendf(buf, " second%s, ", Pluralize(seconds)) } - return buf[:len(buf)-2] + // remove last comma and space + buf = buf[:len(buf)-2] + if idxPartBeg > 0 && idxPartBeg < len(buf) { + // replace last part ', ' with ' and ' in-place, alloc-free + // ', ' is 2 bytes, ' and ' is 5 bytes, so we need to make room for 3 more bytes + tailLen := len(buf) - (idxPartBeg + 2) + buf = append(buf, "000"...) // append 3 bytes for ' and ' + copy(buf[idxPartBeg+5:], buf[idxPartBeg+2:idxPartBeg+2+tailLen]) // shift tail right by 3 + copy(buf[idxPartBeg:], " and ") // overwrite ', ' with ' and ' + } + return buf } func FormatDuration(d time.Duration) string { diff --git a/internal/utils/strutils/format_test.go b/internal/utils/strutils/format_test.go index aeb6ee6..5fa5e6b 100644 --- a/internal/utils/strutils/format_test.go +++ b/internal/utils/strutils/format_test.go @@ -158,6 +158,16 @@ func TestFormatDuration(t *testing.T) { duration: 1*24*time.Hour + 12*time.Hour, expected: "1 day and 12 hours", }, + { + name: "days and hours and minutes", + duration: 1*24*time.Hour + 12*time.Hour + 30*time.Minute, + expected: "1 day, 12 hours and 30 minutes", + }, + { + name: "days and hours and minutes and seconds (ignore seconds)", + duration: 1*24*time.Hour + 12*time.Hour + 30*time.Minute + 15*time.Second, + expected: "1 day, 12 hours and 30 minutes", + }, } for _, tt := range tests { diff --git a/internal/utils/testing/expect.go b/internal/utils/testing/expect.go index 6f7a17d..b787d2b 100644 --- a/internal/utils/testing/expect.go +++ b/internal/utils/testing/expect.go @@ -45,7 +45,7 @@ func ErrorT[T error](t *testing.T, err error, msgAndArgs ...any) { func Equal[T any](t *testing.T, got T, want T, msgAndArgs ...any) { t.Helper() - require.Equal(t, want, got, msgAndArgs...) + require.EqualValues(t, want, got, msgAndArgs...) } func NotEqual[T any](t *testing.T, got T, want T, msgAndArgs ...any) { @@ -53,11 +53,6 @@ func NotEqual[T any](t *testing.T, got T, want T, msgAndArgs ...any) { require.NotEqual(t, want, got, msgAndArgs...) } -func Values(t *testing.T, got any, want any, msgAndArgs ...any) { - t.Helper() - require.EqualValues(t, want, got, msgAndArgs...) -} - func Contains[T any](t *testing.T, got T, wants []T, msgAndArgs ...any) { t.Helper() require.Contains(t, wants, got, msgAndArgs...)