Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3cc3154d authored by Colin Cross's avatar Colin Cross
Browse files

Remove more bp2build

Bug: 315353489
Test: m blueprint_tests
Change-Id: Ib854fe1a448c258fe086691a6e5ed2d98537f5e4
parent d3f7d1a4
Loading
Loading
Loading
Loading
+0 −1
Original line number Diff line number Diff line
@@ -105,7 +105,6 @@ bootstrap_go_package {
        "apex_test.go",
        "arch_test.go",
        "config_test.go",
        "config_bp2build_test.go",
        "configured_jars_test.go",
        "csuite_config_test.go",
        "defaults_test.go",
+0 −1650

File changed.

Preview size limit exceeded, changes collapsed.

+0 −360
Original line number Diff line number Diff line
@@ -15,24 +15,11 @@
package android

import (
	"fmt"
	"reflect"
	"regexp"
	"sort"
	"strings"

	"android/soong/bazel"
	"android/soong/starlark_fmt"

	"github.com/google/blueprint"
)

// BazelVarExporter is a collection of configuration variables that can be exported for use in Bazel rules
type BazelVarExporter interface {
	// asBazel expands strings of configuration variables into their concrete values
	asBazel(Config, ExportedStringVariables, ExportedStringListVariables, ExportedConfigDependingVariables) []bazelConstant
}

// ExportedVariables is a collection of interdependent configuration variables
type ExportedVariables struct {
	// Maps containing toolchain variables that are independent of the
@@ -61,18 +48,6 @@ func NewExportedVariables(pctx PackageContext) ExportedVariables {
	}
}

func (ev ExportedVariables) asBazel(config Config,
	stringVars ExportedStringVariables, stringListVars ExportedStringListVariables, cfgDepVars ExportedConfigDependingVariables) []bazelConstant {
	ret := []bazelConstant{}
	ret = append(ret, ev.exportedStringVars.asBazel(config, stringVars, stringListVars, cfgDepVars)...)
	ret = append(ret, ev.exportedStringListVars.asBazel(config, stringVars, stringListVars, cfgDepVars)...)
	ret = append(ret, ev.exportedStringListDictVars.asBazel(config, stringVars, stringListVars, cfgDepVars)...)
	// Note: ExportedVariableReferenceDictVars collections can only contain references to other variables and must be printed last
	ret = append(ret, ev.exportedVariableReferenceDictVars.asBazel(config, stringVars, stringListVars, cfgDepVars)...)
	ret = append(ret, ev.exportedConfigDependingVars.asBazel(config, stringVars, stringListVars, cfgDepVars)...)
	return ret
}

// ExportStringStaticVariable declares a static string variable and exports it to
// Bazel's toolchain.
func (ev ExportedVariables) ExportStringStaticVariable(name string, value string) {
@@ -142,49 +117,6 @@ func (m ExportedConfigDependingVariables) set(k string, v interface{}) {
	m[k] = v
}

func (m ExportedConfigDependingVariables) asBazel(config Config,
	stringVars ExportedStringVariables, stringListVars ExportedStringListVariables, cfgDepVars ExportedConfigDependingVariables) []bazelConstant {
	ret := make([]bazelConstant, 0, len(m))
	for variable, unevaluatedVar := range m {
		evalFunc := reflect.ValueOf(unevaluatedVar)
		validateVariableMethod(variable, evalFunc)
		evaluatedResult := evalFunc.Call([]reflect.Value{reflect.ValueOf(config)})
		evaluatedValue := evaluatedResult[0].Interface().(string)
		expandedVars, err := expandVar(config, evaluatedValue, stringVars, stringListVars, cfgDepVars)
		if err != nil {
			panic(fmt.Errorf("error expanding config variable %s: %s", variable, err))
		}
		if len(expandedVars) > 1 {
			ret = append(ret, bazelConstant{
				variableName:       variable,
				internalDefinition: starlark_fmt.PrintStringList(expandedVars, 0),
			})
		} else {
			ret = append(ret, bazelConstant{
				variableName:       variable,
				internalDefinition: fmt.Sprintf(`"%s"`, validateCharacters(expandedVars[0])),
			})
		}
	}
	return ret
}

// Ensure that string s has no invalid characters to be generated into the bzl file.
func validateCharacters(s string) string {
	for _, c := range []string{`\n`, `"`, `\`} {
		if strings.Contains(s, c) {
			panic(fmt.Errorf("%s contains illegal character %s", s, c))
		}
	}
	return s
}

type bazelConstant struct {
	variableName       string
	internalDefinition string
	sortLast           bool
}

// ExportedStringVariables is a mapping of variable names to string values
type ExportedStringVariables map[string]string

@@ -192,25 +124,6 @@ func (m ExportedStringVariables) set(k string, v string) {
	m[k] = v
}

func (m ExportedStringVariables) asBazel(config Config,
	stringVars ExportedStringVariables, stringListVars ExportedStringListVariables, cfgDepVars ExportedConfigDependingVariables) []bazelConstant {
	ret := make([]bazelConstant, 0, len(m))
	for k, variableValue := range m {
		expandedVar, err := expandVar(config, variableValue, stringVars, stringListVars, cfgDepVars)
		if err != nil {
			panic(fmt.Errorf("error expanding config variable %s: %s", k, err))
		}
		if len(expandedVar) > 1 {
			panic(fmt.Errorf("%q expands to more than one string value: %q", variableValue, expandedVar))
		}
		ret = append(ret, bazelConstant{
			variableName:       k,
			internalDefinition: fmt.Sprintf(`"%s"`, validateCharacters(expandedVar[0])),
		})
	}
	return ret
}

// ExportedStringListVariables is a mapping of variable names to a list of strings
type ExportedStringListVariables map[string][]string

@@ -218,32 +131,6 @@ func (m ExportedStringListVariables) set(k string, v []string) {
	m[k] = v
}

func (m ExportedStringListVariables) asBazel(config Config,
	stringScope ExportedStringVariables, stringListScope ExportedStringListVariables,
	exportedVars ExportedConfigDependingVariables) []bazelConstant {
	ret := make([]bazelConstant, 0, len(m))
	// For each exported variable, recursively expand elements in the variableValue
	// list to ensure that interpolated variables are expanded according to their values
	// in the variable scope.
	for k, variableValue := range m {
		var expandedVars []string
		for _, v := range variableValue {
			expandedVar, err := expandVar(config, v, stringScope, stringListScope, exportedVars)
			if err != nil {
				panic(fmt.Errorf("Error expanding config variable %s=%s: %s", k, v, err))
			}
			expandedVars = append(expandedVars, expandedVar...)
		}
		// Assign the list as a bzl-private variable; this variable will be exported
		// out through a constants struct later.
		ret = append(ret, bazelConstant{
			variableName:       k,
			internalDefinition: starlark_fmt.PrintStringList(expandedVars, 0),
		})
	}
	return ret
}

// ExportedStringListDictVariables is a mapping from variable names to a
// dictionary which maps keys to lists of strings
type ExportedStringListDictVariables map[string]map[string][]string
@@ -252,19 +139,6 @@ func (m ExportedStringListDictVariables) set(k string, v map[string][]string) {
	m[k] = v
}

// Since dictionaries are not supported in Ninja, we do not expand variables for dictionaries
func (m ExportedStringListDictVariables) asBazel(_ Config, _ ExportedStringVariables,
	_ ExportedStringListVariables, _ ExportedConfigDependingVariables) []bazelConstant {
	ret := make([]bazelConstant, 0, len(m))
	for k, dict := range m {
		ret = append(ret, bazelConstant{
			variableName:       k,
			internalDefinition: starlark_fmt.PrintStringListDict(dict, 0),
		})
	}
	return ret
}

// ExportedVariableReferenceDictVariables is a mapping from variable names to a
// dictionary which references previously defined variables. This is used to
// create a Starlark output such as:
@@ -281,237 +155,3 @@ type ExportedVariableReferenceDictVariables map[string]map[string]string
func (m ExportedVariableReferenceDictVariables) set(k string, v map[string]string) {
	m[k] = v
}

func (m ExportedVariableReferenceDictVariables) asBazel(_ Config, _ ExportedStringVariables,
	_ ExportedStringListVariables, _ ExportedConfigDependingVariables) []bazelConstant {
	ret := make([]bazelConstant, 0, len(m))
	for n, dict := range m {
		for k, v := range dict {
			matches, err := variableReference(v)
			if err != nil {
				panic(err)
			} else if !matches.matches {
				panic(fmt.Errorf("Expected a variable reference, got %q", v))
			} else if len(matches.fullVariableReference) != len(v) {
				panic(fmt.Errorf("Expected only a variable reference, got %q", v))
			}
			dict[k] = "_" + matches.variable
		}
		ret = append(ret, bazelConstant{
			variableName:       n,
			internalDefinition: starlark_fmt.PrintDict(dict, 0),
			sortLast:           true,
		})
	}
	return ret
}

// BazelToolchainVars expands an ExportedVariables collection and returns a string
// of formatted Starlark variable definitions
func BazelToolchainVars(config Config, exportedVars ExportedVariables) string {
	results := exportedVars.asBazel(
		config,
		exportedVars.exportedStringVars,
		exportedVars.exportedStringListVars,
		exportedVars.exportedConfigDependingVars,
	)

	sort.Slice(results, func(i, j int) bool {
		if results[i].sortLast != results[j].sortLast {
			return !results[i].sortLast
		}
		return results[i].variableName < results[j].variableName
	})

	definitions := make([]string, 0, len(results))
	constants := make([]string, 0, len(results))
	for _, b := range results {
		definitions = append(definitions,
			fmt.Sprintf("_%s = %s", b.variableName, b.internalDefinition))
		constants = append(constants,
			fmt.Sprintf("%[1]s%[2]s = _%[2]s,", starlark_fmt.Indention(1), b.variableName))
	}

	// Build the exported constants struct.
	ret := bazel.GeneratedBazelFileWarning
	ret += "\n\n"
	ret += strings.Join(definitions, "\n\n")
	ret += "\n\n"
	ret += "constants = struct(\n"
	ret += strings.Join(constants, "\n")
	ret += "\n)"

	return ret
}

type match struct {
	matches               bool
	fullVariableReference string
	variable              string
}

func variableReference(input string) (match, error) {
	// e.g. "${ExternalCflags}"
	r := regexp.MustCompile(`\${(?:config\.)?([a-zA-Z0-9_]+)}`)

	matches := r.FindStringSubmatch(input)
	if len(matches) == 0 {
		return match{}, nil
	}
	if len(matches) != 2 {
		return match{}, fmt.Errorf("Expected to only match 1 subexpression in %s, got %d", input, len(matches)-1)
	}
	return match{
		matches:               true,
		fullVariableReference: matches[0],
		// Index 1 of FindStringSubmatch contains the subexpression match
		// (variable name) of the capture group.
		variable: matches[1],
	}, nil
}

// expandVar recursively expand interpolated variables in the exportedVars scope.
//
// We're using a string slice to track the seen variables to avoid
// stackoverflow errors with infinite recursion. it's simpler to use a
// string slice than to handle a pass-by-referenced map, which would make it
// quite complex to track depth-first interpolations. It's also unlikely the
// interpolation stacks are deep (n > 1).
func expandVar(config Config, toExpand string, stringScope ExportedStringVariables,
	stringListScope ExportedStringListVariables, exportedVars ExportedConfigDependingVariables) ([]string, error) {

	// Internal recursive function.
	var expandVarInternal func(string, map[string]bool) (string, error)
	expandVarInternal = func(toExpand string, seenVars map[string]bool) (string, error) {
		var ret string
		remainingString := toExpand
		for len(remainingString) > 0 {
			matches, err := variableReference(remainingString)
			if err != nil {
				panic(err)
			}
			if !matches.matches {
				return ret + remainingString, nil
			}
			matchIndex := strings.Index(remainingString, matches.fullVariableReference)
			ret += remainingString[:matchIndex]
			remainingString = remainingString[matchIndex+len(matches.fullVariableReference):]

			variable := matches.variable
			// toExpand contains a variable.
			if _, ok := seenVars[variable]; ok {
				return ret, fmt.Errorf(
					"Unbounded recursive interpolation of variable: %s", variable)
			}
			// A map is passed-by-reference. Create a new map for
			// this scope to prevent variables seen in one depth-first expansion
			// to be also treated as "seen" in other depth-first traversals.
			newSeenVars := map[string]bool{}
			for k := range seenVars {
				newSeenVars[k] = true
			}
			newSeenVars[variable] = true
			if unexpandedVars, ok := stringListScope[variable]; ok {
				expandedVars := []string{}
				for _, unexpandedVar := range unexpandedVars {
					expandedVar, err := expandVarInternal(unexpandedVar, newSeenVars)
					if err != nil {
						return ret, err
					}
					expandedVars = append(expandedVars, expandedVar)
				}
				ret += strings.Join(expandedVars, " ")
			} else if unexpandedVar, ok := stringScope[variable]; ok {
				expandedVar, err := expandVarInternal(unexpandedVar, newSeenVars)
				if err != nil {
					return ret, err
				}
				ret += expandedVar
			} else if unevaluatedVar, ok := exportedVars[variable]; ok {
				evalFunc := reflect.ValueOf(unevaluatedVar)
				validateVariableMethod(variable, evalFunc)
				evaluatedResult := evalFunc.Call([]reflect.Value{reflect.ValueOf(config)})
				evaluatedValue := evaluatedResult[0].Interface().(string)
				expandedVar, err := expandVarInternal(evaluatedValue, newSeenVars)
				if err != nil {
					return ret, err
				}
				ret += expandedVar
			} else {
				return "", fmt.Errorf("Unbound config variable %s", variable)
			}
		}
		return ret, nil
	}
	var ret []string
	stringFields := splitStringKeepingQuotedSubstring(toExpand, ' ')
	for _, v := range stringFields {
		val, err := expandVarInternal(v, map[string]bool{})
		if err != nil {
			return ret, err
		}
		ret = append(ret, val)
	}

	return ret, nil
}

// splitStringKeepingQuotedSubstring splits a string on a provided separator,
// but it will not split substrings inside unescaped double quotes. If the double
// quotes are escaped, then the returned string will only include the quote, and
// not the escape.
func splitStringKeepingQuotedSubstring(s string, delimiter byte) []string {
	var ret []string
	quote := byte('"')

	var substring []byte
	quoted := false
	escaped := false

	for i := range s {
		if !quoted && s[i] == delimiter {
			ret = append(ret, string(substring))
			substring = []byte{}
			continue
		}

		characterIsEscape := i < len(s)-1 && s[i] == '\\' && s[i+1] == quote
		if characterIsEscape {
			escaped = true
			continue
		}

		if s[i] == quote {
			if !escaped {
				quoted = !quoted
			}
			escaped = false
		}

		substring = append(substring, s[i])
	}

	ret = append(ret, string(substring))

	return ret
}

func validateVariableMethod(name string, methodValue reflect.Value) {
	methodType := methodValue.Type()
	if methodType.Kind() != reflect.Func {
		panic(fmt.Errorf("method given for variable %s is not a function",
			name))
	}
	if n := methodType.NumIn(); n != 1 {
		panic(fmt.Errorf("method for variable %s has %d inputs (should be 1)",
			name, n))
	}
	if n := methodType.NumOut(); n != 1 {
		panic(fmt.Errorf("method for variable %s has %d outputs (should be 1)",
			name, n))
	}
	if kind := methodType.Out(0).Kind(); kind != reflect.String {
		panic(fmt.Errorf("method for variable %s does not return a string",
			name))
	}
}

android/config_bp2build_test.go

deleted100644 → 0
+0 −454
Original line number Diff line number Diff line
// Copyright 2021 Google Inc. All rights reserved.
//
// 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.

package android

import (
	"android/soong/bazel"
	"testing"
)

func TestExpandVars(t *testing.T) {
	android_arm64_config := TestConfig("out", nil, "", nil)
	android_arm64_config.BuildOS = Android
	android_arm64_config.BuildArch = Arm64

	testCases := []struct {
		description     string
		config          Config
		stringScope     ExportedStringVariables
		stringListScope ExportedStringListVariables
		configVars      ExportedConfigDependingVariables
		toExpand        string
		expectedValues  []string
	}{
		{
			description:    "no expansion for non-interpolated value",
			toExpand:       "foo",
			expectedValues: []string{"foo"},
		},
		{
			description: "single level expansion for string var",
			stringScope: ExportedStringVariables{
				"foo": "bar",
			},
			toExpand:       "${foo}",
			expectedValues: []string{"bar"},
		},
		{
			description: "single level expansion with short-name for string var",
			stringScope: ExportedStringVariables{
				"foo": "bar",
			},
			toExpand:       "${config.foo}",
			expectedValues: []string{"bar"},
		},
		{
			description: "single level expansion string list var",
			stringListScope: ExportedStringListVariables{
				"foo": []string{"bar"},
			},
			toExpand:       "${foo}",
			expectedValues: []string{"bar"},
		},
		{
			description: "mixed level expansion for string list var",
			stringScope: ExportedStringVariables{
				"foo": "${bar}",
				"qux": "hello",
			},
			stringListScope: ExportedStringListVariables{
				"bar": []string{"baz", "${qux}"},
			},
			toExpand:       "${foo}",
			expectedValues: []string{"baz hello"},
		},
		{
			description: "double level expansion",
			stringListScope: ExportedStringListVariables{
				"foo": []string{"${bar}"},
				"bar": []string{"baz"},
			},
			toExpand:       "${foo}",
			expectedValues: []string{"baz"},
		},
		{
			description: "double level expansion with a literal",
			stringListScope: ExportedStringListVariables{
				"a": []string{"${b}", "c"},
				"b": []string{"d"},
			},
			toExpand:       "${a}",
			expectedValues: []string{"d c"},
		},
		{
			description: "double level expansion, with two variables in a string",
			stringListScope: ExportedStringListVariables{
				"a": []string{"${b} ${c}"},
				"b": []string{"d"},
				"c": []string{"e"},
			},
			toExpand:       "${a}",
			expectedValues: []string{"d e"},
		},
		{
			description: "triple level expansion with two variables in a string",
			stringListScope: ExportedStringListVariables{
				"a": []string{"${b} ${c}"},
				"b": []string{"${c}", "${d}"},
				"c": []string{"${d}"},
				"d": []string{"foo"},
			},
			toExpand:       "${a}",
			expectedValues: []string{"foo foo foo"},
		},
		{
			description: "expansion with config depending vars",
			configVars: ExportedConfigDependingVariables{
				"a": func(c Config) string { return c.BuildOS.String() },
				"b": func(c Config) string { return c.BuildArch.String() },
			},
			config:         android_arm64_config,
			toExpand:       "${a}-${b}",
			expectedValues: []string{"android-arm64"},
		},
		{
			description: "double level multi type expansion",
			stringListScope: ExportedStringListVariables{
				"platform": []string{"${os}-${arch}"},
				"const":    []string{"const"},
			},
			configVars: ExportedConfigDependingVariables{
				"os":   func(c Config) string { return c.BuildOS.String() },
				"arch": func(c Config) string { return c.BuildArch.String() },
				"foo":  func(c Config) string { return "foo" },
			},
			config:         android_arm64_config,
			toExpand:       "${const}/${platform}/${foo}",
			expectedValues: []string{"const/android-arm64/foo"},
		},
	}

	for _, testCase := range testCases {
		t.Run(testCase.description, func(t *testing.T) {
			output, _ := expandVar(testCase.config, testCase.toExpand, testCase.stringScope, testCase.stringListScope, testCase.configVars)
			if len(output) != len(testCase.expectedValues) {
				t.Errorf("Expected %d values, got %d", len(testCase.expectedValues), len(output))
			}
			for i, actual := range output {
				expectedValue := testCase.expectedValues[i]
				if actual != expectedValue {
					t.Errorf("Actual value '%s' doesn't match expected value '%s'", actual, expectedValue)
				}
			}
		})
	}
}

func TestBazelToolchainVars(t *testing.T) {
	testCases := []struct {
		name        string
		config      Config
		vars        ExportedVariables
		expectedOut string
	}{
		{
			name: "exports strings",
			vars: ExportedVariables{
				exportedStringVars: ExportedStringVariables{
					"a": "b",
					"c": "d",
				},
			},
			expectedOut: bazel.GeneratedBazelFileWarning + `

_a = "b"

_c = "d"

constants = struct(
    a = _a,
    c = _c,
)`,
		},
		{
			name: "exports string lists",
			vars: ExportedVariables{
				exportedStringListVars: ExportedStringListVariables{
					"a": []string{"b1", "b2"},
					"c": []string{"d1", "d2"},
				},
			},
			expectedOut: bazel.GeneratedBazelFileWarning + `

_a = [
    "b1",
    "b2",
]

_c = [
    "d1",
    "d2",
]

constants = struct(
    a = _a,
    c = _c,
)`,
		},
		{
			name: "exports string lists dicts",
			vars: ExportedVariables{
				exportedStringListDictVars: ExportedStringListDictVariables{
					"a": map[string][]string{"b1": {"b2"}},
					"c": map[string][]string{"d1": {"d2"}},
				},
			},
			expectedOut: bazel.GeneratedBazelFileWarning + `

_a = {
    "b1": ["b2"],
}

_c = {
    "d1": ["d2"],
}

constants = struct(
    a = _a,
    c = _c,
)`,
		},
		{
			name: "exports dict with var refs",
			vars: ExportedVariables{
				exportedVariableReferenceDictVars: ExportedVariableReferenceDictVariables{
					"a": map[string]string{"b1": "${b2}"},
					"c": map[string]string{"d1": "${config.d2}"},
				},
			},
			expectedOut: bazel.GeneratedBazelFileWarning + `

_a = {
    "b1": _b2,
}

_c = {
    "d1": _d2,
}

constants = struct(
    a = _a,
    c = _c,
)`,
		},
		{
			name: "sorts across types with variable references last",
			vars: ExportedVariables{
				exportedStringVars: ExportedStringVariables{
					"b": "b-val",
					"d": "d-val",
				},
				exportedStringListVars: ExportedStringListVariables{
					"c": []string{"c-val"},
					"e": []string{"e-val"},
				},
				exportedStringListDictVars: ExportedStringListDictVariables{
					"a": map[string][]string{"a1": {"a2"}},
					"f": map[string][]string{"f1": {"f2"}},
				},
				exportedVariableReferenceDictVars: ExportedVariableReferenceDictVariables{
					"aa": map[string]string{"b1": "${b}"},
					"cc": map[string]string{"d1": "${config.d}"},
				},
			},
			expectedOut: bazel.GeneratedBazelFileWarning + `

_a = {
    "a1": ["a2"],
}

_b = "b-val"

_c = ["c-val"]

_d = "d-val"

_e = ["e-val"]

_f = {
    "f1": ["f2"],
}

_aa = {
    "b1": _b,
}

_cc = {
    "d1": _d,
}

constants = struct(
    a = _a,
    b = _b,
    c = _c,
    d = _d,
    e = _e,
    f = _f,
    aa = _aa,
    cc = _cc,
)`,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			out := BazelToolchainVars(tc.config, tc.vars)
			if out != tc.expectedOut {
				t.Errorf("Expected \n%s, got \n%s", tc.expectedOut, out)
			}
		})
	}
}

func TestSplitStringKeepingQuotedSubstring(t *testing.T) {
	testCases := []struct {
		description string
		s           string
		delimiter   byte
		split       []string
	}{
		{
			description: "empty string returns single empty string",
			s:           "",
			delimiter:   ' ',
			split: []string{
				"",
			},
		},
		{
			description: "string with single space returns two empty strings",
			s:           " ",
			delimiter:   ' ',
			split: []string{
				"",
				"",
			},
		},
		{
			description: "string with two spaces returns three empty strings",
			s:           "  ",
			delimiter:   ' ',
			split: []string{
				"",
				"",
				"",
			},
		},
		{
			description: "string with four words returns four word string",
			s:           "hello world with words",
			delimiter:   ' ',
			split: []string{
				"hello",
				"world",
				"with",
				"words",
			},
		},
		{
			description: "string with words and nested quote returns word strings and quote string",
			s:           `hello "world with" words`,
			delimiter:   ' ',
			split: []string{
				"hello",
				`"world with"`,
				"words",
			},
		},
		{
			description: "string with escaped quote inside real quotes",
			s:           `hello \"world "with\" words"`,
			delimiter:   ' ',
			split: []string{
				"hello",
				`"world`,
				`"with" words"`,
			},
		},
		{
			description: "string with words and escaped quotes returns word strings",
			s:           `hello \"world with\" words`,
			delimiter:   ' ',
			split: []string{
				"hello",
				`"world`,
				`with"`,
				"words",
			},
		},
		{
			description: "string which is single quoted substring returns only substring",
			s:           `"hello world with words"`,
			delimiter:   ' ',
			split: []string{
				`"hello world with words"`,
			},
		},
		{
			description: "string starting with quote returns quoted string",
			s:           `"hello world with" words`,
			delimiter:   ' ',
			split: []string{
				`"hello world with"`,
				"words",
			},
		},
		{
			description: "string with starting quote and no ending quote returns quote to end of string",
			s:           `hello "world with words`,
			delimiter:   ' ',
			split: []string{
				"hello",
				`"world with words`,
			},
		},
		{
			description: "quoted string is treated as a single \"word\" unless separated by delimiter",
			s:           `hello "world"with words`,
			delimiter:   ' ',
			split: []string{
				"hello",
				`"world"with`,
				"words",
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.description, func(t *testing.T) {
			split := splitStringKeepingQuotedSubstring(tc.s, tc.delimiter)
			if len(split) != len(tc.split) {
				t.Fatalf("number of split string elements (%d) differs from expected (%d): split string (%v), expected (%v)",
					len(split), len(tc.split), split, tc.split,
				)
			}
			for i := range split {
				if split[i] != tc.split[i] {
					t.Errorf("split string element (%d), %v, differs from expected, %v", i, split[i], tc.split[i])
				}
			}
		})
	}
}
+0 −4

File changed.

Preview size limit exceeded, changes collapsed.

Loading