mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-17 10:09:05 +02:00
167 lines
4.5 KiB
Go
167 lines
4.5 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// Copyright The Prometheus Authors
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//go:build windows
|
|
|
|
package ole
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"runtime"
|
|
"testing"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"github.com/prometheus-community/windows_exporter/internal/utils/recovery"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
type apartment struct {
|
|
threadID uint32
|
|
kind int32
|
|
qualifier int32
|
|
}
|
|
|
|
func currentApartment() (apartment, error) {
|
|
a := apartment{threadID: windows.GetCurrentThreadId()}
|
|
|
|
hr, _, _ := ole32.NewProc("CoGetApartmentType").Call(
|
|
uintptr(unsafe.Pointer(&a.kind)),
|
|
uintptr(unsafe.Pointer(&a.qualifier)),
|
|
)
|
|
|
|
return a, ResultError(hr)
|
|
}
|
|
|
|
func TestWorkerKeepsApartment(t *testing.T) {
|
|
w, err := NewWorker()
|
|
require.NoError(t, err)
|
|
|
|
first, err := Do(t.Context(), w, currentApartment)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int32(1), first.kind, "APTTYPE_MTA")
|
|
require.Equal(t, int32(0), first.qualifier, "explicitly initialized, not the implicit MTA")
|
|
|
|
for range 10 {
|
|
next, err := Do(t.Context(), w, currentApartment)
|
|
require.NoError(t, err)
|
|
require.Equal(t, first, next, "every call runs on the same initialized thread")
|
|
}
|
|
|
|
require.NoError(t, w.Close(time.Second))
|
|
require.NoError(t, w.Close(time.Second), "Close is idempotent")
|
|
|
|
_, err = Do(t.Context(), w, currentApartment)
|
|
require.ErrorIs(t, err, ErrWorkerStopped)
|
|
}
|
|
|
|
func TestWorkerRecoversPanics(t *testing.T) {
|
|
w, err := NewWorker()
|
|
require.NoError(t, err)
|
|
|
|
defer func() { require.NoError(t, w.Close(time.Second)) }()
|
|
|
|
_, err = Do(t.Context(), w, func() (int, error) { panic("boom") })
|
|
require.ErrorIs(t, err, recovery.ErrPanic)
|
|
|
|
value, err := Do(t.Context(), w, func() (int, error) { return 42, nil })
|
|
require.NoError(t, err)
|
|
require.Equal(t, 42, value, "the worker keeps running after a panic")
|
|
|
|
errTest := errors.New("test")
|
|
value, err = Do(t.Context(), w, func() (int, error) { return 7, errTest })
|
|
require.ErrorIs(t, err, errTest)
|
|
require.Equal(t, 7, value, "partial results are returned with the error")
|
|
}
|
|
|
|
// TestWorkerGoexit checks that a function calling runtime.Goexit, which
|
|
// recovery.Run does not stop, ends the worker instead of hanging Do.
|
|
func TestWorkerGoexit(t *testing.T) {
|
|
w, err := NewWorker()
|
|
require.NoError(t, err)
|
|
|
|
done := make(chan error, 1)
|
|
|
|
go func() {
|
|
_, err := Do(context.Background(), w, func() (int, error) {
|
|
runtime.Goexit()
|
|
|
|
return 0, nil
|
|
})
|
|
done <- err
|
|
}()
|
|
|
|
select {
|
|
case err := <-done:
|
|
require.ErrorIs(t, err, ErrWorkerStopped)
|
|
case <-time.After(10 * time.Second):
|
|
t.Fatal("Do did not return after the worker exited")
|
|
}
|
|
|
|
_, err = Do(context.Background(), w, func() (int, error) { return 1, nil })
|
|
require.ErrorIs(t, err, ErrWorkerStopped, "later calls do not block")
|
|
|
|
require.NoError(t, w.Close(time.Second))
|
|
}
|
|
|
|
func TestWorkerBudget(t *testing.T) {
|
|
w, err := NewWorker()
|
|
require.NoError(t, err)
|
|
|
|
release := make(chan struct{})
|
|
running := make(chan struct{})
|
|
|
|
ctx, cancel := context.WithTimeout(t.Context(), 50*time.Millisecond)
|
|
defer cancel()
|
|
|
|
_, err = Do(ctx, w, func() (struct{}, error) {
|
|
close(running)
|
|
<-release
|
|
|
|
return struct{}{}, nil
|
|
})
|
|
require.ErrorIs(t, err, ErrWorkerTimeout)
|
|
require.ErrorIs(t, err, context.DeadlineExceeded)
|
|
|
|
<-running
|
|
|
|
// The abandoned call still occupies the worker.
|
|
ctx, cancel = context.WithTimeout(t.Context(), 50*time.Millisecond)
|
|
defer cancel()
|
|
|
|
_, err = Do(ctx, w, func() (struct{}, error) { return struct{}{}, nil })
|
|
require.ErrorIs(t, err, ErrWorkerTimeout)
|
|
|
|
// A done context fails before the call is submitted.
|
|
canceled, cancelNow := context.WithCancel(t.Context())
|
|
cancelNow()
|
|
|
|
_, err = Do(canceled, w, func() (struct{}, error) {
|
|
t.Error("a call with a done context must not run")
|
|
|
|
return struct{}{}, nil
|
|
})
|
|
require.ErrorIs(t, err, ErrWorkerTimeout)
|
|
|
|
// Close waits for the running call within its bound.
|
|
require.ErrorIs(t, w.Close(50*time.Millisecond), ErrWorkerTimeout)
|
|
|
|
close(release)
|
|
|
|
require.NoError(t, w.Close(time.Second), "the worker stops once the call returns")
|
|
}
|