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

Commit 8460fab9 authored by Colin Cross's avatar Colin Cross Committed by Gerrit Code Review
Browse files

Merge "Remove more bp2build" into main

parents 208444ce 3cc3154d
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