1// Copyright 2022 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 15// Package finalrjar generates a valid final R.jar. 16package finalrjar 17 18import ( 19 "bytes" 20 "sort" 21 "strings" 22 "testing" 23 24 "github.com/google/go-cmp/cmp" 25) 26 27type fakeFile struct { 28 reader *strings.Reader 29} 30 31func (f fakeFile) Read(b []byte) (int, error) { 32 return f.reader.Read(b) 33} 34 35func (f fakeFile) Close() error { 36 return nil 37} 38 39func TestGetIds(t *testing.T) { 40 tests := []struct { 41 name string 42 rtxtFiles []*strings.Reader 43 expectedResources []*resource 44 }{ 45 { 46 name: "one R.txt", 47 rtxtFiles: []*strings.Reader{ 48 strings.NewReader( 49 `int anim abc_fade_in 0 50int anim abc_fade_out 0 51int attr actionBarDivider 0 52int bool abc_action_bar_embed_tabs 0 53int color abc_background_cache_hint_selector_material_dark 0 54int[] color abc_background_cache_hint_selector_material_light 0 55int color abc_btn_colored_borderless_text_material 0 56int dimen tooltip_y_offset_non_touch 0 57int dimen $avd_hide_password__0 0 58int[] dimen tooltip_y_offset_touch 0 59int drawable abc_ab_share_pack_mtrl_alpha 0`), 60 }, 61 expectedResources: []*resource{ 62 &resource{ID: "abc_ab_share_pack_mtrl_alpha", resType: "drawable", varType: "int"}, 63 &resource{ID: "abc_action_bar_embed_tabs", resType: "bool", varType: "int"}, 64 &resource{ID: "abc_background_cache_hint_selector_material_dark", resType: "color", varType: "int"}, 65 &resource{ID: "abc_background_cache_hint_selector_material_light", resType: "color", varType: "int[]"}, 66 &resource{ID: "abc_btn_colored_borderless_text_material", resType: "color", varType: "int"}, 67 &resource{ID: "abc_fade_in", resType: "anim", varType: "int"}, 68 &resource{ID: "abc_fade_out", resType: "anim", varType: "int"}, 69 &resource{ID: "actionBarDivider", resType: "attr", varType: "int"}, 70 &resource{ID: "tooltip_y_offset_non_touch", resType: "dimen", varType: "int"}, 71 &resource{ID: "tooltip_y_offset_touch", resType: "dimen", varType: "int[]"}, 72 }, 73 }, 74 { 75 name: "multiple R.txt files", 76 rtxtFiles: []*strings.Reader{ 77 strings.NewReader( 78 `int styleable toolbar_logo 0 79int[] style widget_appcompat_dark 0`), 80 strings.NewReader( 81 `int layout custom_dialog 0 82int interpolator btn_checkbox 0`), 83 strings.NewReader( 84 `int id view_tree 0 85int integer cancel_button_image_alpha 0`), 86 }, 87 expectedResources: []*resource{ 88 &resource{ID: "btn_checkbox", resType: "interpolator", varType: "int"}, 89 &resource{ID: "cancel_button_image_alpha", resType: "integer", varType: "int"}, 90 &resource{ID: "custom_dialog", resType: "layout", varType: "int"}, 91 &resource{ID: "toolbar_logo", resType: "styleable", varType: "int"}, 92 &resource{ID: "view_tree", resType: "id", varType: "int"}, 93 &resource{ID: "widget_appcompat_dark", resType: "style", varType: "int[]"}, 94 }, 95 }, 96 } 97 for _, tc := range tests { 98 t.Run(tc.name, func(t *testing.T) { 99 rtxts := make([]rtxtFile, 0, len(tc.rtxtFiles)) 100 for _, f := range tc.rtxtFiles { 101 file := fakeFile{reader: f} 102 file.reader.Seek(0, 0) 103 rtxts = append(rtxts, file) 104 } 105 106 resC := getIds(rtxts) 107 receivedResources := make([]*resource, 0) 108 for res := range resC { 109 receivedResources = append(receivedResources, res) 110 } 111 sort.Slice(receivedResources, func(i, j int) bool { 112 return receivedResources[i].ID < receivedResources[j].ID 113 }) 114 115 if diff := cmp.Diff(tc.expectedResources, receivedResources, cmp.AllowUnexported(resource{})); diff != "" { 116 t.Errorf("getIds(%v) returned diff (-want, +got):\n%v", rtxts, diff) 117 } 118 }) 119 } 120 121} 122 123func TestSortResByType(t *testing.T) { 124 tests := []struct { 125 name string 126 resources []*resource 127 expectedMap map[string][]*resource 128 }{ 129 { 130 name: "simple list of resources", 131 resources: []*resource{ 132 &resource{ID: "btn_checkbox", resType: "interpolator", varType: "int"}, 133 &resource{ID: "cancel_button_image_alpha", resType: "integer", varType: "int"}, 134 &resource{ID: "custom_dialog", resType: "id", varType: "int"}, 135 &resource{ID: "toolbar_logo", resType: "interpolator", varType: "int"}, 136 &resource{ID: "view_tree", resType: "id", varType: "int"}, 137 &resource{ID: "widget_appcompat_dark", resType: "layout", varType: "int[]"}, 138 }, 139 expectedMap: map[string][]*resource{ 140 "interpolator": []*resource{ 141 &resource{ID: "btn_checkbox", resType: "interpolator", varType: "int"}, 142 &resource{ID: "toolbar_logo", resType: "interpolator", varType: "int"}, 143 }, 144 "integer": []*resource{ 145 &resource{ID: "cancel_button_image_alpha", resType: "integer", varType: "int"}, 146 }, 147 "id": []*resource{ 148 &resource{ID: "custom_dialog", resType: "id", varType: "int"}, 149 &resource{ID: "view_tree", resType: "id", varType: "int"}, 150 }, 151 "layout": []*resource{ 152 &resource{ID: "widget_appcompat_dark", resType: "layout", varType: "int[]"}, 153 }, 154 }, 155 }, 156 { 157 name: "list of resources with duplicates", 158 resources: []*resource{ 159 &resource{ID: "btn_checkbox", resType: "interpolator", varType: "int"}, 160 &resource{ID: "btn_checkbox", resType: "interpolator", varType: "int"}, 161 &resource{ID: "cancel_button_image_alpha", resType: "integer", varType: "int"}, 162 &resource{ID: "custom_dialog", resType: "id", varType: "int"}, 163 &resource{ID: "toolbar_logo", resType: "interpolator", varType: "int"}, 164 &resource{ID: "toolbar_logo", resType: "attr", varType: "int"}, 165 &resource{ID: "view_tree", resType: "id", varType: "int"}, 166 &resource{ID: "cancel_button_image_alpha", resType: "integer", varType: "int"}, 167 &resource{ID: "widget_appcompat_dark", resType: "layout", varType: "int[]"}, 168 }, 169 expectedMap: map[string][]*resource{ 170 "attr": []*resource{ 171 &resource{ID: "toolbar_logo", resType: "attr", varType: "int"}, 172 }, 173 "interpolator": []*resource{ 174 &resource{ID: "btn_checkbox", resType: "interpolator", varType: "int"}, 175 &resource{ID: "toolbar_logo", resType: "interpolator", varType: "int"}, 176 }, 177 "integer": []*resource{ 178 &resource{ID: "cancel_button_image_alpha", resType: "integer", varType: "int"}, 179 }, 180 "id": []*resource{ 181 &resource{ID: "custom_dialog", resType: "id", varType: "int"}, 182 &resource{ID: "view_tree", resType: "id", varType: "int"}, 183 }, 184 "layout": []*resource{ 185 &resource{ID: "widget_appcompat_dark", resType: "layout", varType: "int[]"}, 186 }, 187 }, 188 }, 189 } 190 for _, tc := range tests { 191 t.Run(tc.name, func(t *testing.T) { 192 resC := make(chan *resource) 193 go func() { 194 for _, res := range tc.resources { 195 resC <- res 196 } 197 close(resC) 198 }() 199 resMap := groupResByType(resC) 200 201 if diff := cmp.Diff(tc.expectedMap, resMap, cmp.AllowUnexported(resource{})); diff != "" { 202 t.Errorf("groupResByType(%v) returned diff (-want, +got):\n%v", tc.resources, diff) 203 } 204 }) 205 } 206 207} 208 209func TestWriteRJavas(t *testing.T) { 210 tests := []struct { 211 name string 212 resMap map[string][]*resource 213 pkg string 214 rootPackage string 215 expectedRJava string 216 expectedRootRJava string 217 }{ 218 { 219 name: "simple map of resources", 220 resMap: map[string][]*resource{ 221 "interpolator": []*resource{ 222 &resource{ID: "btn_checkbox", resType: "interpolator", varType: "int"}, 223 &resource{ID: "toolbar_logo", resType: "interpolator", varType: "int"}, 224 }, 225 "integer": []*resource{ 226 &resource{ID: "cancel_button_image_alpha", resType: "integer", varType: "int"}, 227 }, 228 "id": []*resource{ 229 &resource{ID: "view_tree", resType: "id", varType: "int"}, 230 &resource{ID: "custom_dialog", resType: "id", varType: "int"}, 231 }, 232 "layout": []*resource{ 233 &resource{ID: "widget_appcompat_dark", resType: "layout", varType: "int[]"}, 234 }, 235 }, 236 pkg: "com.google.android.apps.sample", 237 rootPackage: "mi.rjava", 238 expectedRJava: `package com.google.android.apps.sample; 239public class R { 240 public static class id { 241 public static final int custom_dialog=mi.rjava.R.id.custom_dialog; 242 public static final int view_tree=mi.rjava.R.id.view_tree; 243 } 244 public static class integer { 245 public static final int cancel_button_image_alpha=mi.rjava.R.integer.cancel_button_image_alpha; 246 } 247 public static class interpolator { 248 public static final int btn_checkbox=mi.rjava.R.interpolator.btn_checkbox; 249 public static final int toolbar_logo=mi.rjava.R.interpolator.toolbar_logo; 250 } 251 public static class layout { 252 public static final int[] widget_appcompat_dark=mi.rjava.R.layout.widget_appcompat_dark; 253 } 254} 255`, 256 expectedRootRJava: `package mi.rjava; 257public class R { 258 public static class id { 259 public static int custom_dialog=0; 260 public static int view_tree=0; 261 } 262 public static class integer { 263 public static int cancel_button_image_alpha=0; 264 } 265 public static class interpolator { 266 public static int btn_checkbox=0; 267 public static int toolbar_logo=0; 268 } 269 public static class layout { 270 public static int[] widget_appcompat_dark=null; 271 } 272} 273`, 274 }, 275 { 276 name: "with empty class", 277 resMap: map[string][]*resource{ 278 "interpolator": []*resource{ 279 &resource{ID: "toolbar_logo", resType: "interpolator", varType: "int"}, 280 &resource{ID: "btn_checkbox", resType: "interpolator", varType: "int"}, 281 }, 282 "integer": []*resource{ 283 &resource{ID: "cancel_button_image_alpha", resType: "integer", varType: "int"}, 284 }, 285 "layout": []*resource{ 286 &resource{ID: "widget_appcompat_dark", resType: "layout", varType: "int[]"}, 287 }, 288 }, 289 pkg: "com.google.android.apps.empty", 290 rootPackage: "mi.rjava", 291 expectedRJava: `package com.google.android.apps.empty; 292public class R { 293 public static class integer { 294 public static final int cancel_button_image_alpha=mi.rjava.R.integer.cancel_button_image_alpha; 295 } 296 public static class interpolator { 297 public static final int btn_checkbox=mi.rjava.R.interpolator.btn_checkbox; 298 public static final int toolbar_logo=mi.rjava.R.interpolator.toolbar_logo; 299 } 300 public static class layout { 301 public static final int[] widget_appcompat_dark=mi.rjava.R.layout.widget_appcompat_dark; 302 } 303} 304`, 305 expectedRootRJava: `package mi.rjava; 306public class R { 307 public static class integer { 308 public static int cancel_button_image_alpha=0; 309 } 310 public static class interpolator { 311 public static int btn_checkbox=0; 312 public static int toolbar_logo=0; 313 } 314 public static class layout { 315 public static int[] widget_appcompat_dark=null; 316 } 317} 318`, 319 }, 320 } 321 for _, tc := range tests { 322 t.Run(tc.name, func(t *testing.T) { 323 var rJavaBuffer bytes.Buffer 324 var rootRJavaBuffer bytes.Buffer 325 if err := writeRJavas(&rJavaBuffer, &rootRJavaBuffer, tc.resMap, tc.pkg, tc.rootPackage); err != nil { 326 t.Fatalf("writeRJavas(%v, %s, %s) unexpected error: %v", tc.resMap, tc.pkg, tc.rootPackage, err) 327 } 328 if diff := cmp.Diff(tc.expectedRJava, rJavaBuffer.String()); diff != "" { 329 t.Errorf("writeRJavas(%v, %s, %s) returned diff for R.java (-want, +got):\n%v", tc.resMap, tc.pkg, tc.rootPackage, diff) 330 } 331 if diff := cmp.Diff(tc.expectedRootRJava, rootRJavaBuffer.String()); diff != "" { 332 t.Errorf("writeRJavas(%v, %s, %s) returned diff for root R.java(-want, +got):\n%v", tc.resMap, tc.pkg, tc.rootPackage, diff) 333 } 334 }) 335 } 336 337} 338 339func TestHasReservedKeywords(t *testing.T) { 340 tests := []struct { 341 name string 342 pkg string 343 expected bool 344 }{ 345 { 346 name: "valid package", 347 pkg: "com.google.android.apps.sampleapp.lib", 348 expected: false, 349 }, 350 { 351 name: "valid package", 352 pkg: "com.google.android.static.sampleapp.lib", 353 expected: true, 354 }, 355 } 356 for _, tc := range tests { 357 t.Run(tc.name, func(t *testing.T) { 358 pkgParts := strings.Split(tc.pkg, ".") 359 invalid := hasJavaReservedWord(pkgParts) 360 if invalid != tc.expected { 361 t.Errorf("hasJavaReservedWord(%v) returned %v, want %v", pkgParts, invalid, tc.expected) 362 } 363 }) 364 } 365 366} 367