xref: /aosp_15_r20/external/bazelbuild-rules_android/src/tools/ak/bucketize/bucketize_test.go (revision 9e965d6fece27a77de5377433c2f7e6999b8cc0b)
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