blob: c220a501756afd9c6b39b4b3a845b2d767a80574 [file]
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You 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.
package tools
import (
"encoding/json"
"os"
"testing"
structpb "google.golang.org/protobuf/types/known/structpb"
)
func parseProtoForTest(t *testing.T, options string) *structpb.Struct {
if options == "" {
options = "{}"
}
var raw map[string]interface{}
if err := json.Unmarshal([]byte(options), &raw); err != nil {
t.Fatalf("failed to unmarshal JSON for test: %v", err)
}
st, err := structpb.NewStruct(raw)
if err != nil {
t.Fatalf("failed to create structpb for test: %v", err)
}
return st
}
func TestMakePipelineOptionsFileAndEnvVar(t *testing.T) {
tests := []struct {
name string
inputOptions string
expectedError string
}{
{
"empty options",
"{}",
"",
},
{
"valid options",
"{\"abc\": 123}",
"",
},
{
"invalid options",
"{4}",
"options string is not JSON formatted {4}",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Cleanup(os.Clearenv)
err := MakePipelineOptionsFileAndEnvVar(test.inputOptions)
if err != nil {
if got, want := err.Error(), test.expectedError; got != want {
t.Errorf("got error: %v, want error: %v", got, want)
}
}
})
}
os.Remove("pipeline_options.json")
}
func TestParseOptionsFromProto_NestedOptionsNoNamespace(t *testing.T) {
p := parseProtoForTest(t, `{
"options": {
"profiler_agent": "memray",
"profile_upload_interval_sec": 10,
"profiler_stop_after_crash": true,
"profile_sample_rate": 0.5,
"experiments": ["beam_fn_api", "pip_use_build_isolation"]
}
}`)
po := ParseOptionsFromProto(p, "")
if got, err := po.GetString("profiler_agent"); err != nil || got != "memray" {
t.Errorf("GetString(profiler_agent) = (%q, %v), want (\"memray\", nil)", got, err)
}
if got, err := po.GetInt("profile_upload_interval_sec"); err != nil || got != 10 {
t.Errorf("GetInt(profile_upload_interval_sec) = (%d, %v), want (10, nil)", got, err)
}
if got, err := po.GetBool("profiler_stop_after_crash"); err != nil || got != true {
t.Errorf("GetBool(profiler_stop_after_crash) = (%t, %v), want (true, nil)", got, err)
}
// Sample float option in Beam. Unused for memray in practice.
if got, err := po.GetFloat64("profile_sample_rate"); err != nil || got != 0.5 {
t.Errorf("GetFloat64(profile_sample_rate) = (%f, %v), want (0.5, nil)", got, err)
}
experiments, err := po.GetStringSlice("experiments")
if err != nil || len(experiments) != 2 || experiments[0] != "beam_fn_api" || experiments[1] != "pip_use_build_isolation" {
t.Errorf("GetStringSlice(experiments) = (%v, %v), want ([beam_fn_api, pip_use_build_isolation], nil)", experiments, err)
}
if !po.HasExperiment("beam_fn_api") || !po.HasExperiment("pip_use_build_isolation") {
t.Errorf("expected experiments beam_fn_api and pip_use_build_isolation to be present, experiments map: %+v", po.experiments)
}
}
func TestParseOptionsFromProto_FlatOptionsWithURN(t *testing.T) {
p := parseProtoForTest(t, `{
"beam:option:profiler_agent:v1": "memray",
"beam:option:profile_upload_interval_sec:v1": "10",
"beam:option:profiler_stop_after_crash:v1": "true",
"beam:option:profile_sample_rate:v1": "0.5",
"beam:option:experiments:v1": ["beam_fn_api", "another_exp"]
}`)
po := ParseOptionsFromProto(p, "")
if got, err := po.GetString("profiler_agent"); err != nil || got != "memray" {
t.Errorf("GetString(profiler_agent) = (%q, %v), want (\"memray\", nil)", got, err)
}
if got, err := po.GetInt("profile_upload_interval_sec"); err != nil || got != 10 {
t.Errorf("GetInt(profile_upload_interval_sec) = (%d, %v), want (10, nil)", got, err)
}
if got, err := po.GetBool("profiler_stop_after_crash"); err != nil || got != true {
t.Errorf("GetBool(profiler_stop_after_crash) = (%t, %v), want (true, nil)", got, err)
}
if got, err := po.GetFloat64("profile_sample_rate"); err != nil || got != 0.5 {
t.Errorf("GetFloat64(profile_sample_rate) = (%f, %v), want (0.5, nil)", got, err)
}
if got, err := po.GetString("experiments"); err != nil || got != "beam_fn_api,another_exp" {
t.Errorf("GetString(experiments) = (%q, %v), want (\"beam_fn_api,another_exp\", nil)", got, err)
}
experiments, err := po.GetStringSlice("experiments")
if err != nil || len(experiments) != 2 || experiments[0] != "beam_fn_api" || experiments[1] != "another_exp" {
t.Errorf("GetStringSlice(experiments) = (%v, %v), want ([beam_fn_api, another_exp], nil)", experiments, err)
}
if !po.HasExperiment("beam_fn_api") || !po.HasExperiment("another_exp") {
t.Errorf("expected experiments beam_fn_api and another_exp to be present, experiments map: %+v", po.experiments)
}
}
func TestParseOptionsFromProto_CommaSeparatedExperiments(t *testing.T) {
p := parseProtoForTest(t, `{
"beam:option:go_options:v1": {
"options": {
"experiments": "exp1,exp2,exp3",
"dataflow_service_options": "opt1"
}
}
}`)
po := ParseOptionsFromProto(p, "go_options")
experiments, err := po.GetStringSlice("experiments")
if err != nil || len(experiments) != 3 || experiments[0] != "exp1" || experiments[1] != "exp2" || experiments[2] != "exp3" {
t.Errorf("GetStringSlice(experiments) = (%v, %v), want ([exp1, exp2, exp3], nil)", experiments, err)
}
if !po.HasExperiment("exp1") || !po.HasExperiment("exp2") || !po.HasExperiment("exp3") {
t.Errorf("expected experiments exp1, exp2, and exp3 to be present, experiments map: %+v", po.experiments)
}
serviceOpts, err := po.GetStringSlice("dataflow_service_options")
if err != nil || len(serviceOpts) != 1 || serviceOpts[0] != "opt1" {
t.Errorf("GetStringSlice(dataflow_service_options) = (%v, %v), want ([opt1], nil)", serviceOpts, err)
}
}
func TestParseOptionsFromProto_MalformedOptions(t *testing.T) {
t.Run("malformed integer", func(t *testing.T) {
p := parseProtoForTest(t, `{"options": {"profile_upload_interval_sec": "invalid"}}`)
po := ParseOptionsFromProto(p, "")
_, err := po.GetInt("profile_upload_interval_sec")
if err == nil {
t.Errorf("expected error, got nil")
}
})
t.Run("malformed bool", func(t *testing.T) {
p := parseProtoForTest(t, `{"options": {"profiler_stop_after_crash": "maybe"}}`)
po := ParseOptionsFromProto(p, "")
_, err := po.GetBool("profiler_stop_after_crash")
if err == nil {
t.Errorf("expected error, got nil")
}
})
t.Run("type mismatch int expected got bool", func(t *testing.T) {
p := parseProtoForTest(t, `{"options": {"profile_upload_interval_sec": true}}`)
po := ParseOptionsFromProto(p, "")
_, err := po.GetInt("profile_upload_interval_sec")
if err == nil {
t.Errorf("expected error, got nil")
}
})
t.Run("missing key returns error", func(t *testing.T) {
p := parseProtoForTest(t, `{}`)
po := ParseOptionsFromProto(p, "")
_, errInt := po.GetInt("profile_upload_interval_sec")
_, errBool := po.GetBool("profiler_stop_after_crash")
_, errFloat := po.GetFloat64("profile_sample_rate")
if errInt == nil || errBool == nil || errFloat == nil {
t.Errorf("expected error for missing keys, got: intErr=%v, boolErr=%v, floatErr=%v", errInt, errBool, errFloat)
}
})
}
func TestPipelineOptions_HasOption(t *testing.T) {
p := parseProtoForTest(t, `{"options": {"profile_upload_interval_sec": 10}}`)
po := ParseOptionsFromProto(p, "")
if !po.HasOption("profile_upload_interval_sec") {
t.Errorf("HasOption(profile_upload_interval_sec) = false, want true")
}
if po.HasOption("profiler_stop_after_crash") {
t.Errorf("HasOption(profiler_stop_after_crash) = true, want false")
}
}
func TestPipelineOptions_HasExperiment(t *testing.T) {
p := parseProtoForTest(t, `{"options": {"experiments": ["exp1", "exp2=val2"]}}`)
po := ParseOptionsFromProto(p, "")
if !po.HasExperiment("exp1") {
t.Errorf("HasExperiment(exp1) = false, want true")
}
if !po.HasExperiment("exp2") {
t.Errorf("HasExperiment(exp2) = false, want true")
}
if po.HasExperiment("exp3") {
t.Errorf("HasExperiment(exp3) = true, want false")
}
}
func TestPipelineOptions_LookupExperiment(t *testing.T) {
p := parseProtoForTest(t, `{"options": {"experiments": ["exp1", "exp2=val2", "exp3=val3=val4"]}}`)
po := ParseOptionsFromProto(p, "")
val, ok := po.LookupExperiment("exp1")
if !ok || val != "" {
t.Errorf("LookupExperiment(exp1) = (%q, %t), want (\"\", true)", val, ok)
}
val, ok = po.LookupExperiment("exp2")
if !ok || val != "val2" {
t.Errorf("LookupExperiment(exp2) = (%q, %t), want (\"val2\", true)", val, ok)
}
val, ok = po.LookupExperiment("exp3")
if !ok || val != "val3=val4" {
t.Errorf("LookupExperiment(exp3) = (%q, %t), want (\"val3=val4\", true)", val, ok)
}
val, ok = po.LookupExperiment("exp4")
if ok || val != "" {
t.Errorf("LookupExperiment(exp4) = (%q, %t), want (\"\", false)", val, ok)
}
}
func TestParseOptionsFromProto_SDKOptionsPromotion(t *testing.T) {
optionsStruct, err := structpb.NewStruct(map[string]interface{}{
"options": map[string]interface{}{
"region": "us-central1",
},
"beam:option:experiments:v1": []interface{}{"expA", "expB"},
"beam:option:go_options:v1": map[string]interface{}{
"options": map[string]interface{}{
"dataflow_service_options": "enable_google_cloud_profiler,enable_new_custom_feature",
},
},
})
if err != nil {
t.Fatalf("failed to create proto Struct: %v", err)
}
po := ParseOptionsFromProto(optionsStruct, "go_options")
if got, err := po.GetString("region"); err != nil || got != "us-central1" {
t.Errorf("GetString(region) = (%q, %v), want (\"us-central1\", nil)", got, err)
}
if !po.HasExperiment("expA") || !po.HasExperiment("expB") {
t.Errorf("expected experiments expA and expB to be present, options: %+v", po.options)
}
if got, err := po.GetString("dataflow_service_options"); err != nil || got != "enable_google_cloud_profiler,enable_new_custom_feature" {
t.Errorf("GetString(dataflow_service_options) = (%q, %v), want (\"enable_google_cloud_profiler,enable_new_custom_feature\", nil)", got, err)
}
serviceOpts, err := po.GetStringSlice("dataflow_service_options")
if err != nil || len(serviceOpts) != 2 || serviceOpts[0] != "enable_google_cloud_profiler" || serviceOpts[1] != "enable_new_custom_feature" {
t.Errorf("GetStringSlice(dataflow_service_options) = (%v, %v), want ([enable_google_cloud_profiler, enable_new_custom_feature], nil)", serviceOpts, err)
}
goOpts, ok := po.options["go_options"].(map[string]any)
if !ok {
t.Errorf("expected go_options map to be present, options: %+v", po.options)
}
nestedOpts, ok := goOpts["options"].(map[string]any)
if !ok {
t.Errorf("expected nested options map inside go_options, got: %+v", goOpts)
}
if got := nestedOpts["dataflow_service_options"]; got != "enable_google_cloud_profiler,enable_new_custom_feature" {
t.Errorf("got dataflow_service_options = %v, want enable_google_cloud_profiler,enable_new_custom_feature", got)
}
}