Skip to content

Commit d381d36

Browse files
committed
feat: add HostData structure and enhance client configuration
- Introduced a new `HostData` structure to collect and store information about the host system and package versions. - Updated `ClientConfig` to include a `HostData` field for setting X-Client and X-Platform headers. - Modified `NewUnifiedClient` and `NewUnifiedClientWithHostData` functions to support HostData integration. - Enhanced `V2Client` to set appropriate headers using HostData. - Updated `CoreReporter` to create and log HostData during initialization, improving debugging and reporting capabilities. These changes provide better context about the environment in which the SDK is running, enhancing the API's ability to communicate relevant information to the server.
1 parent afe436d commit d381d36

File tree

7 files changed

+296
-2
lines changed

7 files changed

+296
-2
lines changed

pkg/qase-go/clients/client.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ type ClientConfig struct {
4242

4343
// UseAPIv2 determines whether to use API v2 (default: true)
4444
UseAPIv2 bool
45+
46+
// HostData contains information about the host system and package versions
47+
// Used for setting X-Client and X-Platform headers
48+
HostData *HostData
4549
}
4650

4751
// NewClient creates a new Qase client based on configuration

pkg/qase-go/clients/host_data.go

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
package clients
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"runtime"
7+
"strings"
8+
9+
"github.com/qase-tms/qase-go/pkg/qase-go/domain"
10+
"golang.org/x/mod/modfile"
11+
)
12+
13+
// HostData contains information about the host system and package versions
14+
type HostData struct {
15+
System string // OS name (e.g., "Linux", "Darwin", "Windows")
16+
MachineName string // Machine/hostname
17+
Release string // OS release version
18+
Version string // OS version
19+
Arch string // Architecture (e.g., "amd64", "arm64")
20+
Framework string // Testing framework name (e.g., "testing", "testify")
21+
FrameworkVersion string // Testing framework version
22+
Reporter string // Reporter name (e.g., "qase-go")
23+
ReporterVersion string // Reporter version
24+
Commons string // Commons/core version
25+
APIClientV1 string // API client v1 version
26+
APIClientV2 string // API client v2 version
27+
}
28+
29+
// GetHostInfo collects system information and package versions
30+
// frameworkPackage: name of the testing framework (e.g., "testing", "testify")
31+
// frameworkVersion: version of the testing framework
32+
// reporterName: name of the reporter (e.g., "qase-go")
33+
// reporterVersion: version of the reporter
34+
func GetHostInfo() *HostData {
35+
hostname, _ := os.Hostname()
36+
if hostname == "" {
37+
hostname = "unknown"
38+
}
39+
40+
// Get OS information
41+
osName := runtime.GOOS
42+
arch := runtime.GOARCH
43+
44+
// Get Go version and remove "go" prefix
45+
goVersion := runtime.Version()
46+
goVersion = strings.TrimPrefix(goVersion, "go")
47+
48+
// Try to get module versions from go.mod
49+
apiClientV1Version, apiClientV2Version := getModuleVersionsFromGoMod()
50+
51+
return &HostData{
52+
System: osName,
53+
MachineName: hostname,
54+
Release: "",
55+
Version: "",
56+
Arch: arch,
57+
Framework: "go",
58+
FrameworkVersion: goVersion,
59+
Reporter: "qase-go",
60+
ReporterVersion: domain.Version,
61+
Commons: domain.Version,
62+
APIClientV1: apiClientV1Version,
63+
APIClientV2: apiClientV2Version,
64+
}
65+
}
66+
67+
// getModuleVersionsFromGoMod reads go.mod file and extracts versions of API client modules
68+
// Returns versions for qase-api-client and qase-api-v2-client
69+
func getModuleVersionsFromGoMod() (string, string) {
70+
var apiClientV1Version, apiClientV2Version string
71+
72+
var goModPath string
73+
74+
// First, try to find go.mod relative to this package file
75+
// This is more reliable than using working directory
76+
_, filename, _, ok := runtime.Caller(1) // Get caller (GetHostInfo) location
77+
if ok {
78+
packageDir := filepath.Dir(filename)
79+
// Go up to find go.mod (pkg/qase-go/clients -> pkg/qase-go -> go.mod)
80+
for i := 0; i < 2; i++ {
81+
packageDir = filepath.Dir(packageDir)
82+
candidate := filepath.Join(packageDir, "go.mod")
83+
if _, err := os.Stat(candidate); err == nil {
84+
goModPath = candidate
85+
break
86+
}
87+
}
88+
}
89+
90+
// Fallback: try to find go.mod file starting from current directory and going up
91+
if goModPath == "" {
92+
dir, err := os.Getwd()
93+
if err == nil {
94+
for {
95+
candidate := filepath.Join(dir, "go.mod")
96+
if _, err := os.Stat(candidate); err == nil {
97+
goModPath = candidate
98+
break
99+
}
100+
101+
parent := filepath.Dir(dir)
102+
if parent == dir {
103+
// Reached root directory
104+
break
105+
}
106+
dir = parent
107+
}
108+
}
109+
}
110+
111+
if goModPath == "" {
112+
return "", ""
113+
}
114+
115+
// Read go.mod file
116+
data, err := os.ReadFile(goModPath)
117+
if err != nil {
118+
return "", ""
119+
}
120+
121+
// Parse go.mod file
122+
file, err := modfile.Parse(goModPath, data, nil)
123+
if err != nil {
124+
return "", ""
125+
}
126+
127+
// Find versions of API client modules
128+
for _, req := range file.Require {
129+
modulePath := req.Mod.Path
130+
version := req.Mod.Version
131+
132+
if strings.Contains(modulePath, "qase-api-client") && !strings.Contains(modulePath, "qase-api-v2-client") {
133+
// Remove "v" prefix if present, we'll add it later in buildXClientHeader
134+
apiClientV1Version = strings.TrimPrefix(version, "v")
135+
} else if strings.Contains(modulePath, "qase-api-v2-client") {
136+
// Remove "v" prefix if present, we'll add it later in buildXClientHeader
137+
apiClientV2Version = strings.TrimPrefix(version, "v")
138+
}
139+
}
140+
141+
return apiClientV1Version, apiClientV2Version
142+
}
143+
144+
// buildXClientHeader builds the X-Client header value from HostData
145+
// Format: reporter={reporter_name};reporter_version=v{reporter_version};framework={framework};framework_version={framework_version};client_version_v1=v{api_client_v1_version};client_version_v2=v{api_client_v2_version};core_version=v{commons_version}
146+
func buildXClientHeader(hostData *HostData) string {
147+
if hostData == nil {
148+
return ""
149+
}
150+
151+
var parts []string
152+
153+
if hostData.Reporter != "" {
154+
parts = append(parts, "reporter="+hostData.Reporter)
155+
}
156+
157+
if hostData.ReporterVersion != "" {
158+
version := hostData.ReporterVersion
159+
if !strings.HasPrefix(version, "v") {
160+
version = "v" + version
161+
}
162+
parts = append(parts, "reporter_version="+version)
163+
}
164+
165+
if hostData.Framework != "" {
166+
parts = append(parts, "framework="+hostData.Framework)
167+
}
168+
169+
if hostData.FrameworkVersion != "" {
170+
version := hostData.FrameworkVersion
171+
if !strings.HasPrefix(version, "v") {
172+
version = "v" + version
173+
}
174+
parts = append(parts, "framework_version="+version)
175+
}
176+
177+
if hostData.APIClientV1 != "" {
178+
version := hostData.APIClientV1
179+
if !strings.HasPrefix(version, "v") {
180+
version = "v" + version
181+
}
182+
parts = append(parts, "client_version_v1="+version)
183+
}
184+
185+
if hostData.APIClientV2 != "" {
186+
version := hostData.APIClientV2
187+
if !strings.HasPrefix(version, "v") {
188+
version = "v" + version
189+
}
190+
parts = append(parts, "client_version_v2="+version)
191+
}
192+
193+
if hostData.Commons != "" {
194+
version := hostData.Commons
195+
if !strings.HasPrefix(version, "v") {
196+
version = "v" + version
197+
}
198+
parts = append(parts, "core_version="+version)
199+
}
200+
201+
return strings.Join(parts, ";")
202+
}
203+
204+
// buildXPlatformHeader builds the X-Platform header value from HostData
205+
// Format: os={os_name};arch={arch};{language}={language_version};{package_manager}={package_manager_version}
206+
func buildXPlatformHeader(hostData *HostData) string {
207+
if hostData == nil {
208+
return ""
209+
}
210+
211+
var parts []string
212+
213+
if hostData.System != "" {
214+
parts = append(parts, "os="+hostData.System)
215+
}
216+
217+
if hostData.Arch != "" {
218+
parts = append(parts, "arch="+hostData.Arch)
219+
}
220+
221+
// For Go, we use "go" as the language
222+
// Go version from runtime.Version() includes "go" prefix (e.g., "go1.21.0")
223+
goVersion := runtime.Version()
224+
if goVersion != "" {
225+
parts = append(parts, "go="+goVersion)
226+
}
227+
228+
// Package manager for Go would be "go" with version, but we can't easily get it at runtime
229+
// So we'll skip it for now
230+
231+
return strings.Join(parts, ";")
232+
}

pkg/qase-go/clients/unified_client.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,26 @@ type UnifiedClient struct {
1818
}
1919

2020
// NewUnifiedClient creates a new unified client that uses v1 for runs and v2 for results
21+
// This function creates HostData internally for backward compatibility
2122
func NewUnifiedClient(cfg *config.Config) (*UnifiedClient, error) {
23+
hostData := GetHostInfo()
24+
return NewUnifiedClientWithHostData(cfg, hostData)
25+
}
26+
27+
// NewUnifiedClientWithHostData creates a new unified client with provided HostData
28+
// This allows passing HostData from CoreReporter to avoid duplicate creation
29+
func NewUnifiedClientWithHostData(cfg *config.Config, hostData *HostData) (*UnifiedClient, error) {
30+
if hostData == nil {
31+
// Fallback: create host data if not provided
32+
hostData = GetHostInfo()
33+
}
34+
2235
// Create client config from main config
2336
clientConfig := ClientConfig{
2437
BaseURL: buildAPIBaseURL(cfg.TestOps.API.Host),
2538
APIToken: cfg.TestOps.API.Token,
2639
Debug: cfg.Debug,
40+
HostData: hostData,
2741
}
2842

2943
// Create v1 client for run management

pkg/qase-go/clients/v2_client.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,19 @@ func NewV2Client(config ClientConfig) (*V2Client, error) {
4141
}
4242
}
4343

44+
// Set X-Client and X-Platform headers if HostData is provided
45+
if config.HostData != nil {
46+
xClientHeader := buildXClientHeader(config.HostData)
47+
if xClientHeader != "" {
48+
cfg.AddDefaultHeader("X-Client", xClientHeader)
49+
}
50+
51+
xPlatformHeader := buildXPlatformHeader(config.HostData)
52+
if xPlatformHeader != "" {
53+
cfg.AddDefaultHeader("X-Platform", xPlatformHeader)
54+
}
55+
}
56+
4457
client := api_v2_client.NewAPIClient(cfg)
4558

4659
return &V2Client{

pkg/qase-go/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/qase-tms/qase-go/qase-api-client v1.2.1
77
github.com/qase-tms/qase-go/qase-api-v2-client v1.1.4
88
github.com/stretchr/testify v1.9.0
9+
golang.org/x/mod v0.8.0
910
)
1011

1112
require (

pkg/qase-go/go.sum

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
22
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
33
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
4+
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
45
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
6+
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
57
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
68
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
79
github.com/qase-tms/qase-go/qase-api-client v1.2.1 h1:UHXJx8iwvsQKISOpkkMCvjpiEVQfnj6f+zPLthfLyLA=
@@ -10,8 +12,11 @@ github.com/qase-tms/qase-go/qase-api-v2-client v1.1.4 h1:Hrs9oGO/YaQGxea8GUnuXHN
1012
github.com/qase-tms/qase-go/qase-api-v2-client v1.1.4/go.mod h1:qyIUXyT9ein6Ii2+IUW3R0eXWAJzVj44II05RRMR+wg=
1113
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
1214
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
15+
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
16+
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
1317
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
1418
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
19+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
1520
gopkg.in/validator.v2 v2.0.1 h1:xF0KWyGWXm/LM2G1TrEjqOu4pa6coO9AlWSf3msVfDY=
1621
gopkg.in/validator.v2 v2.0.1/go.mod h1:lIUZBlB3Im4s/eYp39Ry/wkR02yOPhZ9IwIRBjuPuG8=
1722
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

pkg/qase-go/reporters/core.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package reporters
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
67
"sync"
78

@@ -26,6 +27,7 @@ type CoreReporter struct {
2627
reporter Reporter
2728
fallback Reporter
2829
statusMapping domain.StatusMapping
30+
hostData *clients.HostData
2931
mutex sync.RWMutex
3032
}
3133

@@ -35,8 +37,25 @@ func NewCoreReporter(cfg *config.Config) (*CoreReporter, error) {
3537
return nil, fmt.Errorf("configuration cannot be nil")
3638
}
3739

40+
// Create host data with default values
41+
// Framework: "go" (Go's built-in testing package)
42+
// Reporter: "qase-go"
43+
// Versions from domain package and go.mod (read automatically)
44+
hostData := clients.GetHostInfo()
45+
// Commons version is automatically set from domain.Version in GetHostInfo
46+
// APIClientV1 and APIClientV2 are automatically set from go.mod in GetHostInfo
47+
48+
// Log host data as JSON
49+
hostDataJSON, err := json.Marshal(hostData)
50+
if err != nil {
51+
logging.Warn("Warning: Failed to marshal host data to JSON: %v", err)
52+
} else {
53+
logging.Debug("Host data: %s", string(hostDataJSON))
54+
}
55+
3856
reporter := &CoreReporter{
39-
config: cfg,
57+
config: cfg,
58+
hostData: hostData,
4059
}
4160

4261
// Initialize status mapping
@@ -160,7 +179,8 @@ func (a *TestOpsClientAdapter) UploadResults(ctx context.Context, runID int64, r
160179
// createTestOpsClient creates a TestOps client based on configuration
161180
func (cr *CoreReporter) createTestOpsClient() (TestOpsClient, error) {
162181
// Use the existing UnifiedClient from the clients package
163-
client, err := clients.NewUnifiedClient(cr.config)
182+
// Pass hostData from CoreReporter
183+
client, err := clients.NewUnifiedClientWithHostData(cr.config, cr.hostData)
164184
if err != nil {
165185
return nil, fmt.Errorf("failed to create unified client: %w", err)
166186
}
@@ -315,3 +335,8 @@ func (cr *CoreReporter) CreateStep(action string, status domain.StepStatus) doma
315335
func (cr *CoreReporter) GetStatusMapping() domain.StatusMapping {
316336
return cr.statusMapping
317337
}
338+
339+
// GetHostData returns the host data information
340+
func (cr *CoreReporter) GetHostData() *clients.HostData {
341+
return cr.hostData
342+
}

0 commit comments

Comments
 (0)