1// Copyright 2018 The Bazel Authors. 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 bucketize 16 17import ( 18 "bytes" 19 "context" 20 "encoding/xml" 21 "fmt" 22 "io" 23 "io/ioutil" 24 "os" 25 "path" 26 "reflect" 27 "strings" 28 "testing" 29 30 "src/common/golang/shard" 31 "src/common/golang/walk" 32 "src/tools/ak/res/res" 33) 34 35func TestNormalizeResPaths(t *testing.T) { 36 // Create a temporary directory to house the fake workspace. 37 tmp, err := ioutil.TempDir("", "") 38 if err != nil { 39 t.Fatalf("Can't make temp directory: %v", err) 40 } 41 defer os.RemoveAll(tmp) 42 43 var resPaths []string 44 fp1 := path.Join(tmp, "foo") 45 _, err = os.Create(fp1) 46 if err != nil { 47 t.Fatalf("Got error while trying to create %s: %v", fp1, err) 48 } 49 resPaths = append(resPaths, fp1) 50 51 dp1 := path.Join(tmp, "bar", "baz", "qux") 52 if err != os.MkdirAll(dp1, 0777) { 53 t.Fatalf("Got error while trying to create %s: %v", dp1, err) 54 } 55 resPaths = append(resPaths, dp1) 56 57 // Create a file nested in the directory that is passed in as a resPath. This file will get 58 // injected between fp1 and fp3 because the directory is defined in the middle. Hence, 59 // files added to the directory will appear between fp1 and fp3. This behavior is intended. 60 fInDP1 := path.Join(dp1, "quux") 61 _, err = os.Create(fInDP1) 62 if err != nil { 63 t.Fatalf("Got error while trying to create %s: %v", fInDP1, err) 64 } 65 66 fp3 := path.Join(tmp, "bar", "corge") 67 _, err = os.Create(fp3) 68 if err != nil { 69 t.Fatalf("Got error while trying to create %s: %v", fp3, err) 70 } 71 resPaths = append(resPaths, fp3) 72 73 gotFiles, err := walk.Files(resPaths) 74 if err != nil { 75 t.Fatalf("Got error getting the resource paths: %v", err) 76 } 77 gotFileIdxs := make(map[string]int) 78 for i, gotFile := range gotFiles { 79 gotFileIdxs[gotFile] = i 80 } 81 82 wantFiles := []string{fp1, fInDP1, fp3} 83 if !reflect.DeepEqual(gotFiles, wantFiles) { 84 t.Errorf("DeepEqual(\n%#v\n,\n%#v\n): returned false", gotFiles, wantFiles) 85 } 86 87 wantFileIdxs := map[string]int{fp1: 0, fInDP1: 1, fp3: 2} 88 if !reflect.DeepEqual(gotFileIdxs, wantFileIdxs) { 89 t.Errorf("DeepEqual(\n%#v\n,\n%#v\n): returned false", gotFileIdxs, wantFileIdxs) 90 } 91} 92 93func TestArchiverWithPartitionSession(t *testing.T) { 94 order := make(map[string]int) 95 ps, err := makePartitionSession(map[res.Type][]io.Writer{}, shard.FNV, order) 96 if err != nil { 97 t.Fatalf("MakePartitionSesion got err: %v", err) 98 } 99 if _, err := makeArchiver([]string{}, ps); err != nil { 100 t.Errorf("MakeArchiver got err: %v", err) 101 } 102} 103 104func TestArchiveNoValues(t *testing.T) { 105 ctx, cxlFn := context.WithCancel(context.Background()) 106 defer cxlFn() 107 a, err := makeArchiver([]string{}, &mockPartitioner{}) 108 if err != nil { 109 t.Fatalf("MakeArchiver got error: %v", err) 110 } 111 a.Archive(ctx) 112} 113 114func TestInternalArchive(t *testing.T) { 115 tcs := []struct { 116 name string 117 p Partitioner 118 pis []*res.PathInfo 119 vrs []*res.ValuesResource 120 ras []ResourcesAttribute 121 errs []error 122 wantErr bool 123 }{ 124 { 125 name: "MultipleResPathInfosAndValuesResources", 126 p: &mockPartitioner{}, 127 pis: []*res.PathInfo{{Path: "foo"}}, 128 vrs: []*res.ValuesResource{ 129 {Src: &res.PathInfo{Path: "bar"}}, 130 {Src: &res.PathInfo{Path: "baz"}}, 131 }, 132 errs: []error{}, 133 }, 134 { 135 name: "NoValues", 136 p: &mockPartitioner{}, 137 pis: []*res.PathInfo{}, 138 vrs: []*res.ValuesResource{}, 139 errs: []error{}, 140 }, 141 { 142 name: "ErrorOccurred", 143 p: &mockPartitioner{}, 144 pis: []*res.PathInfo{{Path: "foo"}}, 145 vrs: []*res.ValuesResource{}, 146 errs: []error{fmt.Errorf("failure")}, 147 wantErr: true, 148 }, 149 } 150 151 for _, tc := range tcs { 152 t.Run(tc.name, func(t *testing.T) { 153 piC := make(chan *res.PathInfo) 154 go func() { 155 defer close(piC) 156 for _, pi := range tc.pis { 157 piC <- pi 158 } 159 }() 160 vrC := make(chan *res.ValuesResource) 161 go func() { 162 defer close(vrC) 163 for _, vr := range tc.vrs { 164 vrC <- vr 165 } 166 }() 167 raC := make(chan *ResourcesAttribute) 168 go func() { 169 defer close(raC) 170 for _, ra := range tc.ras { 171 nra := new(ResourcesAttribute) 172 *nra = ra 173 raC <- nra 174 } 175 }() 176 errC := make(chan error) 177 go func() { 178 defer close(errC) 179 for _, err := range tc.errs { 180 errC <- err 181 } 182 }() 183 a, err := makeArchiver([]string{}, tc.p) 184 if err != nil { 185 t.Errorf("MakeArchiver got error: %v", err) 186 return 187 } 188 ctx, cxlFn := context.WithCancel(context.Background()) 189 defer cxlFn() 190 if err := a.archive(ctx, piC, vrC, raC, errC); err != nil { 191 if !tc.wantErr { 192 t.Errorf("archive got unexpected error: %v", err) 193 } 194 return 195 } 196 }) 197 } 198} 199 200func TestSyncParseReader(t *testing.T) { 201 tcs := []struct { 202 name string 203 pi *res.PathInfo 204 content *bytes.Buffer 205 want map[string]string 206 wantErr bool 207 }{ 208 { 209 name: "SingleResourcesBlock", 210 pi: &res.PathInfo{}, 211 content: bytes.NewBufferString(`<resources> 212 <string name="introduction">hello world</string> 213 <string name="foo">bar</string> 214 <attr name="baz" format="reference|color"></attr> 215 </resources>`), 216 want: map[string]string{ 217 "introduction-string": "<string name=\"introduction\">hello world</string>", 218 "foo-string": "<string name=\"foo\">bar</string>", 219 "baz-attr": "<attr name=\"baz\" format=\"reference|color\"></attr>", 220 }, 221 }, 222 { 223 name: "MultipleResourcesBlocks", 224 pi: &res.PathInfo{}, 225 content: bytes.NewBufferString(`<resources> 226 <string name="introduction">hello world</string> 227 <string name="foo">bar</string> 228 </resources> 229 <!-- 230 Subsequent resources sections are ignored, hence the "qux" item will not 231 materialize in the parsed values. 232 --> 233 <resources> 234 <item name="qux" type="integer">23</item> 235 </resources>`), 236 want: map[string]string{ 237 "introduction-string": "<string name=\"introduction\">hello world</string>", 238 "foo-string": "<string name=\"foo\">bar</string>", 239 }, 240 }, 241 { 242 name: "NamespacedResourcesBlock", 243 pi: &res.PathInfo{}, 244 content: bytes.NewBufferString(`<resources xmlns:foo="bar"> 245 <string name="namespaced"><foo:bar>hello</foo:bar> world</string> 246 </resources>`), 247 want: map[string]string{ 248 "resource_attribute-xmlns:foo": "bar", 249 "namespaced-string": "<string name=\"namespaced\"><foo:bar>hello</foo:bar> world</string>", 250 }, 251 }, 252 { 253 name: "DeclareStyleable", 254 pi: &res.PathInfo{}, 255 content: bytes.NewBufferString("<resources><declare-styleable name=\"foo\"><attr name=\"bar\">baz</attr></declare-styleable></resources>"), 256 want: map[string]string{ 257 "foo-styleable": "<declare-styleable name=\"foo\"><attr name=\"bar\">baz</attr></declare-styleable>", 258 "bar-attr": "<attr name=\"bar\">baz</attr>", 259 }, 260 }, 261 { 262 name: "NamespacedStyleableBlock", 263 pi: &res.PathInfo{}, 264 content: bytes.NewBufferString("<resources xmlns:zoo=\"zoo\"><declare-styleable name=\"foo\"><attr name=\"bar\" zoo:qux=\"rux\">baz</attr></declare-styleable></resources>"), 265 want: map[string]string{ 266 "resource_attribute-xmlns:zoo": "zoo", 267 "foo-styleable": "<declare-styleable name=\"foo\"><attr name=\"bar\" zoo:qux=\"rux\">baz</attr></declare-styleable>", 268 "bar-attr": "<attr name=\"bar\" zoo:qux=\"rux\">baz</attr>", 269 }, 270 }, 271 { 272 name: "PluralsStringArrayOutputToStringToo", 273 pi: &res.PathInfo{}, 274 content: bytes.NewBufferString(`<resources> 275 <string-array name="foo"><item>bar</item><item>baz</item></string-array> 276 <plurals name="corge"><item quantity="one">qux</item><item quantity="other">quux</item></plurals> 277 </resources>`), 278 want: map[string]string{ 279 "foo-array": "<string-array name=\"foo\"><item>bar</item><item>baz</item></string-array>", 280 "corge-plurals": "<plurals name=\"corge\"><item quantity=\"one\">qux</item><item quantity=\"other\">quux</item></plurals>", 281 }, 282 }, 283 { 284 name: "AttrWithFlagOrEnumChildren", 285 pi: &res.PathInfo{}, 286 content: bytes.NewBufferString(`<resources> 287 <attr name="foo"><enum name="bar" value="0" /><enum name="baz" value="10" /></attr> 288 <attr name="qux"><flag name="quux" value="0x4" /></attr> 289 </resources>`), 290 want: map[string]string{ 291 "foo-attr": "<attr name=\"foo\"><enum name=\"bar\" value=\"0\"></enum><enum name=\"baz\" value=\"10\"></enum></attr>", 292 "qux-attr": "<attr name=\"qux\"><flag name=\"quux\" value=\"0x4\"></flag></attr>", 293 }, 294 }, 295 { 296 name: "Style", 297 pi: &res.PathInfo{}, 298 content: bytes.NewBufferString(`<resources> 299 <style name="foo"><item>bar</item><item>baz</item></style> 300 </resources>`), 301 want: map[string]string{ 302 "foo-style": "<style name=\"foo\"><item>bar</item><item>baz</item></style>", 303 }, 304 }, 305 { 306 name: "ArraysGoToStingAndInteger", 307 pi: &res.PathInfo{}, 308 content: bytes.NewBufferString(`<resources> 309 <array name="foo"><item>bar</item><item>1</item></array> 310 </resources>`), 311 want: map[string]string{ 312 "foo-array": "<array name=\"foo\"><item>bar</item><item>1</item></array>", 313 }, 314 }, 315 { 316 name: "NoContent", 317 pi: &res.PathInfo{}, 318 content: &bytes.Buffer{}, 319 want: map[string]string{}, 320 }, 321 { 322 name: "EmptyResources", 323 pi: &res.PathInfo{}, 324 content: bytes.NewBufferString("<resources></resources>"), 325 want: map[string]string{}, 326 }, 327 { 328 name: "IgnoredContent", 329 pi: &res.PathInfo{}, 330 content: bytes.NewBufferString(` 331 <!--ignore my comment--> 332 <ignore_tag /> 333 ignore random string. 334 <resources> 335 <!--ignore this comment too--> 336 <attr name="foo">bar<baz>qux</baz></attr> 337 ignore this random string too. 338 <!-- following are a list of ignored tags --> 339 <eat-comment /> 340 <skip /> 341 </resources>`), 342 want: map[string]string{ 343 "foo-attr": "<attr name=\"foo\">bar<baz>qux</baz></attr>", 344 }, 345 }, 346 { 347 name: "TagMissingNameAttribute", 348 pi: &res.PathInfo{}, 349 content: bytes.NewBufferString(`<resources><string>MissingNameAttr</string></resources>`), 350 wantErr: true, 351 }, 352 { 353 name: "ItemTagMissingTypeAttribute", 354 pi: &res.PathInfo{}, 355 content: bytes.NewBufferString(`<resources><item name="MissingTypeAttr">bar</item></resources>`), 356 wantErr: true, 357 }, 358 { 359 name: "ItemTagUnknownTypeAttribute", 360 pi: &res.PathInfo{}, 361 content: bytes.NewBufferString(`<resources><item name="UnknownType" type="foo" /></resources>`), 362 wantErr: true, 363 }, 364 { 365 name: "UnhandledTag", 366 pi: &res.PathInfo{}, 367 content: bytes.NewBufferString(`<resources><foo name="bar"/></resources>`), 368 wantErr: true, 369 }, 370 { 371 name: "MalFormedXml_OpenResourcesTag", 372 pi: &res.PathInfo{}, 373 content: bytes.NewBufferString(`<resources>`), 374 wantErr: true, 375 }, 376 { 377 name: "MalFormedXml_Unabalanced", 378 pi: &res.PathInfo{}, 379 content: bytes.NewBufferString(`<resources><attr name="unbalanced"><b></attr></resources>`), 380 wantErr: true, 381 }, 382 { 383 name: "NamespaceUsedWithoutNamespaceDefinition", 384 pi: &res.PathInfo{}, 385 content: bytes.NewBufferString(`<resources><string name="ohno"><bad:b>Oh no!</bad:b></string></resources>`), 386 wantErr: true, 387 }, 388 { 389 // Verify parent Encoder is properly shadowing the xml file. 390 name: "NamespaceUsedOutsideOfDefinition", 391 pi: &res.PathInfo{}, 392 content: bytes.NewBufferString(` 393 <resources> 394 <string name="foo" xmlns:bar="baz">qux</string> 395 <string name="ohno"><foo:b>Oh no!</foo:b></string> 396 </resources>`), 397 wantErr: true, 398 }, 399 } 400 for _, tc := range tcs { 401 t.Run(tc.name, func(t *testing.T) { 402 ctx, cxlFn := context.WithCancel(context.Background()) 403 defer cxlFn() 404 vrC := make(chan *res.ValuesResource) 405 raC := make(chan *ResourcesAttribute) 406 errC := make(chan error) 407 go func() { 408 defer close(vrC) 409 defer close(raC) 410 defer close(errC) 411 syncParseReader(ctx, tc.pi, xml.NewDecoder(tc.content), vrC, raC, errC) 412 }() 413 got := make(map[string]string) 414 errMs := make([]string, 0) 415 for errC != nil || vrC != nil { 416 select { 417 case e, ok := <-errC: 418 if !ok { 419 errC = nil 420 } 421 if e != nil { 422 errMs = append(errMs, e.Error()) 423 } 424 case ra, ok := <-raC: 425 if !ok { 426 raC = nil 427 } 428 if ra != nil { 429 a := ra.Attribute 430 got[fmt.Sprintf("resource_attribute-%s:%s", a.Name.Space, a.Name.Local)] = a.Value 431 } 432 case vr, ok := <-vrC: 433 if !ok { 434 vrC = nil 435 } 436 if vr != nil { 437 got[fmt.Sprintf("%s-%s", vr.N.Name, vr.N.Type.String())] = string(vr.Payload) 438 } 439 } 440 } 441 442 // error handling 443 if tc.wantErr { 444 if len(errMs) == 0 { 445 t.Errorf("syncParseReader expected an error.") 446 } 447 return 448 } 449 if len(errMs) > 0 { 450 t.Errorf("syncParserReader got unexpected error(s): \n%s", strings.Join(errMs, "\n")) 451 return 452 } 453 454 if !reflect.DeepEqual(got, tc.want) { 455 t.Errorf("DeepEqual(\n%#v\n,\n%#v\n): returned false", got, tc.want) 456 } 457 }) 458 } 459} 460 461// mockPartitioner is a Partitioner mock used for testing. 462type mockPartitioner struct { 463 strPI []res.PathInfo 464 cvVR []res.ValuesResource 465 ra []*ResourcesAttribute 466} 467 468func (mp *mockPartitioner) Close() error { 469 return nil 470} 471 472func (mp *mockPartitioner) CollectPathResource(src res.PathInfo) { 473 mp.strPI = append(mp.strPI, src) 474} 475 476func (mp *mockPartitioner) CollectValues(vr *res.ValuesResource) error { 477 mp.cvVR = append(mp.cvVR, res.ValuesResource{vr.Src, vr.N, vr.Payload}) 478 return nil 479} 480 481func (mp *mockPartitioner) CollectResourcesAttribute(ra *ResourcesAttribute) { 482 mp.ra = append(mp.ra, ra) 483} 484