mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-17 10:09:05 +02:00
wmi: add generic wmi collector (#2530)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a67a64c55c
commit
88034b818e
@@ -107,6 +107,7 @@ func (instance *Instance) GetElement(elementName string) (*Element, error) {
|
||||
var (
|
||||
valueBuf [5]uint64
|
||||
valueType ValueType
|
||||
flags uint32
|
||||
)
|
||||
|
||||
r0, _, _ := syscall.SyscallN(
|
||||
@@ -115,7 +116,7 @@ func (instance *Instance) GetElement(elementName string) (*Element, error) {
|
||||
uintptr(unsafe.Pointer(elementNameUTF16)),
|
||||
uintptr(unsafe.Pointer(&valueBuf)),
|
||||
uintptr(unsafe.Pointer(&valueType)),
|
||||
0,
|
||||
uintptr(unsafe.Pointer(&flags)),
|
||||
0,
|
||||
)
|
||||
|
||||
@@ -127,6 +128,8 @@ func (instance *Instance) GetElement(elementName string) (*Element, error) {
|
||||
value: uintptr(valueBuf[0]),
|
||||
arrayLen: uint32(valueBuf[1]),
|
||||
valueType: valueType,
|
||||
flags: flags,
|
||||
raw: valueBuf,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -873,3 +873,119 @@ func Test_MI_QueryUnmarshal_Uint16Array_WrongType(t *testing.T) {
|
||||
err = application.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_MI_QueryFunc(t *testing.T) {
|
||||
application, err := mi.ApplicationInitialize()
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() { require.NoError(t, application.Close()) })
|
||||
|
||||
session, err := application.NewSession(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() { require.NoError(t, session.Close()) })
|
||||
|
||||
query, err := mi.NewQuery("SELECT LocalDateTime, NumberOfProcesses, Caption FROM Win32_OperatingSystem")
|
||||
require.NoError(t, err)
|
||||
|
||||
var calls int
|
||||
|
||||
err = session.QueryFunc(mi.NamespaceRootCIMv2, query, 5*time.Second, func(instance *mi.Instance) error {
|
||||
calls++
|
||||
|
||||
// LocalDateTime is a DATETIME timestamp carrying the local UTC offset;
|
||||
// decoding it must land close to the current wall clock.
|
||||
element, err := instance.GetElement("LocalDateTime")
|
||||
require.NoError(t, err)
|
||||
require.False(t, element.IsNull())
|
||||
|
||||
localDateTime, err := element.Float64()
|
||||
require.NoError(t, err)
|
||||
require.InDelta(t, float64(time.Now().Unix()), localDateTime, 60)
|
||||
|
||||
element, err = instance.GetElement("NumberOfProcesses")
|
||||
require.NoError(t, err)
|
||||
|
||||
processes, err := element.Float64()
|
||||
require.NoError(t, err)
|
||||
require.Positive(t, processes)
|
||||
|
||||
element, err = instance.GetElement("Caption")
|
||||
require.NoError(t, err)
|
||||
|
||||
caption, err := element.String()
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, caption, "Windows")
|
||||
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, calls)
|
||||
|
||||
// An error returned by the callback aborts the query and is passed through.
|
||||
errStop := errors.New("stop")
|
||||
|
||||
query, err = mi.NewQuery("SELECT Name FROM Win32_Process")
|
||||
require.NoError(t, err)
|
||||
|
||||
calls = 0
|
||||
err = session.QueryFunc(mi.NamespaceRootCIMv2, query, 5*time.Second, func(*mi.Instance) error {
|
||||
calls++
|
||||
|
||||
return errStop
|
||||
})
|
||||
require.ErrorIs(t, err, errStop)
|
||||
require.Equal(t, 1, calls)
|
||||
|
||||
// The cancelled query must leave the session usable.
|
||||
calls = 0
|
||||
err = session.QueryFunc(mi.NamespaceRootCIMv2, query, 5*time.Second, func(*mi.Instance) error {
|
||||
calls++
|
||||
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, calls, 1)
|
||||
|
||||
// Invalid classes surface as an error rather than an empty result.
|
||||
query, err = mi.NewQuery("SELECT Name FROM Win32_DoesNotExist")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = session.QueryFunc(mi.NamespaceRootCIMv2, query, 5*time.Second, func(*mi.Instance) error {
|
||||
return nil
|
||||
})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func Test_MI_Operation_CloseEarly(t *testing.T) {
|
||||
application, err := mi.ApplicationInitialize()
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() { require.NoError(t, application.Close()) })
|
||||
|
||||
session, err := application.NewSession(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() { require.NoError(t, session.Close()) })
|
||||
|
||||
// Closing an operation with pending results cancels it instead of reading
|
||||
// them, and must leave the session usable.
|
||||
for range 3 {
|
||||
operation, err := session.QueryInstances(mi.OperationFlagsStandardRTTI, nil, mi.NamespaceRootCIMv2, mi.QueryDialectWQL, "SELECT Name FROM Win32_Process")
|
||||
require.NoError(t, err)
|
||||
|
||||
instance, moreResults, err := operation.GetInstance()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, instance)
|
||||
require.True(t, moreResults)
|
||||
|
||||
require.NoError(t, operation.Close())
|
||||
}
|
||||
|
||||
var processes []win32Process
|
||||
|
||||
query, err := mi.NewQuery("SELECT Name FROM Win32_Process WHERE Handle = 0")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, session.Query(&processes, mi.NamespaceRootCIMv2, query, 5*time.Second))
|
||||
require.Equal(t, []win32Process{{Name: "System Idle Process"}}, processes)
|
||||
}
|
||||
|
||||
@@ -51,6 +51,11 @@ type Operation struct {
|
||||
reserved1 uint64
|
||||
reserved2 uintptr
|
||||
ft *OperationFT
|
||||
|
||||
// completed is set once GetInstance has returned the final result. It is
|
||||
// Go-only state after the MI_Operation fields, which MI never writes past;
|
||||
// every Operation is allocated in Go.
|
||||
completed bool
|
||||
}
|
||||
|
||||
// OperationFT represents the function table for Operation.
|
||||
@@ -106,14 +111,17 @@ func (o *Operation) Close() error {
|
||||
return ErrNotInitialized
|
||||
}
|
||||
|
||||
moreResults := true
|
||||
// MI_Operation_Close blocks until the final result has been delivered, so
|
||||
// pending results must be read first. Reading them would cost the full
|
||||
// remaining result set, so the operation is cancelled, which makes the
|
||||
// final result arrive right away. A fully read operation needs neither.
|
||||
if !o.completed {
|
||||
_ = o.Cancel()
|
||||
|
||||
var err error
|
||||
|
||||
for moreResults {
|
||||
_, moreResults, err = o.GetInstance()
|
||||
if err != nil {
|
||||
break
|
||||
for !o.completed {
|
||||
if _, _, err := o.GetInstance(); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,6 +198,9 @@ func (o *Operation) GetInstance() (*Instance, bool, error) {
|
||||
errorMessage = fmt.Sprintf(" (%s)", errorMessage)
|
||||
}
|
||||
|
||||
// A failed result is always the final one.
|
||||
o.completed = true
|
||||
|
||||
return nil, false, fmt.Errorf("instance result: %w%s", instanceResult, errorMessage)
|
||||
}
|
||||
|
||||
@@ -197,6 +208,10 @@ func (o *Operation) GetInstance() (*Instance, bool, error) {
|
||||
return nil, false, result
|
||||
}
|
||||
|
||||
if moreResults != True {
|
||||
o.completed = true
|
||||
}
|
||||
|
||||
return instance, moreResults == True, nil
|
||||
}
|
||||
|
||||
|
||||
+105
-19
@@ -398,29 +398,115 @@ func (s *Session) QueryUnmarshal(dst any,
|
||||
|
||||
// Query queries for a set of instances based on a query expression.
|
||||
func (s *Session) Query(dst any, namespaceName Namespace, queryExpression Query, queryTimeout time.Duration) error {
|
||||
if queryTimeout < 0 {
|
||||
return s.QueryUnmarshal(dst, OperationFlagsStandardRTTI, nil, namespaceName, QueryDialectWQL, queryExpression)
|
||||
}
|
||||
|
||||
app, err := s.GetApplication()
|
||||
operationOptions, err := s.newOperationOptions(queryTimeout)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get application: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
operationOptions, err := app.NewOperationOptions()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create operation options: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = operationOptions.Delete()
|
||||
}()
|
||||
|
||||
if queryTimeout > 0 {
|
||||
if err = operationOptions.SetTimeout(queryTimeout); err != nil {
|
||||
return fmt.Errorf("failed to set timeout: %w", err)
|
||||
}
|
||||
if operationOptions != nil {
|
||||
defer func() {
|
||||
_ = operationOptions.Delete()
|
||||
}()
|
||||
}
|
||||
|
||||
return s.QueryUnmarshal(dst, OperationFlagsStandardRTTI, operationOptions, namespaceName, QueryDialectWQL, queryExpression)
|
||||
}
|
||||
|
||||
// QueryFunc runs a WQL query and calls fn for every returned instance, in
|
||||
// order. It is meant for queries whose result shape is only known at runtime,
|
||||
// where [Session.Query] cannot unmarshal into a struct.
|
||||
//
|
||||
// The instance, and every element read from it, is owned by the operation and
|
||||
// only valid until fn returns. If fn returns an error, the query is cancelled
|
||||
// and that error is returned.
|
||||
func (s *Session) QueryFunc(namespaceName Namespace, queryExpression Query, queryTimeout time.Duration, fn func(*Instance) error) error {
|
||||
if s == nil || s.ft == nil {
|
||||
return ErrNotInitialized
|
||||
}
|
||||
|
||||
operationOptions, err := s.newOperationOptions(queryTimeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if operationOptions != nil {
|
||||
// Deferred first so the options outlive the operation that uses them.
|
||||
defer func() {
|
||||
_ = operationOptions.Delete()
|
||||
}()
|
||||
} else {
|
||||
operationOptions = s.defaultOperationOptions
|
||||
}
|
||||
|
||||
operation := &Operation{}
|
||||
|
||||
r0, _, _ := syscall.SyscallN(
|
||||
s.ft.QueryInstances,
|
||||
uintptr(unsafe.Pointer(s)),
|
||||
uintptr(OperationFlagsStandardRTTI),
|
||||
uintptr(unsafe.Pointer(operationOptions)),
|
||||
uintptr(unsafe.Pointer(namespaceName)),
|
||||
uintptr(unsafe.Pointer(QueryDialectWQL)),
|
||||
uintptr(unsafe.Pointer(queryExpression)),
|
||||
0,
|
||||
uintptr(unsafe.Pointer(operation)),
|
||||
)
|
||||
|
||||
if result := ResultError(r0); !errors.Is(result, MI_RESULT_OK) {
|
||||
return fmt.Errorf("failed to query instances: %w", result)
|
||||
}
|
||||
|
||||
// Close cancels the operation if fn aborted it early.
|
||||
defer func() {
|
||||
_ = operation.Close()
|
||||
}()
|
||||
|
||||
for {
|
||||
instance, moreResults, err := operation.GetInstance()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get instance: %w", err)
|
||||
}
|
||||
|
||||
if instance == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := fn(instance); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !moreResults {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// newOperationOptions creates operation options carrying queryTimeout. A zero
|
||||
// timeout leaves the MI default in place; a negative timeout returns nil
|
||||
// options, so the session defaults are used. The caller must delete non-nil
|
||||
// options once the operation is closed.
|
||||
func (s *Session) newOperationOptions(queryTimeout time.Duration) (*OperationOptions, error) {
|
||||
if queryTimeout < 0 {
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
|
||||
app, err := s.GetApplication()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get application: %w", err)
|
||||
}
|
||||
|
||||
operationOptions, err := app.NewOperationOptions()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create operation options: %w", err)
|
||||
}
|
||||
|
||||
if queryTimeout > 0 {
|
||||
if err = operationOptions.SetTimeout(queryTimeout); err != nil {
|
||||
_ = operationOptions.Delete()
|
||||
|
||||
return nil, fmt.Errorf("failed to set timeout: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return operationOptions, nil
|
||||
}
|
||||
|
||||
+159
-5
@@ -21,6 +21,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
@@ -65,10 +67,166 @@ const (
|
||||
ValueTypeARRAY ValueType = 16
|
||||
)
|
||||
|
||||
// flagNull is MI_FLAG_NULL. MI_Instance_GetElement sets it in the returned
|
||||
// flags when the element has no value.
|
||||
const flagNull uint32 = 0x20000000
|
||||
|
||||
type Element struct {
|
||||
value uintptr
|
||||
arrayLen uint32
|
||||
valueType ValueType
|
||||
flags uint32
|
||||
|
||||
// raw holds the complete MI_Value union. Scalars and pointers live in the
|
||||
// first word, but MI_Datetime is stored inline and spans 36 bytes.
|
||||
raw [5]uint64
|
||||
}
|
||||
|
||||
// IsNull reports whether the element has no value.
|
||||
func (e *Element) IsNull() bool {
|
||||
return e.flags&flagNull != 0
|
||||
}
|
||||
|
||||
// Float64 converts a boolean, numeric or datetime element to a float64.
|
||||
// Booleans become 0 or 1, timestamps become seconds since the Unix epoch and
|
||||
// intervals become seconds.
|
||||
func (e *Element) Float64() (float64, error) {
|
||||
switch e.valueType {
|
||||
case ValueTypeBOOLEAN:
|
||||
if e.value == 1 {
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
case ValueTypeUINT8:
|
||||
return float64(uint8(e.value)), nil
|
||||
case ValueTypeSINT8:
|
||||
return float64(int8(e.value)), nil
|
||||
case ValueTypeUINT16:
|
||||
return float64(uint16(e.value)), nil
|
||||
case ValueTypeSINT16:
|
||||
return float64(int16(e.value)), nil
|
||||
case ValueTypeUINT32:
|
||||
return float64(uint32(e.value)), nil
|
||||
case ValueTypeSINT32:
|
||||
return float64(int32(e.value)), nil
|
||||
case ValueTypeUINT64:
|
||||
return float64(uint64(e.value)), nil
|
||||
case ValueTypeSINT64:
|
||||
return float64(int64(e.value)), nil
|
||||
case ValueTypeREAL32:
|
||||
return float64(math.Float32frombits(uint32(e.value))), nil
|
||||
case ValueTypeREAL64:
|
||||
return math.Float64frombits(uint64(e.value)), nil
|
||||
case ValueTypeDATETIME:
|
||||
dt := e.datetime()
|
||||
if dt.IsTimestamp {
|
||||
return float64(dt.Timestamp.Time().UnixMicro()) / 1e6, nil
|
||||
}
|
||||
|
||||
return dt.Interval.TotalSeconds(), nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unsupported value type for numeric conversion: %d", e.valueType)
|
||||
}
|
||||
}
|
||||
|
||||
// String converts a string, boolean or integer element to its string form.
|
||||
func (e *Element) String() (string, error) {
|
||||
switch e.valueType {
|
||||
case ValueTypeSTRING:
|
||||
if e.value == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return windows.UTF16PtrToString((*uint16)(unsafe.Pointer(e.value))), nil
|
||||
case ValueTypeBOOLEAN:
|
||||
return strconv.FormatBool(e.value == 1), nil
|
||||
case ValueTypeUINT8:
|
||||
return strconv.FormatUint(uint64(uint8(e.value)), 10), nil
|
||||
case ValueTypeUINT16:
|
||||
return strconv.FormatUint(uint64(uint16(e.value)), 10), nil
|
||||
case ValueTypeUINT32:
|
||||
return strconv.FormatUint(uint64(uint32(e.value)), 10), nil
|
||||
case ValueTypeUINT64:
|
||||
return strconv.FormatUint(uint64(e.value), 10), nil
|
||||
case ValueTypeSINT8:
|
||||
return strconv.FormatInt(int64(int8(e.value)), 10), nil
|
||||
case ValueTypeSINT16:
|
||||
return strconv.FormatInt(int64(int16(e.value)), 10), nil
|
||||
case ValueTypeSINT32:
|
||||
return strconv.FormatInt(int64(int32(e.value)), 10), nil
|
||||
case ValueTypeSINT64:
|
||||
return strconv.FormatInt(int64(e.value), 10), nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported value type for string conversion: %d", e.valueType)
|
||||
}
|
||||
}
|
||||
|
||||
// datetime decodes the inline MI_Datetime: an MI_Uint32 isTimestamp followed
|
||||
// by an MI_Timestamp or an MI_Interval of eight 32-bit words.
|
||||
func (e *Element) datetime() Datetime {
|
||||
words := (*[9]uint32)(unsafe.Pointer(&e.raw))
|
||||
|
||||
if words[0] != 0 {
|
||||
return Datetime{
|
||||
IsTimestamp: true,
|
||||
Timestamp: &Timestamp{
|
||||
Year: words[1],
|
||||
Month: words[2],
|
||||
Day: words[3],
|
||||
Hour: words[4],
|
||||
Minute: words[5],
|
||||
Second: words[6],
|
||||
Microseconds: words[7],
|
||||
UTC: int32(words[8]),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return Datetime{
|
||||
Interval: &Interval{
|
||||
Days: words[1],
|
||||
Hours: words[2],
|
||||
Minutes: words[3],
|
||||
Seconds: words[4],
|
||||
Microseconds: words[5],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Time converts the timestamp to a time.Time. UTC is the offset of the
|
||||
// recorded local time from UTC, in minutes.
|
||||
func (t *Timestamp) Time() time.Time {
|
||||
return time.Date(
|
||||
int(t.Year), time.Month(t.Month), int(t.Day),
|
||||
int(t.Hour), int(t.Minute), int(t.Second), int(t.Microseconds)*int(time.Microsecond),
|
||||
time.FixedZone("", int(t.UTC)*60),
|
||||
)
|
||||
}
|
||||
|
||||
// TotalSeconds returns the length of the interval in seconds. Unlike
|
||||
// [Interval.Duration], it covers the full range of an MI_Interval, including
|
||||
// the CIM "infinite" interval of 99999999 days.
|
||||
func (i *Interval) TotalSeconds() float64 {
|
||||
return float64(i.Days)*86400 +
|
||||
float64(i.Hours)*3600 +
|
||||
float64(i.Minutes)*60 +
|
||||
float64(i.Seconds) +
|
||||
float64(i.Microseconds)/1e6
|
||||
}
|
||||
|
||||
// Duration converts the interval to a time.Duration. Intervals longer than
|
||||
// about 292 years exceed time.Duration and are capped at its maximum.
|
||||
func (i *Interval) Duration() time.Duration {
|
||||
if i.TotalSeconds() >= float64(math.MaxInt64)/float64(time.Second) {
|
||||
return time.Duration(math.MaxInt64)
|
||||
}
|
||||
|
||||
return time.Duration(i.Days)*24*time.Hour +
|
||||
time.Duration(i.Hours)*time.Hour +
|
||||
time.Duration(i.Minutes)*time.Minute +
|
||||
time.Duration(i.Seconds)*time.Second +
|
||||
time.Duration(i.Microseconds)*time.Microsecond
|
||||
}
|
||||
|
||||
func (e *Element) GetValue() (any, error) {
|
||||
@@ -98,11 +256,7 @@ func (e *Element) GetValue() (any, error) {
|
||||
case ValueTypeCHAR16:
|
||||
return uint16(e.value), nil
|
||||
case ValueTypeDATETIME:
|
||||
if e.value == 0 {
|
||||
return nil, errors.New("invalid pointer: value is nil")
|
||||
}
|
||||
|
||||
return *(*Datetime)(unsafe.Pointer(e.value)), nil
|
||||
return e.datetime(), nil
|
||||
case ValueTypeSTRING:
|
||||
if e.value == 0 {
|
||||
return nil, errors.New("invalid pointer: value is nil")
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
// 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 mi
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// newElement builds an Element the way Instance.GetElement does, from a raw
|
||||
// MI_Value buffer.
|
||||
func newElement(valueType ValueType, raw [5]uint64, flags uint32) *Element {
|
||||
return &Element{
|
||||
value: uintptr(raw[0]),
|
||||
arrayLen: uint32(raw[1]),
|
||||
valueType: valueType,
|
||||
flags: flags,
|
||||
raw: raw,
|
||||
}
|
||||
}
|
||||
|
||||
func TestElementFloat64(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
valueType ValueType
|
||||
raw uint64
|
||||
want float64
|
||||
}{
|
||||
{"bool true", ValueTypeBOOLEAN, 1, 1},
|
||||
{"bool false", ValueTypeBOOLEAN, 0, 0},
|
||||
{"uint8 ignores high bits", ValueTypeUINT8, 0xFF01, 1},
|
||||
{"sint8 negative", ValueTypeSINT8, 0xFF, -1},
|
||||
{"sint16 negative", ValueTypeSINT16, 0xFFFE, -2},
|
||||
{"uint32", ValueTypeUINT32, math.MaxUint32, math.MaxUint32},
|
||||
{"sint32 negative", ValueTypeSINT32, 0xFFFFFFFD, -3},
|
||||
{"uint64", ValueTypeUINT64, 5_000_000_000, 5_000_000_000},
|
||||
{"sint64 negative", ValueTypeSINT64, math.MaxUint64, -1},
|
||||
{"real32", ValueTypeREAL32, uint64(math.Float32bits(7.5)), 7.5},
|
||||
{"real64", ValueTypeREAL64, math.Float64bits(-0.125), -0.125},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got, err := newElement(tc.valueType, [5]uint64{tc.raw}, 0).Float64()
|
||||
require.NoError(t, err)
|
||||
require.InDelta(t, tc.want, got, 0)
|
||||
})
|
||||
}
|
||||
|
||||
_, err := newElement(ValueTypeSTRING, [5]uint64{}, 0).Float64()
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestElementFloat64Datetime(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("timestamp", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var raw [5]uint64
|
||||
|
||||
// 2026-10-07 14:30:15.25 at UTC+120 minutes is 12:30:15.25 UTC.
|
||||
words := (*[10]uint32)(unsafe.Pointer(&raw))
|
||||
*words = [10]uint32{1, 2026, 10, 7, 14, 30, 15, 250000, 120}
|
||||
|
||||
got, err := newElement(ValueTypeDATETIME, raw, 0).Float64()
|
||||
require.NoError(t, err)
|
||||
|
||||
want := time.Date(2026, 10, 7, 12, 30, 15, 250_000_000, time.UTC)
|
||||
require.InDelta(t, float64(want.UnixMilli())/1e3, got, 1e-6)
|
||||
})
|
||||
|
||||
t.Run("interval", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var raw [5]uint64
|
||||
|
||||
words := (*[10]uint32)(unsafe.Pointer(&raw))
|
||||
*words = [10]uint32{0, 1, 2, 3, 4, 500000}
|
||||
|
||||
got, err := newElement(ValueTypeDATETIME, raw, 0).Float64()
|
||||
require.NoError(t, err)
|
||||
require.InDelta(t, (26*time.Hour + 3*time.Minute + 4500*time.Millisecond).Seconds(), got, 1e-9)
|
||||
})
|
||||
|
||||
t.Run("timestamp west of UTC", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var raw [5]uint64
|
||||
|
||||
// The CIM_DATETIME 20231015123000.000000-420 is 12:30 at UTC-7,
|
||||
// which is 19:30 UTC. GitHub runners use UTC, so this sign is only
|
||||
// covered here.
|
||||
words := (*[10]uint32)(unsafe.Pointer(&raw))
|
||||
utcOffset := int32(-420)
|
||||
*words = [10]uint32{1, 2023, 10, 15, 12, 30, 0, 0, uint32(utcOffset)}
|
||||
|
||||
got, err := newElement(ValueTypeDATETIME, raw, 0).Float64()
|
||||
require.NoError(t, err)
|
||||
require.InDelta(t, float64(time.Date(2023, 10, 15, 19, 30, 0, 0, time.UTC).Unix()), got, 1e-6)
|
||||
})
|
||||
|
||||
t.Run("infinite interval", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var raw [5]uint64
|
||||
|
||||
// The CIM "infinite" interval 99999999235959.000000:000 overflows
|
||||
// time.Duration, which must not wrap around.
|
||||
words := (*[10]uint32)(unsafe.Pointer(&raw))
|
||||
*words = [10]uint32{0, 99999999, 23, 59, 59, 0}
|
||||
|
||||
element := newElement(ValueTypeDATETIME, raw, 0)
|
||||
|
||||
got, err := element.Float64()
|
||||
require.NoError(t, err)
|
||||
require.InDelta(t, 99999999*86400.0+86399, got, 1)
|
||||
|
||||
require.Equal(t, time.Duration(math.MaxInt64), element.datetime().Interval.Duration())
|
||||
})
|
||||
}
|
||||
|
||||
func TestElementString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
valueType ValueType
|
||||
raw uint64
|
||||
want string
|
||||
}{
|
||||
{"bool", ValueTypeBOOLEAN, 1, "true"},
|
||||
{"uint16", ValueTypeUINT16, 0x1_0005, "5"},
|
||||
{"uint64", ValueTypeUINT64, math.MaxUint64, "18446744073709551615"},
|
||||
{"sint32 negative", ValueTypeSINT32, 0xFFFFFFFF, "-1"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got, err := newElement(tc.valueType, [5]uint64{tc.raw}, 0).String()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
|
||||
_, err := newElement(ValueTypeREAL64, [5]uint64{}, 0).String()
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestElementIsNull(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require.True(t, newElement(ValueTypeUINT32, [5]uint64{}, flagNull).IsNull())
|
||||
require.False(t, newElement(ValueTypeUINT32, [5]uint64{}, 0).IsNull())
|
||||
}
|
||||
Reference in New Issue
Block a user