1// Copyright 2014 Google Inc. All rights reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package blueprint 16 17import ( 18 "reflect" 19 "slices" 20 "strconv" 21 "strings" 22 "testing" 23 "unsafe" 24) 25 26type testVariableRef struct { 27 start, end int 28 name string 29} 30 31func TestParseNinjaString(t *testing.T) { 32 testCases := []struct { 33 input string 34 vars []string 35 value string 36 eval string 37 err string 38 }{ 39 { 40 input: "abc def $ghi jkl", 41 vars: []string{"ghi"}, 42 value: "abc def ${namespace.ghi} jkl", 43 eval: "abc def GHI jkl", 44 }, 45 { 46 input: "abc def $ghi$jkl", 47 vars: []string{"ghi", "jkl"}, 48 value: "abc def ${namespace.ghi}${namespace.jkl}", 49 eval: "abc def GHIJKL", 50 }, 51 { 52 input: "foo $012_-345xyz_! bar", 53 vars: []string{"012_-345xyz_"}, 54 value: "foo ${namespace.012_-345xyz_}! bar", 55 eval: "foo 012_-345XYZ_! bar", 56 }, 57 { 58 input: "foo ${012_-345xyz_} bar", 59 vars: []string{"012_-345xyz_"}, 60 value: "foo ${namespace.012_-345xyz_} bar", 61 eval: "foo 012_-345XYZ_ bar", 62 }, 63 { 64 input: "foo ${012_-345xyz_} bar", 65 vars: []string{"012_-345xyz_"}, 66 value: "foo ${namespace.012_-345xyz_} bar", 67 eval: "foo 012_-345XYZ_ bar", 68 }, 69 { 70 input: "foo $$ bar", 71 vars: nil, 72 value: "foo $$ bar", 73 eval: "foo $$ bar", 74 }, 75 { 76 input: "$foo${bar}", 77 vars: []string{"foo", "bar"}, 78 value: "${namespace.foo}${namespace.bar}", 79 eval: "FOOBAR", 80 }, 81 { 82 input: "$foo$$", 83 vars: []string{"foo"}, 84 value: "${namespace.foo}$$", 85 eval: "FOO$$", 86 }, 87 { 88 input: "foo bar", 89 vars: nil, 90 value: "foo bar", 91 eval: "foo bar", 92 }, 93 { 94 input: " foo ", 95 vars: nil, 96 value: "$ foo ", 97 eval: "$ foo ", 98 }, 99 { 100 input: "\tfoo ", 101 vars: nil, 102 value: "\tfoo ", 103 eval: "\tfoo ", 104 }, 105 { 106 input: "\nfoo ", 107 vars: nil, 108 value: "$\nfoo ", 109 eval: "\nfoo ", 110 }, 111 { 112 input: " $foo ", 113 vars: []string{"foo"}, 114 value: "$ ${namespace.foo} ", 115 eval: " FOO ", 116 }, 117 { 118 input: "\t$foo ", 119 vars: []string{"foo"}, 120 value: "\t${namespace.foo} ", 121 eval: "\tFOO ", 122 }, 123 { 124 input: "\n$foo ", 125 vars: []string{"foo"}, 126 value: "$\n${namespace.foo} ", 127 eval: "\nFOO ", 128 }, 129 { 130 input: "foo $ bar", 131 err: `error parsing ninja string "foo $ bar": invalid character after '$' at byte offset 5`, 132 }, 133 { 134 input: "foo $", 135 err: "unexpected end of string after '$'", 136 }, 137 { 138 input: "foo ${} bar", 139 err: `error parsing ninja string "foo ${} bar": empty variable name at byte offset 6`, 140 }, 141 { 142 input: "foo ${abc!} bar", 143 err: `error parsing ninja string "foo ${abc!} bar": invalid character in variable name at byte offset 9`, 144 }, 145 { 146 input: "foo ${abc", 147 err: "unexpected end of string in variable name", 148 }, 149 } 150 151 for _, testCase := range testCases { 152 t.Run(testCase.input, func(t *testing.T) { 153 scope := newLocalScope(nil, "namespace.") 154 variablesMap := map[Variable]*ninjaString{} 155 for _, varName := range testCase.vars { 156 _, err := scope.LookupVariable(varName) 157 if err != nil { 158 v, err := scope.AddLocalVariable(varName, strings.ToUpper(varName)) 159 if err != nil { 160 t.Fatalf("error creating scope: %s", err) 161 } 162 variablesMap[v] = simpleNinjaString(strings.ToUpper(varName)) 163 } 164 } 165 166 output, err := parseNinjaString(scope, testCase.input) 167 if err == nil { 168 if g, w := output.Value(&nameTracker{}), testCase.value; g != w { 169 t.Errorf("incorrect Value output, want %q, got %q", w, g) 170 } 171 172 eval, err := output.Eval(variablesMap) 173 if err != nil { 174 t.Errorf("unexpected error in Eval: %s", err) 175 } 176 if g, w := eval, testCase.eval; g != w { 177 t.Errorf("incorrect Eval output, want %q, got %q", w, g) 178 } 179 } 180 var errStr string 181 if err != nil { 182 errStr = err.Error() 183 } 184 if err != nil && err.Error() != testCase.err { 185 t.Errorf("unexpected error:") 186 t.Errorf(" input: %q", testCase.input) 187 t.Errorf(" expected: %q", testCase.err) 188 t.Errorf(" got: %q", errStr) 189 } 190 }) 191 } 192} 193 194func TestParseNinjaStringWithImportedVar(t *testing.T) { 195 pctx := &packageContext{} 196 pkgNames := map[*packageContext]string{ 197 pctx: "impPkg", 198 } 199 ImpVar := &staticVariable{pctx: pctx, name_: "ImpVar"} 200 impScope := newScope(nil) 201 impScope.AddVariable(ImpVar) 202 scope := newScope(nil) 203 scope.AddImport("impPkg", impScope) 204 205 input := "abc def ${impPkg.ImpVar} ghi" 206 output, err := parseNinjaString(scope, input) 207 if err != nil { 208 t.Fatalf("unexpected error: %s", err) 209 } 210 211 expect := []variableReference{{8, 24, ImpVar}} 212 if !reflect.DeepEqual(*output.variables, expect) { 213 t.Errorf("incorrect output:") 214 t.Errorf(" input: %q", input) 215 t.Errorf(" expected: %#v", expect) 216 t.Errorf(" got: %#v", *output.variables) 217 } 218 219 if g, w := output.Value(&nameTracker{pkgNames: pkgNames}), "abc def ${g.impPkg.ImpVar} ghi"; g != w { 220 t.Errorf("incorrect Value output, want %q got %q", w, g) 221 } 222} 223 224func Test_parseNinjaOrSimpleStrings(t *testing.T) { 225 testCases := []struct { 226 name string 227 in []string 228 outStrings []string 229 outNinjaStrings []string 230 sameSlice bool 231 }{ 232 { 233 name: "nil", 234 in: nil, 235 sameSlice: true, 236 }, 237 { 238 name: "empty", 239 in: []string{}, 240 sameSlice: true, 241 }, 242 { 243 name: "string", 244 in: []string{"abc"}, 245 sameSlice: true, 246 }, 247 { 248 name: "ninja string", 249 in: []string{"$abc"}, 250 outStrings: nil, 251 outNinjaStrings: []string{"${abc}"}, 252 }, 253 { 254 name: "ninja string first", 255 in: []string{"$abc", "def", "ghi"}, 256 outStrings: []string{"def", "ghi"}, 257 outNinjaStrings: []string{"${abc}"}, 258 }, 259 { 260 name: "ninja string middle", 261 in: []string{"abc", "$def", "ghi"}, 262 outStrings: []string{"abc", "ghi"}, 263 outNinjaStrings: []string{"${def}"}, 264 }, 265 { 266 name: "ninja string last", 267 in: []string{"abc", "def", "$ghi"}, 268 outStrings: []string{"abc", "def"}, 269 outNinjaStrings: []string{"${ghi}"}, 270 }, 271 } 272 273 for _, tt := range testCases { 274 t.Run(tt.name, func(t *testing.T) { 275 inCopy := slices.Clone(tt.in) 276 277 scope := newLocalScope(nil, "") 278 scope.AddLocalVariable("abc", "abc") 279 scope.AddLocalVariable("def", "def") 280 scope.AddLocalVariable("ghi", "ghi") 281 gotNinjaStrings, gotStrings, err := parseNinjaOrSimpleStrings(scope, tt.in) 282 if err != nil { 283 t.Errorf("unexpected error %s", err) 284 } 285 286 wantStrings := tt.outStrings 287 if tt.sameSlice { 288 wantStrings = tt.in 289 } 290 291 wantNinjaStrings := tt.outNinjaStrings 292 293 var evaluatedNinjaStrings []string 294 if gotNinjaStrings != nil { 295 evaluatedNinjaStrings = make([]string, 0, len(gotNinjaStrings)) 296 for _, ns := range gotNinjaStrings { 297 evaluatedNinjaStrings = append(evaluatedNinjaStrings, ns.Value(&nameTracker{})) 298 } 299 } 300 301 if !reflect.DeepEqual(gotStrings, wantStrings) { 302 t.Errorf("incorrect strings output, want %q got %q", wantStrings, gotStrings) 303 } 304 if !reflect.DeepEqual(evaluatedNinjaStrings, wantNinjaStrings) { 305 t.Errorf("incorrect ninja strings output, want %q got %q", wantNinjaStrings, evaluatedNinjaStrings) 306 } 307 if len(inCopy) != len(tt.in) && (len(tt.in) == 0 || !reflect.DeepEqual(inCopy, tt.in)) { 308 t.Errorf("input modified, want %#v, got %#v", inCopy, tt.in) 309 } 310 311 if (unsafe.SliceData(tt.in) == unsafe.SliceData(gotStrings)) != tt.sameSlice { 312 if tt.sameSlice { 313 t.Errorf("expected input and output slices to have the same backing arrays") 314 } else { 315 t.Errorf("expected input and output slices to have different backing arrays") 316 } 317 } 318 319 }) 320 } 321} 322 323func Benchmark_parseNinjaString(b *testing.B) { 324 b.Run("constant", func(b *testing.B) { 325 for _, l := range []int{1, 10, 100, 1000} { 326 b.Run(strconv.Itoa(l), func(b *testing.B) { 327 b.ReportAllocs() 328 for n := 0; n < b.N; n++ { 329 _ = simpleNinjaString(strings.Repeat("a", l)) 330 } 331 }) 332 } 333 }) 334 b.Run("variable", func(b *testing.B) { 335 for _, l := range []int{1, 10, 100, 1000} { 336 scope := newLocalScope(nil, "") 337 scope.AddLocalVariable("a", strings.Repeat("b", l/3)) 338 b.Run(strconv.Itoa(l), func(b *testing.B) { 339 b.ReportAllocs() 340 for n := 0; n < b.N; n++ { 341 _, _ = parseNinjaString(scope, strings.Repeat("a", l/3)+"${a}"+strings.Repeat("a", l/3)) 342 } 343 }) 344 } 345 }) 346 b.Run("variables", func(b *testing.B) { 347 for _, l := range []int{1, 2, 3, 4, 5, 10, 100, 1000} { 348 scope := newLocalScope(nil, "") 349 str := strings.Repeat("a", 10) 350 for i := 0; i < l; i++ { 351 scope.AddLocalVariable("a"+strconv.Itoa(i), strings.Repeat("b", 10)) 352 str += "${a" + strconv.Itoa(i) + "}" 353 } 354 b.Run(strconv.Itoa(l), func(b *testing.B) { 355 b.ReportAllocs() 356 for n := 0; n < b.N; n++ { 357 _, _ = parseNinjaString(scope, str) 358 } 359 }) 360 } 361 }) 362 363} 364 365func BenchmarkNinjaString_Value(b *testing.B) { 366 b.Run("constant", func(b *testing.B) { 367 for _, l := range []int{1, 10, 100, 1000} { 368 ns := simpleNinjaString(strings.Repeat("a", l)) 369 b.Run(strconv.Itoa(l), func(b *testing.B) { 370 b.ReportAllocs() 371 for n := 0; n < b.N; n++ { 372 ns.Value(&nameTracker{}) 373 } 374 }) 375 } 376 }) 377 b.Run("variable", func(b *testing.B) { 378 for _, l := range []int{1, 10, 100, 1000} { 379 scope := newLocalScope(nil, "") 380 scope.AddLocalVariable("a", strings.Repeat("b", l/3)) 381 ns, _ := parseNinjaString(scope, strings.Repeat("a", l/3)+"${a}"+strings.Repeat("a", l/3)) 382 b.Run(strconv.Itoa(l), func(b *testing.B) { 383 b.ReportAllocs() 384 for n := 0; n < b.N; n++ { 385 ns.Value(&nameTracker{}) 386 } 387 }) 388 } 389 }) 390 b.Run("variables", func(b *testing.B) { 391 for _, l := range []int{1, 2, 3, 4, 5, 10, 100, 1000} { 392 scope := newLocalScope(nil, "") 393 str := strings.Repeat("a", 10) 394 for i := 0; i < l; i++ { 395 scope.AddLocalVariable("a"+strconv.Itoa(i), strings.Repeat("b", 10)) 396 str += "${a" + strconv.Itoa(i) + "}" 397 } 398 ns, _ := parseNinjaString(scope, str) 399 b.Run(strconv.Itoa(l), func(b *testing.B) { 400 b.ReportAllocs() 401 for n := 0; n < b.N; n++ { 402 ns.Value(&nameTracker{}) 403 } 404 }) 405 } 406 }) 407 408} 409