From e63bae0c8bdc674a397ad9550e254f10789ccced Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 20 Aug 2026 13:36:53 +0200 Subject: [PATCH] chore: upgrade Go to 1.27 (#30601) --- .github/actions/build-toolchain/action.yml | 2 +- .golangci.yml | 1 + Dockerfile | 2 +- charger/evsewifi_test.go | 8 +++--- charger/ghostone/identity_test.go | 12 ++++---- charger/go-e/api_test.go | 6 ++-- charger/go-e_test.go | 9 ++++-- charger/ocpp/forwarder_test.go | 8 +++--- charger/semp_test.go | 32 +++++++++++----------- charger/trydan_test.go | 13 +++++---- go.mod | 4 +-- hems/shm/shm.go | 8 ++---- meter/dsmr_test.go | 4 +-- meter/eebus.go | 7 ++--- meter/shelly/gen2.go | 14 +++++----- plugin/cached.go | 10 +++---- plugin/cached_test.go | 4 +-- plugin/http_test.go | 7 ++--- plugin/script.go | 8 +++--- plugin/sleep.go | 9 +++--- plugin/socket_test.go | 5 ++-- plugin/valid.go | 10 +++---- plugin/watchdog.go | 9 +++--- plugin/watchdog_test.go | 10 +++---- server/remote/tunnel_test.go | 13 +++++---- util/homeassistant/connection_test.go | 8 +++--- util/urlvalues/url.go | 10 ------- vehicle/bluelink/cci.go | 4 +-- vehicle/bluelink/identity.go | 6 ++-- vehicle/bluelink/identity_test.go | 12 ++++---- vehicle/jlr.go | 4 +-- vehicle/mercedes/helper.go | 3 +- vehicle/mercedes/identity.go | 2 +- vehicle/polestar/grpc.go | 4 +-- vehicle/saic/api.go | 10 +++---- vehicle/tesla/api_test.go | 4 +-- vehicle/vag/vwidentity/endpoint.go | 7 ++--- 37 files changed, 135 insertions(+), 154 deletions(-) diff --git a/.github/actions/build-toolchain/action.yml b/.github/actions/build-toolchain/action.yml index 7d0f17453..89adc042f 100644 --- a/.github/actions/build-toolchain/action.yml +++ b/.github/actions/build-toolchain/action.yml @@ -13,7 +13,7 @@ inputs: runs: using: composite steps: - - uses: actions/setup-go@v6 + - uses: actions/setup-go@v7 with: go-version-file: go.mod cache: false # managed explicitly below to keep the scoped namespaces diff --git a/.golangci.yml b/.golangci.yml index 8c1836a1e..ae69e7c0f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -23,6 +23,7 @@ linters: settings: modernize: disable: + - embedlit - stringsbuilder staticcheck: checks: diff --git a/Dockerfile b/Dockerfile index 7dafc4a83..4341548b7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ RUN make ui # STEP 2 build executable binary -FROM --platform=$BUILDPLATFORM golang:1.26.3-alpine AS builder +FROM --platform=$BUILDPLATFORM golang:1.27.0-alpine AS builder # Install git + SSL ca certificates. # Git is required for fetching the dependencies. diff --git a/charger/evsewifi_test.go b/charger/evsewifi_test.go index 03fd1cd2f..020e5b298 100644 --- a/charger/evsewifi_test.go +++ b/charger/evsewifi_test.go @@ -10,10 +10,10 @@ import ( ) func TestEvseWifi(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintln(w, `{"list":[{"useMeter":true, "alwaysActive":true}]}`) })) - defer ts.Close() + ts.Start() wb, err := NewEVSEWifiFromConfig(map[string]any{ "uri": ts.URL, @@ -46,10 +46,10 @@ func TestEvseWifi(t *testing.T) { } func TestEvseWifiEx(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintln(w, `{"list":[{"actualCurrentMA":600, "alwaysActive":true}]}`) })) - defer ts.Close() + ts.Start() wb, err := NewEVSEWifiFromConfig(map[string]any{ "uri": ts.URL, diff --git a/charger/ghostone/identity_test.go b/charger/ghostone/identity_test.go index 03f441d2e..50153a2da 100644 --- a/charger/ghostone/identity_test.go +++ b/charger/ghostone/identity_test.go @@ -16,13 +16,11 @@ import ( func TestTokenSource_ContextCancellation(t *testing.T) { // server that blocks -- simulates slow/unreachable wallbox unblock := make(chan struct{}) - srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { <-unblock })) - defer func() { - close(unblock) - srv.Close() - }() + srv.StartTLS() + defer close(unblock) log := util.NewLogger("test") @@ -40,12 +38,12 @@ func TestTokenSource_ContextCancellation(t *testing.T) { func TestTokenSource_Success(t *testing.T) { // server that returns a valid JWT-style token - srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Authorization", fmt.Sprintf("Bearer %s", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjo5OTk5OTk5OTk5fQ.signature")) w.WriteHeader(http.StatusOK) })) - defer srv.Close() + srv.StartTLS() log := util.NewLogger("test") diff --git a/charger/go-e/api_test.go b/charger/go-e/api_test.go index 3db51a3d5..ee674860d 100644 --- a/charger/go-e/api_test.go +++ b/charger/go-e/api_test.go @@ -34,7 +34,8 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func TestLocalV1(t *testing.T) { h := &handler{} - srv := httptest.NewServer(h) + srv := httptest.NewTestServer(t, h) + srv.Start() // h.expect("/api/status?filter=alw") local := NewLocal(util.NewLogger("foo"), srv.URL, 0) @@ -52,7 +53,8 @@ func TestLocalV1(t *testing.T) { func TestLocalV2(t *testing.T) { h := &handler{} - srv := httptest.NewServer(h) + srv := httptest.NewTestServer(t, h) + srv.Start() h.expect("/api/status?filter=alw") local := NewLocal(util.NewLogger("foo"), srv.URL, 0) diff --git a/charger/go-e_test.go b/charger/go-e_test.go index ed77a96ff..d74bfcf61 100644 --- a/charger/go-e_test.go +++ b/charger/go-e_test.go @@ -33,7 +33,8 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func TestGoEV1(t *testing.T) { - srv := httptest.NewServer(new(handler)) + srv := httptest.NewTestServer(t, new(handler)) + srv.Start() sponsor.Subject = "foo" @@ -56,8 +57,10 @@ func TestGoEV1(t *testing.T) { } func TestGoEV2(t *testing.T) { - srv := httptest.NewServer(new(handler)) - srv.Config.Handler.(*handler).expect("/api/status?filter=alw") + h := new(handler) + srv := httptest.NewTestServer(t, h) + srv.Start() + h.expect("/api/status?filter=alw") sponsor.Subject = "foo" diff --git a/charger/ocpp/forwarder_test.go b/charger/ocpp/forwarder_test.go index 7eded2504..00f178750 100644 --- a/charger/ocpp/forwarder_test.go +++ b/charger/ocpp/forwarder_test.go @@ -109,7 +109,7 @@ func TestForwarderUpstreamReconnect(t *testing.T) { var conns atomic.Int32 frames := make(chan []byte, 1) - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c, err := websocket.Accept(w, r, &websocket.AcceptOptions{Subprotocols: []string{"ocpp1.6"}}) if err != nil { return @@ -127,7 +127,7 @@ func TestForwarderUpstreamReconnect(t *testing.T) { } c.CloseNow() })) - defer srv.Close() + srv.Start() cleanup := forwarderTestSetup(t, id) defer cleanup() @@ -144,7 +144,7 @@ func TestForwarderDialRetry(t *testing.T) { var reqs atomic.Int32 frames := make(chan []byte, 1) - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if reqs.Add(1) <= 2 { // reject the websocket upgrade to simulate an unreachable upstream http.Error(w, "unavailable", http.StatusServiceUnavailable) @@ -162,7 +162,7 @@ func TestForwarderDialRetry(t *testing.T) { } c.CloseNow() })) - defer srv.Close() + srv.Start() cleanup := forwarderTestSetup(t, id) defer cleanup() diff --git a/charger/semp_test.go b/charger/semp_test.go index 18eaafe71..0061c5ade 100644 --- a/charger/semp_test.go +++ b/charger/semp_test.go @@ -251,8 +251,8 @@ func TestSEMPCharger(t *testing.T) { planningResponse: mockPlanningRequestResponse, infoResponse: mockDeviceInfoResponse, } - server := httptest.NewServer(handler) - defer server.Close() + server := httptest.NewTestServer(t, handler) + server.Start() ctx := t.Context() @@ -318,8 +318,8 @@ func TestSEMPChargerOff(t *testing.T) { planningResponse: mockEmptyPlanningRequestResponse, infoResponse: mockDeviceInfoResponse, } - server := httptest.NewServer(handler) - defer server.Close() + server := httptest.NewTestServer(t, handler) + server.Start() ctx := t.Context() @@ -353,8 +353,8 @@ func TestSEMPChargerDeviceNotFound(t *testing.T) { planningResponse: mockPlanningRequestResponse, infoResponse: mockDeviceInfoResponse, } - server := httptest.NewServer(handler) - defer server.Close() + server := httptest.NewTestServer(t, handler) + server.Start() // NewSEMP now calls Enabled() which will fail if device is not found _, err := NewSEMP(t.Context(), server.URL+"/semp", "F-12345678-ABCDEF123456-00", time.Second) @@ -368,8 +368,8 @@ func TestSEMPChargerReady(t *testing.T) { planningResponse: mockPlanningRequestResponse, infoResponse: mockDeviceInfoResponse, } - server := httptest.NewServer(handler) - defer server.Close() + server := httptest.NewTestServer(t, handler) + server.Start() ctx := t.Context() @@ -396,8 +396,8 @@ func TestSEMPChargerPhases1p3p(t *testing.T) { planningResponse: mockPlanningRequestResponse, infoResponse: mockDeviceInfoPhases1p3pResponse, } - server := httptest.NewServer(handler) - defer server.Close() + server := httptest.NewTestServer(t, handler) + server.Start() ctx := t.Context() @@ -440,8 +440,8 @@ func TestSEMPChargerChargedEnergy(t *testing.T) { infoResponse: mockDeviceInfoResponse, parametersResponse: mockParametersResponse, } - server := httptest.NewServer(handler) - defer server.Close() + server := httptest.NewTestServer(t, handler) + server.Start() ctx := t.Context() @@ -465,8 +465,8 @@ func TestSEMPChargerChargedEnergy(t *testing.T) { infoResponse: mockDeviceInfoResponse, // parametersResponse left empty } - server2 := httptest.NewServer(handler2) - defer server2.Close() + server2 := httptest.NewTestServer(t, handler2) + server2.Start() wb2, err := NewSEMP(t.Context(), server2.URL+"/semp", "F-12345678-ABCDEF123456-00", time.Second) require.NoError(t, err) @@ -483,8 +483,8 @@ func TestSEMPChargerAutoDetectDeviceID(t *testing.T) { planningResponse: mockPlanningRequestResponse, infoResponse: mockDeviceInfoResponse, } - server := httptest.NewServer(handler) - defer server.Close() + server := httptest.NewTestServer(t, handler) + server.Start() ctx := t.Context() diff --git a/charger/trydan_test.go b/charger/trydan_test.go index 368625006..cba28705f 100644 --- a/charger/trydan_test.go +++ b/charger/trydan_test.go @@ -20,10 +20,13 @@ func withSponsor(t *testing.T) { t.Cleanup(func() { sponsor.Subject = orig }) } -func trydanTestServerWithBody(body string) *httptest.Server { - return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +func trydanTestServerWithBody(t *testing.T, body string) *httptest.Server { + t.Helper() + srv := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, body) })) + srv.Start() + return srv } // ChargeState maps directly to api.ChargeStatus, except firmware 2.5.0 keeps it at @@ -47,8 +50,7 @@ func TestTrydanStatus(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - srv := trydanTestServerWithBody(tc.json) - defer srv.Close() + srv := trydanTestServerWithBody(t, tc.json) wb, err := NewTrydan(srv.URL, 0) if err != nil { @@ -107,8 +109,7 @@ func TestTrydanPhaseMeasurementsUnavailable(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - srv := trydanTestServerWithBody(tc.json) - defer srv.Close() + srv := trydanTestServerWithBody(t, tc.json) wb, err := NewTrydan(srv.URL, 0) if err != nil { diff --git a/go.mod b/go.mod index 044665c48..eed038174 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/evcc-io/evcc -go 1.26.3 +go 1.27.0 require ( dario.cat/mergo v1.0.2 @@ -45,7 +45,6 @@ require ( github.com/gokrazy/updater v0.0.0-20260620140544-0a84d8ab3878 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/google/go-github/v32 v32.1.0 - github.com/google/uuid v1.6.0 github.com/gorilla/handlers v1.5.2 github.com/gorilla/mux v1.8.1 github.com/gosimple/slug v1.15.0 @@ -175,6 +174,7 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/google/jsonschema-go v0.4.3 // indirect github.com/google/renameio/v2 v2.0.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/gosimple/unidecode v1.0.1 // indirect github.com/govalues/decimal v0.1.36 // indirect diff --git a/hems/shm/shm.go b/hems/shm/shm.go index d731b2ff1..7a4e5968d 100644 --- a/hems/shm/shm.go +++ b/hems/shm/shm.go @@ -8,13 +8,13 @@ import ( "net/http" "strings" "time" + "uuid" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/core/loadpoint" "github.com/evcc-io/evcc/core/site" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/machine" - "github.com/google/uuid" "github.com/gorilla/mux" "github.com/koron/go-ssdp" ) @@ -56,10 +56,7 @@ func NewFromConfig(cfg Config, hostUri string, site site.API, addr string, route return fmt.Errorf("invalid vendor id: %v. Must be 8 characters HEX string", vendorId) } - uid, err := uuid.NewUUID() - if err != nil { - return err - } + uid := uuid.New() // Only if DeviceSerial is explicitly configured: validate it and patch the // UUID node (last 6 bytes) to ensure the UDN and DeviceSerial are stable across restarts. @@ -80,6 +77,7 @@ func NewFromConfig(cfg Config, hostUri string, site site.API, addr string, route } var did []byte + var err error if cfg.DeviceId == "" { if did, err = UniqueDeviceID(); err != nil { return fmt.Errorf("creating device id: %w", err) diff --git a/meter/dsmr_test.go b/meter/dsmr_test.go index e856b4c49..4d32f08d7 100644 --- a/meter/dsmr_test.go +++ b/meter/dsmr_test.go @@ -266,7 +266,7 @@ func TestDsmrWebSocket(t *testing.T) { t.Run(tc.name, func(t *testing.T) { payload := dsmrFrame(dsmrTelegram50) - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { conn, err := websocket.Accept(w, r, nil) if err != nil { return @@ -280,7 +280,7 @@ func TestDsmrWebSocket(t *testing.T) { time.Sleep(50 * time.Millisecond) } })) - defer srv.Close() + srv.Start() uri := "ws" + strings.TrimPrefix(srv.URL, "http") diff --git a/meter/eebus.go b/meter/eebus.go index 69262af24..e15dc824a 100644 --- a/meter/eebus.go +++ b/meter/eebus.go @@ -186,7 +186,7 @@ func (c *EEBus) lastCurtailPercent() int { return c.curtailPercent } -func (c *EEBus) readValue(scenario uint, update func(entity spineapi.EntityRemoteInterface) (float64, error)) (float64, error) { +func (c *EEBus) readValue[T any](scenario uint, update func(entity spineapi.EntityRemoteInterface) (T, error)) (T, error) { c.mu.Lock() defer c.mu.Unlock() return eebusReadValue(c.mm, c.maEntity, scenario, update) @@ -205,10 +205,7 @@ func (c *EEBus) TotalEnergy() (float64, error) { } func (c *EEBus) readPhases(scenario uint, update func(entity spineapi.EntityRemoteInterface) ([]float64, error)) (float64, float64, float64, error) { - c.mu.Lock() - defer c.mu.Unlock() - - res, err := eebusReadValue(c.mm, c.maEntity, scenario, update) + res, err := c.readValue(scenario, update) if err != nil { return 0, 0, 0, err } diff --git a/meter/shelly/gen2.go b/meter/shelly/gen2.go index a34cc6822..1d18314b0 100644 --- a/meter/shelly/gen2.go +++ b/meter/shelly/gen2.go @@ -108,7 +108,7 @@ type gen2 struct { emdata func() (Gen2EMData, error) } -func apiCall[T any](c *gen2, id int, method string) func() (T, error) { +func (c *gen2) apiCall[T any](id int, method string) func() (T, error) { return func() (T, error) { var res T if err := c.execCmd(id, method, &res); err != nil { @@ -152,9 +152,9 @@ func newGen2(helper *request.Helper, uri, model string, channel int, user, passw } if c.hasMethod("PM1.GetStatus") { - c.switchstatus = util.ResettableCached(apiCall[Gen2SwitchStatus](c, channel, "PM1.GetStatus"), cache) + c.switchstatus = util.ResettableCached(c.apiCall[Gen2SwitchStatus](channel, "PM1.GetStatus"), cache) } else { - c.switchstatus = util.ResettableCached(apiCall[Gen2SwitchStatus](c, c.switchchannel, "Switch.GetStatus"), cache) + c.switchstatus = util.ResettableCached(c.apiCall[Gen2SwitchStatus](c.switchchannel, "Switch.GetStatus"), cache) } // device-side "Reverse power measurement" setting (requires restart, hence static) var cfgMethod string @@ -177,10 +177,10 @@ func newGen2(helper *request.Helper, uri, model string, channel int, user, passw c.reversed = cfg.Reverse } - c.em1status = util.Cached(apiCall[Gen2EM1Status](c, channel, "EM1.GetStatus"), cache) - c.em1data = util.Cached(apiCall[Gen2EM1Data](c, channel, "EM1Data.GetStatus"), cache) - c.emstatus = util.Cached(apiCall[Gen2EMStatus](c, channel, "EM.GetStatus"), cache) - c.emdata = util.Cached(apiCall[Gen2EMData](c, channel, "EMData.GetStatus"), cache) + c.em1status = util.Cached(c.apiCall[Gen2EM1Status](channel, "EM1.GetStatus"), cache) + c.em1data = util.Cached(c.apiCall[Gen2EM1Data](channel, "EM1Data.GetStatus"), cache) + c.emstatus = util.Cached(c.apiCall[Gen2EMStatus](channel, "EM.GetStatus"), cache) + c.emdata = util.Cached(c.apiCall[Gen2EMData](channel, "EMData.GetStatus"), cache) return c, nil } diff --git a/plugin/cached.go b/plugin/cached.go index 94a37aaad..9d8b068fa 100644 --- a/plugin/cached.go +++ b/plugin/cached.go @@ -46,7 +46,7 @@ func NewCachedFromConfig(ctx context.Context, other map[string]any) (Plugin, err }, nil } -func cachedGetter[T any](o *cachedPlugin, valuer func(ctx context.Context) (func() (T, error), error)) (func() (T, error), error) { +func (o *cachedPlugin) cachedGetter[T any](valuer func(ctx context.Context) (func() (T, error), error)) (func() (T, error), error) { value, err := valuer(o.ctx) if err != nil { return nil, fmt.Errorf("cached: %w", err) @@ -77,23 +77,23 @@ func cachedGetter[T any](o *cachedPlugin, valuer func(ctx context.Context) (func var _ StringGetter = (*cachedPlugin)(nil) func (o *cachedPlugin) StringGetter() (func() (string, error), error) { - return cachedGetter(o, o.value.StringGetter) + return o.cachedGetter(o.value.StringGetter) } var _ FloatGetter = (*cachedPlugin)(nil) func (o *cachedPlugin) FloatGetter() (func() (float64, error), error) { - return cachedGetter(o, o.value.FloatGetter) + return o.cachedGetter(o.value.FloatGetter) } var _ IntGetter = (*cachedPlugin)(nil) func (o *cachedPlugin) IntGetter() (func() (int64, error), error) { - return cachedGetter(o, o.value.IntGetter) + return o.cachedGetter(o.value.IntGetter) } var _ BoolGetter = (*cachedPlugin)(nil) func (o *cachedPlugin) BoolGetter() (func() (bool, error), error) { - return cachedGetter(o, o.value.BoolGetter) + return o.cachedGetter(o.value.BoolGetter) } diff --git a/plugin/cached_test.go b/plugin/cached_test.go index 0e27859c0..9c4c1d2b5 100644 --- a/plugin/cached_test.go +++ b/plugin/cached_test.go @@ -15,7 +15,7 @@ func TestCachedHitAndExpiry(t *testing.T) { o := &cachedPlugin{ctx: context.Background(), clock: c, cache: time.Hour} var calls int - get, err := cachedGetter(o, func(context.Context) (func() (int64, error), error) { + get, err := o.cachedGetter(func(context.Context) (func() (int64, error), error) { return func() (int64, error) { calls++ return int64(calls), nil @@ -47,7 +47,7 @@ func TestCachedDoesNotCacheErrors(t *testing.T) { o := &cachedPlugin{ctx: context.Background(), clock: c, cache: time.Hour} var calls int - get, err := cachedGetter(o, func(context.Context) (func() (int64, error), error) { + get, err := o.cachedGetter(func(context.Context) (func() (int64, error), error) { return func() (int64, error) { calls++ return 0, errors.New("boom") diff --git a/plugin/http_test.go b/plugin/http_test.go index 543a80257..05a59b748 100644 --- a/plugin/http_test.go +++ b/plugin/http_test.go @@ -60,11 +60,8 @@ type httpTestSuite struct { func (suite *httpTestSuite) SetupSuite() { suite.h = new(httpHandler) - suite.srv = httptest.NewServer(suite.h) -} - -func (suite *httpTestSuite) TearDown() { - suite.srv.Close() + suite.srv = httptest.NewTestServer(suite.T(), suite.h) + suite.srv.Start() } func (suite *httpTestSuite) TestGet() { diff --git a/plugin/script.go b/plugin/script.go index 5cb98c423..6b2a2223e 100644 --- a/plugin/script.go +++ b/plugin/script.go @@ -125,7 +125,7 @@ func (p *Script) StringGetter() (func() (string, error), error) { }, nil } -func scriptSetter[T any](p *Script, param string) (func(T) error, error) { +func (p *Script) scriptSetter[T any](param string) (func(T) error, error) { return func(val T) error { cmd, err := util.ReplaceFormatted(p.script, map[string]any{ param: val, @@ -143,19 +143,19 @@ var _ IntSetter = (*Script)(nil) // IntSetter invokes script with parameter replaced by int value func (p *Script) IntSetter(param string) (func(int64) error, error) { - return scriptSetter[int64](p, param) + return p.scriptSetter[int64](param) } var _ BoolSetter = (*Script)(nil) // BoolSetter invokes script with parameter replaced by bool value func (p *Script) BoolSetter(param string) (func(bool) error, error) { - return scriptSetter[bool](p, param) + return p.scriptSetter[bool](param) } var _ StringSetter = (*Script)(nil) // StringSetter returns a function that invokes a script with parameter by a string value func (p *Script) StringSetter(param string) (func(string) error, error) { - return scriptSetter[string](p, param) + return p.scriptSetter[string](param) } diff --git a/plugin/sleep.go b/plugin/sleep.go index dd3c14d3e..27646d0e6 100644 --- a/plugin/sleep.go +++ b/plugin/sleep.go @@ -33,8 +33,7 @@ func NewSleepFromConfig(ctx context.Context, other map[string]any) (Plugin, erro } // sleeper is the generic sleeper function for sleepPlugin -// it is currently not possible to write this as a method -func sleeper[T comparable](o *sleepPlugin) func(T) error { +func (o *sleepPlugin) sleeper[T comparable]() func(T) error { return func(val T) error { <-time.After(o.duration) @@ -45,17 +44,17 @@ func sleeper[T comparable](o *sleepPlugin) func(T) error { var _ IntSetter = (*sleepPlugin)(nil) func (o *sleepPlugin) IntSetter(param string) (func(int64) error, error) { - return sleeper[int64](o), nil + return o.sleeper[int64](), nil } var _ FloatSetter = (*sleepPlugin)(nil) func (o *sleepPlugin) FloatSetter(param string) (func(float64) error, error) { - return sleeper[float64](o), nil + return o.sleeper[float64](), nil } var _ BoolSetter = (*sleepPlugin)(nil) func (o *sleepPlugin) BoolSetter(param string) (func(bool) error, error) { - return sleeper[bool](o), nil + return o.sleeper[bool](), nil } diff --git a/plugin/socket_test.go b/plugin/socket_test.go index 06d3b69b7..06c5cbd7f 100644 --- a/plugin/socket_test.go +++ b/plugin/socket_test.go @@ -16,7 +16,7 @@ func TestSockePlugin(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), time.Second*10) defer cancel() - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c, err := websocket.Accept(w, r, nil) require.NoError(t, err) defer c.Close(websocket.StatusNormalClosure, "") @@ -36,8 +36,7 @@ func TestSockePlugin(t *testing.T) { } } })) - - defer srv.Close() + srv.Start() addr := "ws://" + srv.Listener.Addr().String() p, err := NewSocketPluginFromConfig(map[string]any{ diff --git a/plugin/valid.go b/plugin/valid.go index a739d3de5..1df0400c3 100644 --- a/plugin/valid.go +++ b/plugin/valid.go @@ -48,9 +48,7 @@ func NewValidPlugin(ctx context.Context, valid func() (bool, error), value Confi } } -var _ StringGetter = (*validPlugin)(nil) - -func validGetter[T any](o *validPlugin, valuer func(ctx context.Context) (func() (T, error), error)) (func() (T, error), error) { +func (o *validPlugin) validGetter[T any](valuer func(ctx context.Context) (func() (T, error), error)) (func() (T, error), error) { value, err := valuer(o.ctx) if err != nil { return nil, fmt.Errorf("valid: %w", err) @@ -74,17 +72,17 @@ func validGetter[T any](o *validPlugin, valuer func(ctx context.Context) (func() var _ StringGetter = (*validPlugin)(nil) func (o *validPlugin) StringGetter() (func() (string, error), error) { - return validGetter(o, o.value.StringGetter) + return o.validGetter(o.value.StringGetter) } var _ FloatGetter = (*validPlugin)(nil) func (o *validPlugin) FloatGetter() (func() (float64, error), error) { - return validGetter(o, o.value.FloatGetter) + return o.validGetter(o.value.FloatGetter) } var _ IntGetter = (*validPlugin)(nil) func (o *validPlugin) IntGetter() (func() (int64, error), error) { - return validGetter(o, o.value.IntGetter) + return o.validGetter(o.value.IntGetter) } diff --git a/plugin/watchdog.go b/plugin/watchdog.go index 3d3754e1f..6abbe6341 100644 --- a/plugin/watchdog.go +++ b/plugin/watchdog.go @@ -76,8 +76,7 @@ type deferredState[T comparable] struct { } // setter is the generic setter function for watchdogPlugin -// it is currently not possible to write this as a method -func setter[T comparable](o *watchdogPlugin, set func(T) error, reset []T) func(T) error { +func (o *watchdogPlugin) setter[T comparable](set func(T) error, reset []T) func(T) error { var state *deferredState[T] // seed with now, not zero: otherwise the first write's delay computes to 0 and skips // deferral, which is wrong for an unknown last write @@ -195,7 +194,7 @@ func (o *watchdogPlugin) IntSetter(param string) (func(int64) error, error) { } } - res := setter(o, set, reset) + res := o.setter(set, reset) if o.initial != nil { val, err := strconv.ParseInt(*o.initial, 10, 64) if err != nil { @@ -229,7 +228,7 @@ func (o *watchdogPlugin) FloatSetter(param string) (func(float64) error, error) } } - res := setter(o, set, reset) + res := o.setter(set, reset) if o.initial != nil { val, err := strconv.ParseFloat(*o.initial, 64) if err != nil { @@ -263,7 +262,7 @@ func (o *watchdogPlugin) BoolSetter(param string) (func(bool) error, error) { reset = append(reset, val) } - res := setter(o, set, reset) + res := o.setter(set, reset) if o.initial != nil { val, err := strconv.ParseBool(*o.initial) if err != nil { diff --git a/plugin/watchdog_test.go b/plugin/watchdog_test.go index abae08f3e..cec104158 100644 --- a/plugin/watchdog_test.go +++ b/plugin/watchdog_test.go @@ -22,7 +22,7 @@ func TestWatchdogSetterConcurrency(t *testing.T) { var u atomic.Uint32 - set := setter(p, func(i int) error { + set := p.setter(func(i int) error { if !u.CompareAndSwap(0, 1) { return errors.New("race") } @@ -63,7 +63,7 @@ func TestWatchdogDeferredUpdate(t *testing.T) { } var calls []int - set := setter(p, func(i int) error { + set := p.setter(func(i int) error { calls = append(calls, i) return nil }, []int{1}) // 1 is reset value @@ -108,7 +108,7 @@ func TestWatchdogCancelPendingDeferredUpdate(t *testing.T) { } var calls []int - set := setter(p, func(i int) error { + set := p.setter(func(i int) error { calls = append(calls, i) return nil }, []int{1}) // 1 is reset value @@ -154,7 +154,7 @@ func TestWatchdogDelayBackwardCompatibility(t *testing.T) { } var calls []int - set := setter(p, func(i int) error { + set := p.setter(func(i int) error { calls = append(calls, i) return nil }, []int{1}) // 1 is reset value @@ -185,7 +185,7 @@ func TestWatchdogResetStopsInflightTick(t *testing.T) { var sawReset, stale atomic.Bool - set := setter(p, func(v int) error { + set := p.setter(func(v int) error { if v == 1 { sawReset.Store(true) // hold the lock so an in-flight tick queues behind the reset diff --git a/server/remote/tunnel_test.go b/server/remote/tunnel_test.go index 3bad1eb48..a1088389a 100644 --- a/server/remote/tunnel_test.go +++ b/server/remote/tunnel_test.go @@ -39,9 +39,11 @@ func serveSession(w http.ResponseWriter, r *http.Request, sessions chan<- *yamux // session to the sessions channel. Simulates the cloud proxy. func tunnelTestServer(t *testing.T, sessions chan<- *yamux.Session) *httptest.Server { t.Helper() - return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { serveSession(w, r, sessions) })) + srv.Start() + return srv } // requireReachable opens a stream through the server session and issues an @@ -78,7 +80,6 @@ func TestTunnelReconnect(t *testing.T) { sessions := make(chan *yamux.Session, 4) srv := tunnelTestServer(t, sessions) - defer srv.Close() wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") @@ -104,11 +105,11 @@ func TestTunnelReconnect(t *testing.T) { // the proxy rejects credentials (401/403); a new token requires a restart. func TestTunnelRejectedCredentialsStops(t *testing.T) { var attempts atomic.Int32 - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { attempts.Add(1) w.WriteHeader(http.StatusUnauthorized) })) - defer srv.Close() + srv.Start() wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") @@ -138,7 +139,7 @@ func TestTunnelReconnectsAfterTransientError(t *testing.T) { var attempts atomic.Int32 sessions := make(chan *yamux.Session, 4) - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // first attempt fails transiently, later ones succeed if attempts.Add(1) == 1 { w.WriteHeader(http.StatusServiceUnavailable) @@ -146,7 +147,7 @@ func TestTunnelReconnectsAfterTransientError(t *testing.T) { } serveSession(w, r, sessions) })) - defer srv.Close() + srv.Start() wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") diff --git a/util/homeassistant/connection_test.go b/util/homeassistant/connection_test.go index 21c29b1e8..f504bc7c6 100644 --- a/util/homeassistant/connection_test.go +++ b/util/homeassistant/connection_test.go @@ -23,10 +23,10 @@ func newTestConnection(baseURL string) *Connection { // newStateConnection returns a connection serving state for any entity func newStateConnection(t *testing.T, state string) *Connection { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, `{"entity_id":"sensor.foo","state":%q}`, state) })) - t.Cleanup(srv.Close) + srv.Start() return newTestConnection(srv.URL) } @@ -116,13 +116,13 @@ func TestCallSwitchService_DomainDispatch(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { var gotPath, gotBody string - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotPath = r.URL.Path body, _ := io.ReadAll(r.Body) gotBody = string(body) w.WriteHeader(http.StatusOK) })) - defer srv.Close() + srv.Start() err := newTestConnection(srv.URL).CallSwitchService(tc.entity, tc.turnOn) diff --git a/util/urlvalues/url.go b/util/urlvalues/url.go index d96eb4256..048d4dbd1 100644 --- a/util/urlvalues/url.go +++ b/util/urlvalues/url.go @@ -4,19 +4,9 @@ package urlvalues import ( "errors" "net/url" - "slices" "strings" ) -// Copy creates a deep copy of url values -func Copy(q url.Values) url.Values { - res := make(url.Values, len(q)) - for k, v := range q { - res[k] = slices.Clone(v) - } - return res -} - // Require verifies that url contains the required non-nil values func Require(q url.Values, keys ...string) error { for _, k := range keys { diff --git a/vehicle/bluelink/cci.go b/vehicle/bluelink/cci.go index 5d31d631a..4fdc4e016 100644 --- a/vehicle/bluelink/cci.go +++ b/vehicle/bluelink/cci.go @@ -14,10 +14,10 @@ import ( "net/url" "strings" "time" + "uuid" "github.com/evcc-io/evcc/server/db/settings" "github.com/evcc-io/evcc/util/request" - "github.com/google/uuid" "golang.org/x/oauth2" ) @@ -92,7 +92,7 @@ func (v *Identity) loginCCI(password string) (*oauth2.Token, error) { // fetch RSA cert, signin, exchange the auth code for CCI and then CCS tokens func (v *Identity) loginCCIPassword(password string) (*oauth2.Token, error) { c := v.config.CCI - deviceID := uuid.NewString() + deviceID := uuid.New().String() v.log.DEBUG.Println("cci: logging in via OneApp/CCI password login") diff --git a/vehicle/bluelink/identity.go b/vehicle/bluelink/identity.go index af9ef836a..25eb245e4 100644 --- a/vehicle/bluelink/identity.go +++ b/vehicle/bluelink/identity.go @@ -9,12 +9,12 @@ import ( "strconv" "strings" "time" + "uuid" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/oauth" "github.com/evcc-io/evcc/util/request" - "github.com/google/uuid" "github.com/samber/lo" "golang.org/x/oauth2" ) @@ -79,11 +79,11 @@ func (v *Identity) getDeviceID() (string, error) { return "", err } - uuid := uuid.NewString() + id := uuid.New().String() data := map[string]any{ "pushRegId": lo.RandomString(64, []rune("0123456789ABCDEF")), "pushType": v.config.PushType, - "uuid": uuid, + "uuid": id, } headers := map[string]string{ diff --git a/vehicle/bluelink/identity_test.go b/vehicle/bluelink/identity_test.go index 2c75d76f1..3fc8b98ed 100644 --- a/vehicle/bluelink/identity_test.go +++ b/vehicle/bluelink/identity_test.go @@ -150,8 +150,8 @@ func TestLoginUsesCCIAndWiresRefreshCCI(t *testing.T) { loginSrv := httptest.NewServer(loginMux) defer loginSrv.Close() - var refreshCalls int32 - var exchangeCalls int32 + var refreshCalls atomic.Int32 + var exchangeCalls atomic.Int32 cciMux := http.NewServeMux() cciMux.HandleFunc("/domain/api/v1/auth/token", func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode(map[string]any{ @@ -165,14 +165,14 @@ func TestLoginUsesCCIAndWiresRefreshCCI(t *testing.T) { // the first (login) exchange returns a token expiring inside oauth2's // 10s buffer, so the next Token() call triggers exactly one refresh resp := map[string]any{"accessToken": "ccs-token-2", "expiresTime": time.Now().Add(time.Hour).Unix()} - if atomic.AddInt32(&exchangeCalls, 1) == 1 { + if exchangeCalls.Add(1) == 1 { resp["accessToken"] = "ccs-token-1" resp["expiresTime"] = time.Now().Add(5 * time.Second).Unix() } _ = json.NewEncoder(w).Encode(resp) }) cciMux.HandleFunc("/domain/api/v2/auth/token-refresh", func(w http.ResponseWriter, _ *http.Request) { - atomic.AddInt32(&refreshCalls, 1) + refreshCalls.Add(1) _ = json.NewEncoder(w).Encode(map[string]any{ "accessToken": "cci-access-2", "refreshToken": "cci-refresh-2", }) @@ -192,12 +192,12 @@ func TestLoginUsesCCIAndWiresRefreshCCI(t *testing.T) { token, err := identity.Token() require.NoError(t, err) assert.Equal(t, "ccs-token-2", token.AccessToken) - assert.EqualValues(t, 1, atomic.LoadInt32(&refreshCalls), "expected TokenSource to be wired to refreshCCI") + assert.EqualValues(t, 1, refreshCalls.Load(), "expected TokenSource to be wired to refreshCCI") // the now long-lived token must not trigger a further refresh _, err = identity.Token() require.NoError(t, err) - assert.EqualValues(t, 1, atomic.LoadInt32(&refreshCalls), "unexpected additional refresh") + assert.EqualValues(t, 1, refreshCalls.Load(), "unexpected additional refresh") } // TestLoginPropagatesLegacyError covers the legacy path: a failure there must diff --git a/vehicle/jlr.go b/vehicle/jlr.go index a300f2b79..bad56e758 100644 --- a/vehicle/jlr.go +++ b/vehicle/jlr.go @@ -5,12 +5,12 @@ import ( "net/http" "net/url" "time" + "uuid" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" "github.com/evcc-io/evcc/vehicle/jlr" - "github.com/google/uuid" ) // https://github.com/ardevd/jlrpy @@ -54,7 +54,7 @@ func NewJLRFromConfig(other map[string]any) (api.Vehicle, error) { log := util.NewLogger("jlr").Redact(cc.User, cc.Password, cc.VIN, cc.DeviceID) if cc.DeviceID == "" { - cc.DeviceID = uuid.NewString() + cc.DeviceID = uuid.New().String() log.WARN.Println("new device id generated, add `deviceid` to config:", cc.DeviceID) } diff --git a/vehicle/mercedes/helper.go b/vehicle/mercedes/helper.go index 7bc3ada48..22dc5a3b1 100644 --- a/vehicle/mercedes/helper.go +++ b/vehicle/mercedes/helper.go @@ -4,8 +4,7 @@ import ( "fmt" "net/http" "sync" - - "github.com/google/uuid" + "uuid" ) // Helper provides utility primitives diff --git a/vehicle/mercedes/identity.go b/vehicle/mercedes/identity.go index 1ec34dfda..a2761a911 100644 --- a/vehicle/mercedes/identity.go +++ b/vehicle/mercedes/identity.go @@ -7,13 +7,13 @@ import ( "net/url" "strings" "sync" + "uuid" "github.com/evcc-io/evcc/server/db/settings" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/oauth" "github.com/evcc-io/evcc/util/request" "github.com/evcc-io/evcc/util/transport" - "github.com/google/uuid" "golang.org/x/oauth2" ) diff --git a/vehicle/polestar/grpc.go b/vehicle/polestar/grpc.go index 70de34f8d..7504d14e6 100644 --- a/vehicle/polestar/grpc.go +++ b/vehicle/polestar/grpc.go @@ -8,12 +8,12 @@ import ( "net" "net/http" "strconv" + "uuid" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" "github.com/evcc-io/evcc/vehicle/polestar/pb" - "github.com/google/uuid" "golang.org/x/oauth2" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -119,7 +119,7 @@ func (v *GrpcAPI) Battery(ctx context.Context, vin string) (*pb.Battery, error) ctx = metadata.AppendToOutgoingContext(ctx, "vin", vin) res, err := v.client.GetLatestBattery(ctx, &pb.GetBatteryRequest{ - Id: uuid.NewString(), + Id: uuid.New().String(), Vin: vin, }) if err != nil { diff --git a/vehicle/saic/api.go b/vehicle/saic/api.go index c1a32570d..3e405b3e2 100644 --- a/vehicle/saic/api.go +++ b/vehicle/saic/api.go @@ -95,7 +95,7 @@ func (v *API) doRepeatedRequest(path string, event_id string) error { event_id) var res requests.Answer[requests.ChargeStatus] - if _, err = doRequest(v, req, &res); err == nil { + if _, err = v.doRequest(req, &res); err == nil { v.store(res.Data) } return err @@ -120,7 +120,7 @@ func (v *API) repeatRequest(path string, event_id string) { v.mu.Unlock() } -func doRequest[T any](v *API, req *http.Request, result *requests.Answer[T]) (string, error) { +func (v *API) doRequest[T any](req *http.Request, result *requests.Answer[T]) (string, error) { resp, err := v.Do(req) if err != nil { return "", err @@ -183,7 +183,7 @@ func (v *API) Wakeup(vin string) error { return err } - doRequest[any](v, req, nil) + v.doRequest[any](req, nil) return nil } @@ -214,7 +214,7 @@ func (v *API) Status(vin string) (requests.ChargeStatus, error) { "") var res requests.Answer[requests.ChargeStatus] - event_id, err := doRequest(v, req, &res) + event_id, err := v.doRequest(req, &res) if err != nil { return zero, err } @@ -234,7 +234,7 @@ func (v *API) Status(vin string) (requests.ChargeStatus, error) { event_id) // answer not yet available, keep polling in the background - if _, err = doRequest(v, req, &res); err == api.ErrMustRetry { + if _, err = v.doRequest(req, &res); err == api.ErrMustRetry { v.mu.Lock() v.state = stateRunning v.mu.Unlock() diff --git a/vehicle/tesla/api_test.go b/vehicle/tesla/api_test.go index 93924019e..dd0ce9de6 100644 --- a/vehicle/tesla/api_test.go +++ b/vehicle/tesla/api_test.go @@ -16,7 +16,7 @@ import ( func TestCommandResponse(t *testing.T) { sponsor.Subject = "any" - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Println(r.URL.Path) switch r.URL.Path { @@ -29,7 +29,7 @@ func TestCommandResponse(t *testing.T) { w.Write([]byte(`{"response": null, "error": "vehicle unavailable: vehicle is offline or asleep"}`)) } })) - defer srv.Close() + srv.Start() ts := oauth2.StaticTokenSource(new(oauth2.Token)) client, err := tesla.NewClient(t.Context(), tesla.WithTokenSource(ts)) diff --git a/vehicle/vag/vwidentity/endpoint.go b/vehicle/vag/vwidentity/endpoint.go index 9dc390e7f..c3a7fd245 100644 --- a/vehicle/vag/vwidentity/endpoint.go +++ b/vehicle/vag/vwidentity/endpoint.go @@ -9,15 +9,14 @@ import ( "net/http/cookiejar" "net/url" "strings" + "uuid" "github.com/PuerkitoBio/goquery" "github.com/coreos/go-oidc/v3/oidc" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" - "github.com/evcc-io/evcc/util/urlvalues" "github.com/evcc-io/evcc/vehicle/vag" - "github.com/google/uuid" "github.com/samber/lo" "golang.org/x/net/publicsuffix" ) @@ -42,7 +41,7 @@ func LoginWithAuthURL(log *util.Logger, uri string, q url.Values, user, password var verify func(url.Values) // add code challenge - q = urlvalues.Copy(q) + q = q.Clone() if rt := q.Get("response_type"); strings.Contains(rt, "code") { verify = vag.ChallengeAndVerifier(q) } @@ -92,7 +91,7 @@ func (v *Service) Login(uri, user, password string) (url.Values, error) { // add nonce and state query := url.Values{ "nonce": {lo.RandomString(43, lo.LettersCharset)}, - "state": {uuid.NewString()}, + "state": {uuid.New().String()}, } uri = uri + "&" + query.Encode()