diff --git a/internal/civisibility/constants/env.go b/internal/civisibility/constants/env.go index 17c58c2fbb..08029a84f1 100644 --- a/internal/civisibility/constants/env.go +++ b/internal/civisibility/constants/env.go @@ -46,4 +46,7 @@ const ( // CIVisibilityAutoInstrumentationProviderEnvironmentVariable indicates that the auto-instrumentation script was used. CIVisibilityAutoInstrumentationProviderEnvironmentVariable = "DD_CIVISIBILITY_AUTO_INSTRUMENTATION_PROVIDER" + + // CIVisibilityEnvironmentDataFilePath is the environment variable that holds the path to the file containing the environmental data. + CIVisibilityEnvironmentDataFilePath = "DD_TEST_OPTIMIZATION_ENV_DATA_FILE" ) diff --git a/internal/civisibility/utils/codeowners.go b/internal/civisibility/utils/codeowners.go index 0674945151..fb3883e939 100644 --- a/internal/civisibility/utils/codeowners.go +++ b/internal/civisibility/utils/codeowners.go @@ -73,22 +73,40 @@ func GetCodeOwners() *CodeOwners { filepath.Join(v, ".docs", "CODEOWNERS"), } for _, path := range paths { - if _, err := os.Stat(path); err == nil { - codeowners, err = NewCodeOwners(path) - if err == nil { - if logger.DebugEnabled() { - logger.Debug("civisibility: codeowner file '%v' was loaded successfully.", path) - } - return codeowners - } - logger.Debug("Error parsing codeowners: %s", err) + if cow, err := parseCodeOwners(path); err == nil { + codeowners = cow + return codeowners } } } + // If the codeowners file is not found, let's try a last resort by looking in the current directory (for standalone test binaries) + for _, path := range []string{"CODEOWNERS", filepath.Join(filepath.Dir(os.Args[0]), "CODEOWNERS")} { + if cow, err := parseCodeOwners(path); err == nil { + codeowners = cow + return codeowners + } + } + return nil } +// parseCodeOwners reads and parses the CODEOWNERS file located at the given filePath. +func parseCodeOwners(filePath string) (*CodeOwners, error) { + if _, err := os.Stat(filePath); err != nil { + return nil, err + } + cow, err := NewCodeOwners(filePath) + if err == nil { + if logger.DebugEnabled() { + logger.Debug("civisibility: codeowner file '%v' was loaded successfully.", filePath) + } + return cow, nil + } + logger.Debug("Error parsing codeowners: %s", err) + return nil, err +} + // NewCodeOwners creates a new instance of CodeOwners by parsing a CODEOWNERS file located at the given filePath. // It returns an error if the file cannot be read or parsed properly. func NewCodeOwners(filePath string) (*CodeOwners, error) { diff --git a/internal/civisibility/utils/environmentTags.go b/internal/civisibility/utils/environmentTags.go index a1fa281e30..76ac62aa3d 100644 --- a/internal/civisibility/utils/environmentTags.go +++ b/internal/civisibility/utils/environmentTags.go @@ -306,6 +306,9 @@ func createCITagsMap() map[string]string { } } + // Apply environmental data if is available + applyEnvironmentalDataIfRequired(localTags) + log.Debug("civisibility: workspace directory: %v", localTags[constants.CIWorkspacePath]) log.Debug("civisibility: common tags created with %v items", len(localTags)) return localTags diff --git a/internal/civisibility/utils/file_environmental_data.go b/internal/civisibility/utils/file_environmental_data.go new file mode 100644 index 0000000000..6e817296f0 --- /dev/null +++ b/internal/civisibility/utils/file_environmental_data.go @@ -0,0 +1,275 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2025 Datadog, Inc. + +package utils + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + _ "unsafe" // required for go:linkname + + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/constants" + logger "gopkg.in/DataDog/dd-trace-go.v1/internal/log" +) + +type ( + /* + { + "ci.workspace_path": "ci.workspace_path", + "git.repository_url": "git.repository_url", + "git.commit.sha": "git.commit.sha", + "git.branch": "user-supplied-branch", + "git.tag": "user-supplied-tag", + "git.commit.author.date": "usersupplied-authordate", + "git.commit.author.name": "usersupplied-authorname", + "git.commit.author.email": "usersupplied-authoremail", + "git.commit.committer.date": "usersupplied-comitterdate", + "git.commit.committer.name": "usersupplied-comittername", + "git.commit.committer.email": "usersupplied-comitteremail", + "git.commit.message": "usersupplied-message", + "ci.provider.name": "", + "ci.pipeline.id": "", + "ci.pipeline.url": "", + "ci.pipeline.name": "", + "ci.pipeline.number": "", + "ci.stage.name": "", + "ci.job.name": "", + "ci.job.url": "", + "ci.node.name": "", + "ci.node.labels": "", + "_dd.ci.env_vars": "" + } + */ + + // fileEnvironmentalData represents the environmental data for the complete test session. + fileEnvironmentalData struct { + WorkspacePath string `json:"ci.workspace_path,omitempty"` + RepositoryURL string `json:"git.repository_url,omitempty"` + CommitSHA string `json:"git.commit.sha,omitempty"` + Branch string `json:"git.branch,omitempty"` + Tag string `json:"git.tag,omitempty"` + CommitAuthorDate string `json:"git.commit.author.date,omitempty"` + CommitAuthorName string `json:"git.commit.author.name,omitempty"` + CommitAuthorEmail string `json:"git.commit.author.email,omitempty"` + CommitCommitterDate string `json:"git.commit.committer.date,omitempty"` + CommitCommitterName string `json:"git.commit.committer.name,omitempty"` + CommitCommitterEmail string `json:"git.commit.committer.email,omitempty"` + CommitMessage string `json:"git.commit.message,omitempty"` + CIProviderName string `json:"ci.provider.name,omitempty"` + CIPipelineID string `json:"ci.pipeline.id,omitempty"` + CIPipelineURL string `json:"ci.pipeline.url,omitempty"` + CIPipelineName string `json:"ci.pipeline.name,omitempty"` + CIPipelineNumber string `json:"ci.pipeline.number,omitempty"` + CIStageName string `json:"ci.stage.name,omitempty"` + CIJobName string `json:"ci.job.name,omitempty"` + CIJobURL string `json:"ci.job.url,omitempty"` + CINodeName string `json:"ci.node.name,omitempty"` + CINodeLabels string `json:"ci.node.labels,omitempty"` + DDCIEnvVars string `json:"_dd.ci.env_vars,omitempty"` + } +) + +// getEnvironmentalData reads the environmental data from the file. +// +//go:linkname getEnvironmentalData +func getEnvironmentalData() *fileEnvironmentalData { + envDataFileName := getEnvDataFileName() + if _, err := os.Stat(envDataFileName); os.IsNotExist(err) { + logger.Debug("civisibility: reading environmental data from %s not found.", envDataFileName) + return nil + } + file, err := os.Open(envDataFileName) + if err != nil { + logger.Error("civisibility: error reading environmental data from %s: %v", envDataFileName, err) + return nil + } + defer file.Close() + var envData fileEnvironmentalData + if err := json.NewDecoder(file).Decode(&envData); err != nil { + logger.Error("civisibility: error decoding environmental data from %s: %v", envDataFileName, err) + return nil + } + logger.Debug("civisibility: loaded environmental data from %s", envDataFileName) + return &envData +} + +// getEnvDataFileName returns the environmental data file name. +// +//go:linkname getEnvDataFileName +func getEnvDataFileName() string { + envDataFileName := strings.TrimSpace(os.Getenv(constants.CIVisibilityEnvironmentDataFilePath)) + if envDataFileName != "" { + return envDataFileName + } + cmd := filepath.Base(os.Args[0]) + cmdWithoutExt := strings.TrimSuffix(cmd, filepath.Ext(cmd)) + folder := filepath.Dir(os.Args[0]) + return filepath.Join(folder, cmdWithoutExt+".env.json") +} + +// applyEnvironmentalDataIfRequired applies the environmental data to the given tags if required. +// +//go:linkname applyEnvironmentalDataIfRequired +func applyEnvironmentalDataIfRequired(tags map[string]string) { + if tags == nil { + return + } + envData := getEnvironmentalData() + if envData == nil { + logger.Debug("civisibility: no environmental data found") + return + } + + logger.Debug("civisibility: applying environmental data") + + if envData.WorkspacePath != "" && tags[constants.CIWorkspacePath] == "" { + tags[constants.CIWorkspacePath] = envData.WorkspacePath + } + + if envData.RepositoryURL != "" && tags[constants.GitRepositoryURL] == "" { + tags[constants.GitRepositoryURL] = envData.RepositoryURL + } + + if envData.CommitSHA != "" && tags[constants.GitCommitSHA] == "" { + tags[constants.GitCommitSHA] = envData.CommitSHA + } + + if envData.Branch != "" && tags[constants.GitBranch] == "" { + tags[constants.GitBranch] = envData.Branch + } + + if envData.Tag != "" && tags[constants.GitTag] == "" { + tags[constants.GitTag] = envData.Tag + } + + if envData.CommitAuthorDate != "" && tags[constants.GitCommitAuthorDate] == "" { + tags[constants.GitCommitAuthorDate] = envData.CommitAuthorDate + } + + if envData.CommitAuthorName != "" && tags[constants.GitCommitAuthorName] == "" { + tags[constants.GitCommitAuthorName] = envData.CommitAuthorName + } + + if envData.CommitAuthorEmail != "" && tags[constants.GitCommitAuthorEmail] == "" { + tags[constants.GitCommitAuthorEmail] = envData.CommitAuthorEmail + } + + if envData.CommitCommitterDate != "" && tags[constants.GitCommitCommitterDate] == "" { + tags[constants.GitCommitCommitterDate] = envData.CommitCommitterDate + } + + if envData.CommitCommitterName != "" && tags[constants.GitCommitCommitterName] == "" { + tags[constants.GitCommitCommitterName] = envData.CommitCommitterName + } + + if envData.CommitCommitterEmail != "" && tags[constants.GitCommitCommitterEmail] == "" { + tags[constants.GitCommitCommitterEmail] = envData.CommitCommitterEmail + } + + if envData.CommitMessage != "" && tags[constants.GitCommitMessage] == "" { + tags[constants.GitCommitMessage] = envData.CommitMessage + } + + if envData.CIProviderName != "" && tags[constants.CIProviderName] == "" { + tags[constants.CIProviderName] = envData.CIProviderName + } + + if envData.CIPipelineID != "" && tags[constants.CIPipelineID] == "" { + tags[constants.CIPipelineID] = envData.CIPipelineID + } + + if envData.CIPipelineURL != "" && tags[constants.CIPipelineURL] == "" { + tags[constants.CIPipelineURL] = envData.CIPipelineURL + } + + if envData.CIPipelineName != "" && tags[constants.CIPipelineName] == "" { + tags[constants.CIPipelineName] = envData.CIPipelineName + } + + if envData.CIPipelineNumber != "" && tags[constants.CIPipelineNumber] == "" { + tags[constants.CIPipelineNumber] = envData.CIPipelineNumber + } + + if envData.CIStageName != "" && tags[constants.CIStageName] == "" { + tags[constants.CIStageName] = envData.CIStageName + } + + if envData.CIJobName != "" && tags[constants.CIJobName] == "" { + tags[constants.CIJobName] = envData.CIJobName + } + + if envData.CIJobURL != "" && tags[constants.CIJobURL] == "" { + tags[constants.CIJobURL] = envData.CIJobURL + } + + if envData.CINodeName != "" && tags[constants.CINodeName] == "" { + tags[constants.CINodeName] = envData.CINodeName + } + + if envData.CINodeLabels != "" && tags[constants.CINodeLabels] == "" { + tags[constants.CINodeLabels] = envData.CINodeLabels + } + + if envData.DDCIEnvVars != "" && tags[constants.CIEnvVars] == "" { + tags[constants.CIEnvVars] = envData.DDCIEnvVars + } +} + +// createEnvironmentalDataFromTags creates a fileEnvironmentalData object from the given tags. +// +//go:linkname createEnvironmentalDataFromTags +func createEnvironmentalDataFromTags(tags map[string]string) *fileEnvironmentalData { + if tags == nil { + return nil + } + + return &fileEnvironmentalData{ + WorkspacePath: tags[constants.CIWorkspacePath], + RepositoryURL: tags[constants.GitRepositoryURL], + CommitSHA: tags[constants.GitCommitSHA], + Branch: tags[constants.GitBranch], + Tag: tags[constants.GitTag], + CommitAuthorDate: tags[constants.GitCommitAuthorDate], + CommitAuthorName: tags[constants.GitCommitAuthorName], + CommitAuthorEmail: tags[constants.GitCommitAuthorEmail], + CommitCommitterDate: tags[constants.GitCommitCommitterDate], + CommitCommitterName: tags[constants.GitCommitCommitterName], + CommitCommitterEmail: tags[constants.GitCommitCommitterEmail], + CommitMessage: tags[constants.GitCommitMessage], + CIProviderName: tags[constants.CIProviderName], + CIPipelineID: tags[constants.CIPipelineID], + CIPipelineURL: tags[constants.CIPipelineURL], + CIPipelineName: tags[constants.CIPipelineName], + CIPipelineNumber: tags[constants.CIPipelineNumber], + CIStageName: tags[constants.CIStageName], + CIJobName: tags[constants.CIJobName], + CIJobURL: tags[constants.CIJobURL], + CINodeName: tags[constants.CINodeName], + CINodeLabels: tags[constants.CINodeLabels], + DDCIEnvVars: tags[constants.CIEnvVars], + } +} + +// writeEnvironmentalDataToFile writes the environmental data to a file. +// +//go:linkname writeEnvironmentalDataToFile +func writeEnvironmentalDataToFile(filePath string, tags map[string]string) error { + envData := createEnvironmentalDataFromTags(tags) + if envData == nil { + return nil + } + + file, err := os.Create(filePath) + if err != nil { + return err + } + defer file.Close() + + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + return encoder.Encode(envData) +} diff --git a/internal/civisibility/utils/file_environmental_data_test.go b/internal/civisibility/utils/file_environmental_data_test.go new file mode 100644 index 0000000000..00ad68228c --- /dev/null +++ b/internal/civisibility/utils/file_environmental_data_test.go @@ -0,0 +1,550 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2025 Datadog, Inc. + +package utils + +import ( + "encoding/json" + "os" + "path/filepath" + "reflect" + "testing" + + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/constants" +) + +// --------------------- Tests for getEnvironmentalData ------------------------- + +// TestGetEnvironmentalData_NoFile verifies that when the expected environmental +// data file does not exist, getEnvironmentalData returns nil. +func TestGetEnvironmentalData_NoFile(t *testing.T) { + t.Setenv(constants.CIVisibilityEnvironmentDataFilePath, "") + + origArg := os.Args[0] + defer func() { os.Args[0] = origArg }() + + tempDir, err := os.MkdirTemp("", "envtest") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + + // Set os.Args[0] so that fallback filename is computed. + binaryPath := filepath.Join(tempDir, "testbinary") + os.Args[0] = binaryPath + + // Since no .env.json file exists, expect nil. + if result := getEnvironmentalData(); result != nil { + t.Errorf("Expected nil when environmental file does not exist, got: %+v", result) + } +} + +// TestGetEnvironmentalData_InvalidJSON creates an env file with invalid JSON and +// verifies that getEnvironmentalData returns nil. +func TestGetEnvironmentalData_InvalidJSON(t *testing.T) { + t.Setenv(constants.CIVisibilityEnvironmentDataFilePath, "") + + origArg := os.Args[0] + defer func() { os.Args[0] = origArg }() + + tempDir, err := os.MkdirTemp("", "envtest") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + + binaryPath := filepath.Join(tempDir, "testbinary") + os.Args[0] = binaryPath + + // Write a file with invalid JSON. + envFilePath := filepath.Join(tempDir, "testbinary.env.json") + invalidContent := []byte("{ invalid json }") + if err := os.WriteFile(envFilePath, invalidContent, 0644); err != nil { + t.Fatal(err) + } + + if result := getEnvironmentalData(); result != nil { + t.Errorf("Expected nil when JSON is invalid, got: %+v", result) + } +} + +// TestGetEnvironmentalData_ValidJSON creates a valid env file and verifies that +// getEnvironmentalData correctly decodes it. +func TestGetEnvironmentalData_ValidJSON(t *testing.T) { + t.Setenv(constants.CIVisibilityEnvironmentDataFilePath, "") + + origArg := os.Args[0] + defer func() { os.Args[0] = origArg }() + + tempDir, err := os.MkdirTemp("", "envtest") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + + binaryPath := filepath.Join(tempDir, "testbinary") + os.Args[0] = binaryPath + + expected := &fileEnvironmentalData{ + WorkspacePath: "/workspace/path", + RepositoryURL: "https://github.com/repo.git", + CommitSHA: "abc123", + Branch: "main", + Tag: "v1.0", + CommitAuthorDate: "2021-01-01", + CommitAuthorName: "Author", + CommitAuthorEmail: "author@example.com", + CommitCommitterDate: "2021-01-02", + CommitCommitterName: "Committer", + CommitCommitterEmail: "committer@example.com", + CommitMessage: "Initial commit", + CIProviderName: "provider", + CIPipelineID: "pipeline_id", + CIPipelineURL: "https://ci.example.com", + CIPipelineName: "pipeline", + CIPipelineNumber: "42", + CIStageName: "stage", + CIJobName: "job", + CIJobURL: "https://ci.example.com/job", + CINodeName: "node", + CINodeLabels: "label", + DDCIEnvVars: "env_vars", + } + + envFilePath := filepath.Join(tempDir, "testbinary.env.json") + file, err := os.Create(envFilePath) + if err != nil { + t.Fatal(err) + } + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + if err := encoder.Encode(expected); err != nil { + t.Fatal(err) + } + file.Close() + + result := getEnvironmentalData() + if result == nil { + t.Fatal("Expected non-nil result when environmental file exists with valid JSON") + } + if !reflect.DeepEqual(result, expected) { + t.Errorf("Mismatch in environmental data.\nGot: %+v\nExpected: %+v", result, expected) + } +} + +// TestGetEnvironmentalData_UsesEnvVar verifies that when the environment variable +// is set, getEnvironmentalData uses that file. +func TestGetEnvironmentalData_UsesEnvVar(t *testing.T) { + origEnv := os.Getenv(constants.CIVisibilityEnvironmentDataFilePath) + defer os.Setenv(constants.CIVisibilityEnvironmentDataFilePath, origEnv) + + // Create a temporary file for environmental data. + tempDir, err := os.MkdirTemp("", "envtest") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + + customPath := filepath.Join(tempDir, "custom.env.json") + if err := os.Setenv(constants.CIVisibilityEnvironmentDataFilePath, customPath); err != nil { + t.Fatal(err) + } + + expected := &fileEnvironmentalData{ + // other fields can be left empty for this test + } + + file, err := os.Create(customPath) + if err != nil { + t.Fatal(err) + } + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + if err := encoder.Encode(expected); err != nil { + t.Fatal(err) + } + file.Close() + + result := getEnvironmentalData() + if result == nil { + t.Fatal("Expected non-nil environmental data when using env var") + } +} + +// --------------------- Tests for getEnvDataFileName ------------------------- + +// TestGetEnvDataFileName_WithEnvVar verifies that getEnvDataFileName returns the +// value from the environment variable when set. +func TestGetEnvDataFileName_WithEnvVar(t *testing.T) { + const customPath = "/tmp/custom.env.json" + orig := os.Getenv(constants.CIVisibilityEnvironmentDataFilePath) + defer os.Setenv(constants.CIVisibilityEnvironmentDataFilePath, orig) + + if err := os.Setenv(constants.CIVisibilityEnvironmentDataFilePath, customPath); err != nil { + t.Fatal(err) + } + if got := getEnvDataFileName(); got != customPath { + t.Errorf("Expected %q, got %q", customPath, got) + } +} + +// TestGetEnvDataFileName_WithoutEnvVar verifies that getEnvDataFileName constructs +// the file name based on os.Args[0] when the env var is not set. +func TestGetEnvDataFileName_WithoutEnvVar(t *testing.T) { + origEnv := os.Getenv(constants.CIVisibilityEnvironmentDataFilePath) + defer os.Setenv(constants.CIVisibilityEnvironmentDataFilePath, origEnv) + os.Setenv(constants.CIVisibilityEnvironmentDataFilePath, "") + + origArg := os.Args[0] + defer func() { os.Args[0] = origArg }() + + tempDir, err := os.MkdirTemp("", "envtest") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + + binaryPath := filepath.Join(tempDir, "testbinary") + os.Args[0] = binaryPath + + expected := filepath.Join(tempDir, "testbinary.env.json") + if got := getEnvDataFileName(); got != expected { + t.Errorf("Expected %q, got %q", expected, got) + } +} + +// --------------------- Tests for applyEnvironmentalDataIfRequired ------------------------- + +// TestApplyEnvironmentalDataIfRequired_NoEnvFile verifies that if there is no +// environmental file, the tags map remains unchanged. +func TestApplyEnvironmentalDataIfRequired_NoEnvFile(t *testing.T) { + t.Setenv(constants.CIVisibilityEnvironmentDataFilePath, "") + + origArg := os.Args[0] + defer func() { os.Args[0] = origArg }() + + tempDir, err := os.MkdirTemp("", "envtest") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + + binaryPath := filepath.Join(tempDir, "testbinary") + os.Args[0] = binaryPath + + tags := map[string]string{ + constants.CIWorkspacePath: "", + constants.GitRepositoryURL: "", + constants.GitCommitSHA: "", + constants.GitBranch: "", + constants.GitTag: "", + constants.GitCommitAuthorDate: "", + constants.GitCommitAuthorName: "", + constants.GitCommitAuthorEmail: "", + constants.GitCommitCommitterDate: "", + constants.GitCommitCommitterName: "", + constants.GitCommitCommitterEmail: "", + constants.GitCommitMessage: "", + constants.CIProviderName: "", + constants.CIPipelineID: "", + constants.CIPipelineURL: "", + constants.CIPipelineName: "", + constants.CIPipelineNumber: "", + constants.CIStageName: "", + constants.CIJobName: "", + constants.CIJobURL: "", + constants.CINodeName: "", + constants.CINodeLabels: "", + constants.CIEnvVars: "", + } + + applyEnvironmentalDataIfRequired(tags) + for key, val := range tags { + if val != "" { + t.Errorf("Expected tag %s to remain empty, got: %s", key, val) + } + } +} + +// TestApplyEnvironmentalDataIfRequired_WithEnvFile creates an env file and checks +// that applyEnvironmentalDataIfRequired populates only the missing values. +func TestApplyEnvironmentalDataIfRequired_WithEnvFile(t *testing.T) { + t.Setenv(constants.CIVisibilityEnvironmentDataFilePath, "") + + origArg := os.Args[0] + defer func() { os.Args[0] = origArg }() + + tempDir, err := os.MkdirTemp("", "envtest") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + + binaryPath := filepath.Join(tempDir, "testbinary") + os.Args[0] = binaryPath + + envData := &fileEnvironmentalData{ + WorkspacePath: "/workspace", + RepositoryURL: "repo_url", + CommitSHA: "sha", + Branch: "branch", + Tag: "tag", + CommitAuthorDate: "date1", + CommitAuthorName: "author", + CommitAuthorEmail: "author@example.com", + CommitCommitterDate: "date2", + CommitCommitterName: "committer", + CommitCommitterEmail: "committer@example.com", + CommitMessage: "message", + CIProviderName: "ci_provider", + CIPipelineID: "pipeline_id", + CIPipelineURL: "pipeline_url", + CIPipelineName: "pipeline_name", + CIPipelineNumber: "pipeline_number", + CIStageName: "stage", + CIJobName: "job", + CIJobURL: "job_url", + CINodeName: "node", + CINodeLabels: "labels", + DDCIEnvVars: "env_vars", + } + + // Create the env file. + envFilePath := filepath.Join(tempDir, "testbinary.env.json") + file, err := os.Create(envFilePath) + if err != nil { + t.Fatal(err) + } + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + if err := encoder.Encode(envData); err != nil { + t.Fatal(err) + } + file.Close() + + // Prepare tags with some values already set. + tags := map[string]string{ + constants.CIWorkspacePath: "existing", // should not be overwritten + constants.GitRepositoryURL: "", + constants.GitCommitSHA: "", + constants.GitBranch: "", + constants.GitTag: "", + constants.GitCommitAuthorDate: "", + constants.GitCommitAuthorName: "", + constants.GitCommitAuthorEmail: "", + constants.GitCommitCommitterDate: "", + constants.GitCommitCommitterName: "", + constants.GitCommitCommitterEmail: "", + constants.GitCommitMessage: "", + constants.CIProviderName: "", + constants.CIPipelineID: "", + constants.CIPipelineURL: "", + constants.CIPipelineName: "", + constants.CIPipelineNumber: "", + constants.CIStageName: "", + constants.CIJobName: "", + constants.CIJobURL: "", + constants.CINodeName: "", + constants.CINodeLabels: "", + constants.CIEnvVars: "", + } + + applyEnvironmentalDataIfRequired(tags) + if tags[constants.CIWorkspacePath] != "existing" { + t.Errorf("Expected CIWorkspacePath to remain 'existing', got '%s'", tags[constants.CIWorkspacePath]) + } + if tags[constants.GitRepositoryURL] != "repo_url" { + t.Errorf("Expected GitRepositoryURL to be 'repo_url', got '%s'", tags[constants.GitRepositoryURL]) + } + if tags[constants.GitCommitSHA] != "sha" { + t.Errorf("Expected GitCommitSHA to be 'sha', got '%s'", tags[constants.GitCommitSHA]) + } + if tags[constants.GitBranch] != "branch" { + t.Errorf("Expected GitBranch to be 'branch', got '%s'", tags[constants.GitBranch]) + } + if tags[constants.GitTag] != "tag" { + t.Errorf("Expected GitTag to be 'tag', got '%s'", tags[constants.GitTag]) + } + if tags[constants.GitCommitAuthorDate] != "date1" { + t.Errorf("Expected GitCommitAuthorDate to be 'date1', got '%s'", tags[constants.GitCommitAuthorDate]) + } + if tags[constants.GitCommitAuthorName] != "author" { + t.Errorf("Expected GitCommitAuthorName to be 'author', got '%s'", tags[constants.GitCommitAuthorName]) + } + if tags[constants.GitCommitAuthorEmail] != "author@example.com" { + t.Errorf("Expected GitCommitAuthorEmail to be 'author@example.com', got '%s'", tags[constants.GitCommitAuthorEmail]) + } + if tags[constants.GitCommitCommitterDate] != "date2" { + t.Errorf("Expected GitCommitCommitterDate to be 'date2', got '%s'", tags[constants.GitCommitCommitterDate]) + } + if tags[constants.GitCommitCommitterName] != "committer" { + t.Errorf("Expected GitCommitCommitterName to be 'committer', got '%s'", tags[constants.GitCommitCommitterName]) + } + if tags[constants.GitCommitCommitterEmail] != "committer@example.com" { + t.Errorf("Expected GitCommitCommitterEmail to be 'committer@example.com', got '%s'", tags[constants.GitCommitCommitterEmail]) + } + if tags[constants.GitCommitMessage] != "message" { + t.Errorf("Expected GitCommitMessage to be 'message', got '%s'", tags[constants.GitCommitMessage]) + } + if tags[constants.CIProviderName] != "ci_provider" { + t.Errorf("Expected CIProviderName to be 'ci_provider', got '%s'", tags[constants.CIProviderName]) + } + if tags[constants.CIPipelineID] != "pipeline_id" { + t.Errorf("Expected CIPipelineID to be 'pipeline_id', got '%s'", tags[constants.CIPipelineID]) + } + if tags[constants.CIPipelineURL] != "pipeline_url" { + t.Errorf("Expected CIPipelineURL to be 'pipeline_url', got '%s'", tags[constants.CIPipelineURL]) + } + if tags[constants.CIPipelineName] != "pipeline_name" { + t.Errorf("Expected CIPipelineName to be 'pipeline_name', got '%s'", tags[constants.CIPipelineName]) + } + if tags[constants.CIPipelineNumber] != "pipeline_number" { + t.Errorf("Expected CIPipelineNumber to be 'pipeline_number', got '%s'", tags[constants.CIPipelineNumber]) + } + if tags[constants.CIStageName] != "stage" { + t.Errorf("Expected CIStageName to be 'stage', got '%s'", tags[constants.CIStageName]) + } + if tags[constants.CIJobName] != "job" { + t.Errorf("Expected CIJobName to be 'job', got '%s'", tags[constants.CIJobName]) + } + if tags[constants.CIJobURL] != "job_url" { + t.Errorf("Expected CIJobURL to be 'job_url', got '%s'", tags[constants.CIJobURL]) + } + if tags[constants.CINodeName] != "node" { + t.Errorf("Expected CINodeName to be 'node', got '%s'", tags[constants.CINodeName]) + } + if tags[constants.CINodeLabels] != "labels" { + t.Errorf("Expected CINodeLabels to be 'labels', got '%s'", tags[constants.CINodeLabels]) + } + if tags[constants.CIEnvVars] != "env_vars" { + t.Errorf("Expected CIEnvVars to be 'env_vars', got '%s'", tags[constants.CIEnvVars]) + } +} + +// --------------------- Tests for createEnvironmentalDataFromTags ------------------------- + +// TestCreateEnvironmentalDataFromTags checks that a proper fileEnvironmentalData +// object is created from the given tags. +func TestCreateEnvironmentalDataFromTags(t *testing.T) { + // Nil tags should return nil. + if data := createEnvironmentalDataFromTags(nil); data != nil { + t.Error("Expected nil for nil tags") + } + + tags := map[string]string{ + constants.TestSessionName: "session", + constants.CIWorkspacePath: "/workspace", + constants.GitRepositoryURL: "repo", + constants.GitCommitSHA: "sha", + constants.GitBranch: "branch", + constants.GitTag: "tag", + constants.GitCommitAuthorDate: "date1", + constants.GitCommitAuthorName: "author", + constants.GitCommitAuthorEmail: "author@example.com", + constants.GitCommitCommitterDate: "date2", + constants.GitCommitCommitterName: "committer", + constants.GitCommitCommitterEmail: "committer@example.com", + constants.GitCommitMessage: "message", + constants.CIProviderName: "provider", + constants.CIPipelineID: "id", + constants.CIPipelineURL: "url", + constants.CIPipelineName: "name", + constants.CIPipelineNumber: "num", + constants.CIStageName: "stage", + constants.CIJobName: "job", + constants.CIJobURL: "joburl", + constants.CINodeName: "node", + constants.CINodeLabels: "labels", + constants.CIEnvVars: "env_vars", + } + + data := createEnvironmentalDataFromTags(tags) + if data == nil { + t.Fatal("Expected non-nil environmental data") + } + if data.WorkspacePath != "/workspace" { + t.Errorf("Expected WorkspacePath '/workspace', got '%s'", data.WorkspacePath) + } + // Additional field checks can be added similarly if needed. +} + +// --------------------- Tests for writeEnvironmentalDataToFile ------------------------- + +// TestWriteEnvironmentalDataToFile_NilTags verifies that if tags is nil, +// the function returns nil and does not create a file. +func TestWriteEnvironmentalDataToFile_NilTags(t *testing.T) { + t.Setenv(constants.CIVisibilityEnvironmentDataFilePath, "") + + tempDir, err := os.MkdirTemp("", "envtest") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + + filePath := filepath.Join(tempDir, "output.env.json") + err = writeEnvironmentalDataToFile(filePath, nil) + if err != nil { + t.Errorf("Expected nil error for nil tags, got: %v", err) + } + if _, err := os.Stat(filePath); !os.IsNotExist(err) { + t.Errorf("Expected file not to exist when tags is nil") + } +} + +// TestWriteEnvironmentalDataToFile_WithTags creates a file from given tags and +// verifies that the written JSON matches the expected values. +func TestWriteEnvironmentalDataToFile_WithTags(t *testing.T) { + t.Setenv(constants.CIVisibilityEnvironmentDataFilePath, "") + + tempDir, err := os.MkdirTemp("", "envtest") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + + filePath := filepath.Join(tempDir, "output.env.json") + tags := map[string]string{ + constants.CIWorkspacePath: "/workspace", + constants.GitRepositoryURL: "repo", + constants.GitCommitSHA: "sha", + constants.GitBranch: "branch", + constants.GitTag: "tag", + constants.GitCommitAuthorDate: "date1", + constants.GitCommitAuthorName: "author", + constants.GitCommitAuthorEmail: "author@example.com", + constants.GitCommitCommitterDate: "date2", + constants.GitCommitCommitterName: "committer", + constants.GitCommitCommitterEmail: "committer@example.com", + constants.GitCommitMessage: "message", + constants.CIProviderName: "provider", + constants.CIPipelineID: "id", + constants.CIPipelineURL: "url", + constants.CIPipelineName: "name", + constants.CIPipelineNumber: "num", + constants.CIStageName: "stage", + constants.CIJobName: "job", + constants.CIJobURL: "joburl", + constants.CINodeName: "node", + constants.CINodeLabels: "labels", + constants.CIEnvVars: "env_vars", + } + + err = writeEnvironmentalDataToFile(filePath, tags) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + data, err := os.ReadFile(filePath) + if err != nil { + t.Fatalf("Error reading file: %v", err) + } + var envData fileEnvironmentalData + if err := json.Unmarshal(data, &envData); err != nil { + t.Fatalf("Error decoding JSON: %v", err) + } + if envData.WorkspacePath != "/workspace" { + t.Errorf("Expected WorkspacePath '/workspace', got '%s'", envData.WorkspacePath) + } + // Additional field checks can be added similarly if needed. +}