| // Copyright 2014 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 blueprint |
| |
| import ( |
| "reflect" |
| "slices" |
| "strconv" |
| "strings" |
| "testing" |
| "unsafe" |
| ) |
| |
| type testVariableRef struct { |
| start, end int |
| name string |
| } |
| |
| func TestParseNinjaString(t *testing.T) { |
| testCases := []struct { |
| input string |
| vars []string |
| value string |
| eval string |
| err string |
| }{ |
| { |
| input: "abc def $ghi jkl", |
| vars: []string{"ghi"}, |
| value: "abc def ${namespace.ghi} jkl", |
| eval: "abc def GHI jkl", |
| }, |
| { |
| input: "abc def $ghi$jkl", |
| vars: []string{"ghi", "jkl"}, |
| value: "abc def ${namespace.ghi}${namespace.jkl}", |
| eval: "abc def GHIJKL", |
| }, |
| { |
| input: "foo $012_-345xyz_! bar", |
| vars: []string{"012_-345xyz_"}, |
| value: "foo ${namespace.012_-345xyz_}! bar", |
| eval: "foo 012_-345XYZ_! bar", |
| }, |
| { |
| input: "foo ${012_-345xyz_} bar", |
| vars: []string{"012_-345xyz_"}, |
| value: "foo ${namespace.012_-345xyz_} bar", |
| eval: "foo 012_-345XYZ_ bar", |
| }, |
| { |
| input: "foo ${012_-345xyz_} bar", |
| vars: []string{"012_-345xyz_"}, |
| value: "foo ${namespace.012_-345xyz_} bar", |
| eval: "foo 012_-345XYZ_ bar", |
| }, |
| { |
| input: "foo $$ bar", |
| vars: nil, |
| value: "foo $$ bar", |
| eval: "foo $$ bar", |
| }, |
| { |
| input: "$foo${bar}", |
| vars: []string{"foo", "bar"}, |
| value: "${namespace.foo}${namespace.bar}", |
| eval: "FOOBAR", |
| }, |
| { |
| input: "$foo$$", |
| vars: []string{"foo"}, |
| value: "${namespace.foo}$$", |
| eval: "FOO$$", |
| }, |
| { |
| input: "foo bar", |
| vars: nil, |
| value: "foo bar", |
| eval: "foo bar", |
| }, |
| { |
| input: " foo ", |
| vars: nil, |
| value: "$ foo ", |
| eval: "$ foo ", |
| }, |
| { |
| input: "\tfoo ", |
| vars: nil, |
| value: "\tfoo ", |
| eval: "\tfoo ", |
| }, |
| { |
| input: "\nfoo ", |
| vars: nil, |
| value: "$\nfoo ", |
| eval: "\nfoo ", |
| }, |
| { |
| input: " $foo ", |
| vars: []string{"foo"}, |
| value: "$ ${namespace.foo} ", |
| eval: " FOO ", |
| }, |
| { |
| input: "\t$foo ", |
| vars: []string{"foo"}, |
| value: "\t${namespace.foo} ", |
| eval: "\tFOO ", |
| }, |
| { |
| input: "\n$foo ", |
| vars: []string{"foo"}, |
| value: "$\n${namespace.foo} ", |
| eval: "\nFOO ", |
| }, |
| { |
| input: "foo $ bar", |
| err: `error parsing ninja string "foo $ bar": invalid character after '$' at byte offset 5`, |
| }, |
| { |
| input: "foo $", |
| err: "unexpected end of string after '$'", |
| }, |
| { |
| input: "foo ${} bar", |
| err: `error parsing ninja string "foo ${} bar": empty variable name at byte offset 6`, |
| }, |
| { |
| input: "foo ${abc!} bar", |
| err: `error parsing ninja string "foo ${abc!} bar": invalid character in variable name at byte offset 9`, |
| }, |
| { |
| input: "foo ${abc", |
| err: "unexpected end of string in variable name", |
| }, |
| } |
| |
| for _, testCase := range testCases { |
| t.Run(testCase.input, func(t *testing.T) { |
| scope := newLocalScope(nil, "namespace.") |
| variablesMap := map[Variable]*ninjaString{} |
| for _, varName := range testCase.vars { |
| _, err := scope.LookupVariable(varName) |
| if err != nil { |
| v, err := scope.AddLocalVariable(varName, strings.ToUpper(varName)) |
| if err != nil { |
| t.Fatalf("error creating scope: %s", err) |
| } |
| variablesMap[v] = simpleNinjaString(strings.ToUpper(varName)) |
| } |
| } |
| |
| output, err := parseNinjaString(scope, testCase.input) |
| if err == nil { |
| if g, w := output.Value(&nameTracker{}), testCase.value; g != w { |
| t.Errorf("incorrect Value output, want %q, got %q", w, g) |
| } |
| |
| eval, err := output.Eval(variablesMap) |
| if err != nil { |
| t.Errorf("unexpected error in Eval: %s", err) |
| } |
| if g, w := eval, testCase.eval; g != w { |
| t.Errorf("incorrect Eval output, want %q, got %q", w, g) |
| } |
| } |
| var errStr string |
| if err != nil { |
| errStr = err.Error() |
| } |
| if err != nil && err.Error() != testCase.err { |
| t.Errorf("unexpected error:") |
| t.Errorf(" input: %q", testCase.input) |
| t.Errorf(" expected: %q", testCase.err) |
| t.Errorf(" got: %q", errStr) |
| } |
| }) |
| } |
| } |
| |
| func TestParseNinjaStringWithImportedVar(t *testing.T) { |
| pctx := &packageContext{} |
| pkgNames := map[*packageContext]string{ |
| pctx: "impPkg", |
| } |
| ImpVar := &staticVariable{pctx: pctx, name_: "ImpVar"} |
| impScope := newScope(nil) |
| impScope.AddVariable(ImpVar) |
| scope := newScope(nil) |
| scope.AddImport("impPkg", impScope) |
| |
| input := "abc def ${impPkg.ImpVar} ghi" |
| output, err := parseNinjaString(scope, input) |
| if err != nil { |
| t.Fatalf("unexpected error: %s", err) |
| } |
| |
| expect := []variableReference{{8, 24, ImpVar}} |
| if !reflect.DeepEqual(*output.variables, expect) { |
| t.Errorf("incorrect output:") |
| t.Errorf(" input: %q", input) |
| t.Errorf(" expected: %#v", expect) |
| t.Errorf(" got: %#v", *output.variables) |
| } |
| |
| if g, w := output.Value(&nameTracker{pkgNames: pkgNames}), "abc def ${g.impPkg.ImpVar} ghi"; g != w { |
| t.Errorf("incorrect Value output, want %q got %q", w, g) |
| } |
| } |
| |
| func Test_parseNinjaOrSimpleStrings(t *testing.T) { |
| testCases := []struct { |
| name string |
| in []string |
| outStrings []string |
| outNinjaStrings []string |
| sameSlice bool |
| }{ |
| { |
| name: "nil", |
| in: nil, |
| sameSlice: true, |
| }, |
| { |
| name: "empty", |
| in: []string{}, |
| sameSlice: true, |
| }, |
| { |
| name: "string", |
| in: []string{"abc"}, |
| sameSlice: true, |
| }, |
| { |
| name: "ninja string", |
| in: []string{"$abc"}, |
| outStrings: nil, |
| outNinjaStrings: []string{"${abc}"}, |
| }, |
| { |
| name: "ninja string first", |
| in: []string{"$abc", "def", "ghi"}, |
| outStrings: []string{"def", "ghi"}, |
| outNinjaStrings: []string{"${abc}"}, |
| }, |
| { |
| name: "ninja string middle", |
| in: []string{"abc", "$def", "ghi"}, |
| outStrings: []string{"abc", "ghi"}, |
| outNinjaStrings: []string{"${def}"}, |
| }, |
| { |
| name: "ninja string last", |
| in: []string{"abc", "def", "$ghi"}, |
| outStrings: []string{"abc", "def"}, |
| outNinjaStrings: []string{"${ghi}"}, |
| }, |
| } |
| |
| for _, tt := range testCases { |
| t.Run(tt.name, func(t *testing.T) { |
| inCopy := slices.Clone(tt.in) |
| |
| scope := newLocalScope(nil, "") |
| scope.AddLocalVariable("abc", "abc") |
| scope.AddLocalVariable("def", "def") |
| scope.AddLocalVariable("ghi", "ghi") |
| gotNinjaStrings, gotStrings, err := parseNinjaOrSimpleStrings(scope, tt.in) |
| if err != nil { |
| t.Errorf("unexpected error %s", err) |
| } |
| |
| wantStrings := tt.outStrings |
| if tt.sameSlice { |
| wantStrings = tt.in |
| } |
| |
| wantNinjaStrings := tt.outNinjaStrings |
| |
| var evaluatedNinjaStrings []string |
| if gotNinjaStrings != nil { |
| evaluatedNinjaStrings = make([]string, 0, len(gotNinjaStrings)) |
| for _, ns := range gotNinjaStrings { |
| evaluatedNinjaStrings = append(evaluatedNinjaStrings, ns.Value(&nameTracker{})) |
| } |
| } |
| |
| if !reflect.DeepEqual(gotStrings, wantStrings) { |
| t.Errorf("incorrect strings output, want %q got %q", wantStrings, gotStrings) |
| } |
| if !reflect.DeepEqual(evaluatedNinjaStrings, wantNinjaStrings) { |
| t.Errorf("incorrect ninja strings output, want %q got %q", wantNinjaStrings, evaluatedNinjaStrings) |
| } |
| if len(inCopy) != len(tt.in) && (len(tt.in) == 0 || !reflect.DeepEqual(inCopy, tt.in)) { |
| t.Errorf("input modified, want %#v, got %#v", inCopy, tt.in) |
| } |
| |
| if (unsafe.SliceData(tt.in) == unsafe.SliceData(gotStrings)) != tt.sameSlice { |
| if tt.sameSlice { |
| t.Errorf("expected input and output slices to have the same backing arrays") |
| } else { |
| t.Errorf("expected input and output slices to have different backing arrays") |
| } |
| } |
| |
| }) |
| } |
| } |
| |
| func Benchmark_parseNinjaString(b *testing.B) { |
| b.Run("constant", func(b *testing.B) { |
| for _, l := range []int{1, 10, 100, 1000} { |
| b.Run(strconv.Itoa(l), func(b *testing.B) { |
| b.ReportAllocs() |
| for n := 0; n < b.N; n++ { |
| _ = simpleNinjaString(strings.Repeat("a", l)) |
| } |
| }) |
| } |
| }) |
| b.Run("variable", func(b *testing.B) { |
| for _, l := range []int{1, 10, 100, 1000} { |
| scope := newLocalScope(nil, "") |
| scope.AddLocalVariable("a", strings.Repeat("b", l/3)) |
| b.Run(strconv.Itoa(l), func(b *testing.B) { |
| b.ReportAllocs() |
| for n := 0; n < b.N; n++ { |
| _, _ = parseNinjaString(scope, strings.Repeat("a", l/3)+"${a}"+strings.Repeat("a", l/3)) |
| } |
| }) |
| } |
| }) |
| b.Run("variables", func(b *testing.B) { |
| for _, l := range []int{1, 2, 3, 4, 5, 10, 100, 1000} { |
| scope := newLocalScope(nil, "") |
| str := strings.Repeat("a", 10) |
| for i := 0; i < l; i++ { |
| scope.AddLocalVariable("a"+strconv.Itoa(i), strings.Repeat("b", 10)) |
| str += "${a" + strconv.Itoa(i) + "}" |
| } |
| b.Run(strconv.Itoa(l), func(b *testing.B) { |
| b.ReportAllocs() |
| for n := 0; n < b.N; n++ { |
| _, _ = parseNinjaString(scope, str) |
| } |
| }) |
| } |
| }) |
| |
| } |
| |
| func BenchmarkNinjaString_Value(b *testing.B) { |
| b.Run("constant", func(b *testing.B) { |
| for _, l := range []int{1, 10, 100, 1000} { |
| ns := simpleNinjaString(strings.Repeat("a", l)) |
| b.Run(strconv.Itoa(l), func(b *testing.B) { |
| b.ReportAllocs() |
| for n := 0; n < b.N; n++ { |
| ns.Value(&nameTracker{}) |
| } |
| }) |
| } |
| }) |
| b.Run("variable", func(b *testing.B) { |
| for _, l := range []int{1, 10, 100, 1000} { |
| scope := newLocalScope(nil, "") |
| scope.AddLocalVariable("a", strings.Repeat("b", l/3)) |
| ns, _ := parseNinjaString(scope, strings.Repeat("a", l/3)+"${a}"+strings.Repeat("a", l/3)) |
| b.Run(strconv.Itoa(l), func(b *testing.B) { |
| b.ReportAllocs() |
| for n := 0; n < b.N; n++ { |
| ns.Value(&nameTracker{}) |
| } |
| }) |
| } |
| }) |
| b.Run("variables", func(b *testing.B) { |
| for _, l := range []int{1, 2, 3, 4, 5, 10, 100, 1000} { |
| scope := newLocalScope(nil, "") |
| str := strings.Repeat("a", 10) |
| for i := 0; i < l; i++ { |
| scope.AddLocalVariable("a"+strconv.Itoa(i), strings.Repeat("b", 10)) |
| str += "${a" + strconv.Itoa(i) + "}" |
| } |
| ns, _ := parseNinjaString(scope, str) |
| b.Run(strconv.Itoa(l), func(b *testing.B) { |
| b.ReportAllocs() |
| for n := 0; n < b.N; n++ { |
| ns.Value(&nameTracker{}) |
| } |
| }) |
| } |
| }) |
| |
| } |