blob: c4832dc9a00389f73242781633fbf315ef21056b [file] [log] [blame]
Nan Zhang674dd932018-01-26 18:30:36 -08001// Copyright 2018 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 zip
16
17import (
Colin Cross05518bc2018-09-27 15:06:19 -070018 "bytes"
19 "hash/crc32"
20 "io"
21 "os"
Nan Zhang674dd932018-01-26 18:30:36 -080022 "reflect"
Colin Cross05518bc2018-09-27 15:06:19 -070023 "syscall"
Nan Zhang674dd932018-01-26 18:30:36 -080024 "testing"
Colin Cross05518bc2018-09-27 15:06:19 -070025
26 "android/soong/third_party/zip"
27
28 "github.com/google/blueprint/pathtools"
Nan Zhang674dd932018-01-26 18:30:36 -080029)
30
Colin Cross05518bc2018-09-27 15:06:19 -070031var (
32 fileA = []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
33 fileB = []byte("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")
34 fileC = []byte("CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC")
35 fileEmpty = []byte("")
36 fileManifest = []byte("Manifest-Version: 1.0\nCreated-By: soong_zip\n\n")
37
38 fileCustomManifest = []byte("Custom manifest: true\n")
39 customManifestAfter = []byte("Manifest-Version: 1.0\nCreated-By: soong_zip\nCustom manifest: true\n\n")
40)
41
42var mockFs = pathtools.MockFs(map[string][]byte{
Colin Cross9cb51db2019-06-17 14:12:41 -070043 "a/a/a": fileA,
44 "a/a/b": fileB,
45 "a/a/c -> ../../c": nil,
46 "dangling -> missing": nil,
47 "a/a/d -> b": nil,
48 "c": fileC,
Colin Cross7ddd08a2022-08-15 15:47:41 -070049 "d/a/a": nil,
Colin Crosscaf4d4c2021-02-03 15:15:14 -080050 "l_nl": []byte("a/a/a\na/a/b\nc\n\\[\n"),
51 "l_sp": []byte("a/a/a a/a/b c \\["),
Colin Cross9cb51db2019-06-17 14:12:41 -070052 "l2": []byte("missing\n"),
Colin Crosscaf4d4c2021-02-03 15:15:14 -080053 "rsp": []byte("'a/a/a'\na/a/b\n'@'\n'foo'\\''bar'\n'['"),
Colin Cross053fca12020-08-19 13:51:47 -070054 "@ -> c": nil,
55 "foo'bar -> c": nil,
Colin Cross9cb51db2019-06-17 14:12:41 -070056 "manifest.txt": fileCustomManifest,
Colin Crosscaf4d4c2021-02-03 15:15:14 -080057 "[": fileEmpty,
Colin Cross05518bc2018-09-27 15:06:19 -070058})
59
60func fh(name string, contents []byte, method uint16) zip.FileHeader {
61 return zip.FileHeader{
62 Name: name,
63 Method: method,
64 CRC32: crc32.ChecksumIEEE(contents),
65 UncompressedSize64: uint64(len(contents)),
Chris Grossfa5b4e92021-06-02 12:56:08 -070066 ExternalAttrs: (syscall.S_IFREG | 0644) << 16,
Colin Cross05518bc2018-09-27 15:06:19 -070067 }
68}
69
70func fhManifest(contents []byte) zip.FileHeader {
71 return zip.FileHeader{
72 Name: "META-INF/MANIFEST.MF",
73 Method: zip.Store,
74 CRC32: crc32.ChecksumIEEE(contents),
75 UncompressedSize64: uint64(len(contents)),
Chris Grossfa5b4e92021-06-02 12:56:08 -070076 ExternalAttrs: (syscall.S_IFREG | 0644) << 16,
Colin Cross05518bc2018-09-27 15:06:19 -070077 }
78}
79
80func fhLink(name string, to string) zip.FileHeader {
81 return zip.FileHeader{
82 Name: name,
83 Method: zip.Store,
84 CRC32: crc32.ChecksumIEEE([]byte(to)),
85 UncompressedSize64: uint64(len(to)),
86 ExternalAttrs: (syscall.S_IFLNK | 0777) << 16,
87 }
88}
89
90func fhDir(name string) zip.FileHeader {
91 return zip.FileHeader{
92 Name: name,
93 Method: zip.Store,
94 CRC32: crc32.ChecksumIEEE(nil),
95 UncompressedSize64: 0,
Chris Grossfa5b4e92021-06-02 12:56:08 -070096 ExternalAttrs: (syscall.S_IFDIR|0755)<<16 | 0x10,
Colin Cross05518bc2018-09-27 15:06:19 -070097 }
98}
99
100func fileArgsBuilder() *FileArgsBuilder {
101 return &FileArgsBuilder{
102 fs: mockFs,
103 }
104}
105
106func TestZip(t *testing.T) {
107 testCases := []struct {
Colin Cross4be8f9e2018-09-28 15:16:48 -0700108 name string
109 args *FileArgsBuilder
110 compressionLevel int
111 emulateJar bool
112 nonDeflatedFiles map[string]bool
113 dirEntries bool
114 manifest string
115 storeSymlinks bool
116 ignoreMissingFiles bool
Colin Cross05518bc2018-09-27 15:06:19 -0700117
118 files []zip.FileHeader
119 err error
120 }{
121 {
122 name: "empty args",
123 args: fileArgsBuilder(),
124
125 files: []zip.FileHeader{},
126 },
127 {
128 name: "files",
129 args: fileArgsBuilder().
130 File("a/a/a").
131 File("a/a/b").
Colin Crosscaf4d4c2021-02-03 15:15:14 -0800132 File("c").
133 File(`\[`),
Colin Cross05518bc2018-09-27 15:06:19 -0700134 compressionLevel: 9,
135
136 files: []zip.FileHeader{
137 fh("a/a/a", fileA, zip.Deflate),
138 fh("a/a/b", fileB, zip.Deflate),
139 fh("c", fileC, zip.Deflate),
Colin Crosscaf4d4c2021-02-03 15:15:14 -0800140 fh("[", fileEmpty, zip.Store),
Colin Cross05518bc2018-09-27 15:06:19 -0700141 },
142 },
143 {
Colin Cross1d98ee22018-09-18 17:05:15 -0700144 name: "files glob",
145 args: fileArgsBuilder().
146 SourcePrefixToStrip("a").
147 File("a/**/*"),
148 compressionLevel: 9,
Colin Cross09f11052018-09-21 15:12:39 -0700149 storeSymlinks: true,
Colin Cross1d98ee22018-09-18 17:05:15 -0700150
151 files: []zip.FileHeader{
152 fh("a/a", fileA, zip.Deflate),
153 fh("a/b", fileB, zip.Deflate),
154 fhLink("a/c", "../../c"),
155 fhLink("a/d", "b"),
156 },
157 },
158 {
159 name: "dir",
160 args: fileArgsBuilder().
161 SourcePrefixToStrip("a").
162 Dir("a"),
163 compressionLevel: 9,
Colin Cross09f11052018-09-21 15:12:39 -0700164 storeSymlinks: true,
Colin Cross1d98ee22018-09-18 17:05:15 -0700165
166 files: []zip.FileHeader{
167 fh("a/a", fileA, zip.Deflate),
168 fh("a/b", fileB, zip.Deflate),
169 fhLink("a/c", "../../c"),
170 fhLink("a/d", "b"),
171 },
172 },
173 {
Colin Cross05518bc2018-09-27 15:06:19 -0700174 name: "stored files",
175 args: fileArgsBuilder().
176 File("a/a/a").
177 File("a/a/b").
178 File("c"),
179 compressionLevel: 0,
180
181 files: []zip.FileHeader{
182 fh("a/a/a", fileA, zip.Store),
183 fh("a/a/b", fileB, zip.Store),
184 fh("c", fileC, zip.Store),
185 },
186 },
187 {
188 name: "symlinks in zip",
189 args: fileArgsBuilder().
190 File("a/a/a").
191 File("a/a/b").
192 File("a/a/c").
193 File("a/a/d"),
194 compressionLevel: 9,
Colin Cross09f11052018-09-21 15:12:39 -0700195 storeSymlinks: true,
Colin Cross05518bc2018-09-27 15:06:19 -0700196
197 files: []zip.FileHeader{
198 fh("a/a/a", fileA, zip.Deflate),
199 fh("a/a/b", fileB, zip.Deflate),
200 fhLink("a/a/c", "../../c"),
201 fhLink("a/a/d", "b"),
202 },
203 },
204 {
Colin Cross09f11052018-09-21 15:12:39 -0700205 name: "follow symlinks",
206 args: fileArgsBuilder().
207 File("a/a/a").
208 File("a/a/b").
209 File("a/a/c").
210 File("a/a/d"),
211 compressionLevel: 9,
212 storeSymlinks: false,
213
214 files: []zip.FileHeader{
215 fh("a/a/a", fileA, zip.Deflate),
216 fh("a/a/b", fileB, zip.Deflate),
217 fh("a/a/c", fileC, zip.Deflate),
218 fh("a/a/d", fileB, zip.Deflate),
219 },
220 },
221 {
Colin Cross9cb51db2019-06-17 14:12:41 -0700222 name: "dangling symlinks",
223 args: fileArgsBuilder().
224 File("dangling"),
225 compressionLevel: 9,
226 storeSymlinks: true,
227
228 files: []zip.FileHeader{
229 fhLink("dangling", "missing"),
230 },
231 },
232 {
Colin Cross05518bc2018-09-27 15:06:19 -0700233 name: "list",
234 args: fileArgsBuilder().
Jiyong Park04bbf982019-11-04 13:18:41 +0900235 List("l_nl"),
236 compressionLevel: 9,
237
238 files: []zip.FileHeader{
239 fh("a/a/a", fileA, zip.Deflate),
240 fh("a/a/b", fileB, zip.Deflate),
241 fh("c", fileC, zip.Deflate),
Colin Crosscaf4d4c2021-02-03 15:15:14 -0800242 fh("[", fileEmpty, zip.Store),
Jiyong Park04bbf982019-11-04 13:18:41 +0900243 },
244 },
245 {
246 name: "list",
247 args: fileArgsBuilder().
248 List("l_sp"),
Colin Cross05518bc2018-09-27 15:06:19 -0700249 compressionLevel: 9,
250
251 files: []zip.FileHeader{
252 fh("a/a/a", fileA, zip.Deflate),
253 fh("a/a/b", fileB, zip.Deflate),
254 fh("c", fileC, zip.Deflate),
Colin Crosscaf4d4c2021-02-03 15:15:14 -0800255 fh("[", fileEmpty, zip.Store),
Colin Cross05518bc2018-09-27 15:06:19 -0700256 },
257 },
258 {
Colin Cross053fca12020-08-19 13:51:47 -0700259 name: "rsp",
260 args: fileArgsBuilder().
261 RspFile("rsp"),
262 compressionLevel: 9,
263
264 files: []zip.FileHeader{
265 fh("a/a/a", fileA, zip.Deflate),
266 fh("a/a/b", fileB, zip.Deflate),
267 fh("@", fileC, zip.Deflate),
268 fh("foo'bar", fileC, zip.Deflate),
Colin Crosscaf4d4c2021-02-03 15:15:14 -0800269 fh("[", fileEmpty, zip.Store),
Colin Cross053fca12020-08-19 13:51:47 -0700270 },
271 },
272 {
Colin Cross05518bc2018-09-27 15:06:19 -0700273 name: "prefix in zip",
274 args: fileArgsBuilder().
275 PathPrefixInZip("foo").
276 File("a/a/a").
277 File("a/a/b").
278 File("c"),
279 compressionLevel: 9,
280
281 files: []zip.FileHeader{
282 fh("foo/a/a/a", fileA, zip.Deflate),
283 fh("foo/a/a/b", fileB, zip.Deflate),
284 fh("foo/c", fileC, zip.Deflate),
285 },
286 },
287 {
288 name: "relative root",
289 args: fileArgsBuilder().
290 SourcePrefixToStrip("a").
291 File("a/a/a").
292 File("a/a/b"),
293 compressionLevel: 9,
294
295 files: []zip.FileHeader{
296 fh("a/a", fileA, zip.Deflate),
297 fh("a/b", fileB, zip.Deflate),
298 },
299 },
300 {
301 name: "multiple relative root",
302 args: fileArgsBuilder().
303 SourcePrefixToStrip("a").
304 File("a/a/a").
305 SourcePrefixToStrip("a/a").
306 File("a/a/b"),
307 compressionLevel: 9,
308
309 files: []zip.FileHeader{
310 fh("a/a", fileA, zip.Deflate),
311 fh("b", fileB, zip.Deflate),
312 },
313 },
314 {
315 name: "emulate jar",
316 args: fileArgsBuilder().
317 File("a/a/a").
318 File("a/a/b"),
319 compressionLevel: 9,
320 emulateJar: true,
321
322 files: []zip.FileHeader{
323 fhDir("META-INF/"),
324 fhManifest(fileManifest),
325 fhDir("a/"),
326 fhDir("a/a/"),
327 fh("a/a/a", fileA, zip.Deflate),
328 fh("a/a/b", fileB, zip.Deflate),
329 },
330 },
331 {
332 name: "emulate jar with manifest",
333 args: fileArgsBuilder().
334 File("a/a/a").
335 File("a/a/b"),
336 compressionLevel: 9,
337 emulateJar: true,
338 manifest: "manifest.txt",
339
340 files: []zip.FileHeader{
341 fhDir("META-INF/"),
342 fhManifest(customManifestAfter),
343 fhDir("a/"),
344 fhDir("a/a/"),
345 fh("a/a/a", fileA, zip.Deflate),
346 fh("a/a/b", fileB, zip.Deflate),
347 },
348 },
349 {
350 name: "dir entries",
351 args: fileArgsBuilder().
352 File("a/a/a").
353 File("a/a/b"),
354 compressionLevel: 9,
355 dirEntries: true,
356
357 files: []zip.FileHeader{
358 fhDir("a/"),
359 fhDir("a/a/"),
360 fh("a/a/a", fileA, zip.Deflate),
361 fh("a/a/b", fileB, zip.Deflate),
362 },
363 },
364 {
365 name: "junk paths",
366 args: fileArgsBuilder().
367 JunkPaths(true).
368 File("a/a/a").
369 File("a/a/b"),
370 compressionLevel: 9,
371
372 files: []zip.FileHeader{
373 fh("a", fileA, zip.Deflate),
374 fh("b", fileB, zip.Deflate),
375 },
376 },
377 {
378 name: "non deflated files",
379 args: fileArgsBuilder().
380 File("a/a/a").
381 File("a/a/b"),
382 compressionLevel: 9,
383 nonDeflatedFiles: map[string]bool{"a/a/a": true},
384
385 files: []zip.FileHeader{
386 fh("a/a/a", fileA, zip.Store),
387 fh("a/a/b", fileB, zip.Deflate),
388 },
389 },
Colin Cross4be8f9e2018-09-28 15:16:48 -0700390 {
391 name: "ignore missing files",
392 args: fileArgsBuilder().
393 File("a/a/a").
394 File("a/a/b").
395 File("missing"),
396 compressionLevel: 9,
397 ignoreMissingFiles: true,
398
399 files: []zip.FileHeader{
400 fh("a/a/a", fileA, zip.Deflate),
401 fh("a/a/b", fileB, zip.Deflate),
402 },
403 },
Colin Cross7ddd08a2022-08-15 15:47:41 -0700404 {
405 name: "duplicate sources",
406 args: fileArgsBuilder().
407 File("a/a/a").
408 File("a/a/a"),
409 compressionLevel: 9,
410
411 files: []zip.FileHeader{
412 fh("a/a/a", fileA, zip.Deflate),
413 },
414 },
Colin Cross05518bc2018-09-27 15:06:19 -0700415
416 // errors
417 {
418 name: "error missing file",
419 args: fileArgsBuilder().
420 File("missing"),
421 err: os.ErrNotExist,
422 },
423 {
Colin Cross1d98ee22018-09-18 17:05:15 -0700424 name: "error missing dir",
425 args: fileArgsBuilder().
426 Dir("missing"),
427 err: os.ErrNotExist,
428 },
429 {
Colin Cross05518bc2018-09-27 15:06:19 -0700430 name: "error missing file in list",
431 args: fileArgsBuilder().
432 List("l2"),
433 err: os.ErrNotExist,
434 },
Colin Cross1d98ee22018-09-18 17:05:15 -0700435 {
436 name: "error incorrect relative root",
437 args: fileArgsBuilder().
438 SourcePrefixToStrip("b").
439 File("a/a/a"),
440 err: IncorrectRelativeRootError{},
441 },
Colin Cross7ddd08a2022-08-15 15:47:41 -0700442 {
443 name: "error conflicting file",
444 args: fileArgsBuilder().
445 SourcePrefixToStrip("a").
446 File("a/a/a").
447 SourcePrefixToStrip("d").
448 File("d/a/a"),
449 err: ConflictingFileError{},
450 },
Colin Cross05518bc2018-09-27 15:06:19 -0700451 }
452
453 for _, test := range testCases {
454 t.Run(test.name, func(t *testing.T) {
455 if test.args.Error() != nil {
456 t.Fatal(test.args.Error())
457 }
458
459 args := ZipArgs{}
460 args.FileArgs = test.args.FileArgs()
461 args.CompressionLevel = test.compressionLevel
462 args.EmulateJar = test.emulateJar
463 args.AddDirectoryEntriesToZip = test.dirEntries
464 args.NonDeflatedFiles = test.nonDeflatedFiles
465 args.ManifestSourcePath = test.manifest
Colin Cross09f11052018-09-21 15:12:39 -0700466 args.StoreSymlinks = test.storeSymlinks
Colin Cross4be8f9e2018-09-28 15:16:48 -0700467 args.IgnoreMissingFiles = test.ignoreMissingFiles
Colin Cross05518bc2018-09-27 15:06:19 -0700468 args.Filesystem = mockFs
Colin Cross4be8f9e2018-09-28 15:16:48 -0700469 args.Stderr = &bytes.Buffer{}
Colin Cross05518bc2018-09-27 15:06:19 -0700470
471 buf := &bytes.Buffer{}
Sasha Smundak8eedba62020-11-16 19:00:27 -0800472 err := zipTo(args, buf)
Colin Cross05518bc2018-09-27 15:06:19 -0700473
474 if (err != nil) != (test.err != nil) {
475 t.Fatalf("want error %v, got %v", test.err, err)
476 } else if test.err != nil {
477 if os.IsNotExist(test.err) {
Colin Cross7ddd08a2022-08-15 15:47:41 -0700478 if !os.IsNotExist(err) {
Colin Cross05518bc2018-09-27 15:06:19 -0700479 t.Fatalf("want error %v, got %v", test.err, err)
480 }
Colin Cross1d98ee22018-09-18 17:05:15 -0700481 } else if _, wantRelativeRootErr := test.err.(IncorrectRelativeRootError); wantRelativeRootErr {
482 if _, gotRelativeRootErr := err.(IncorrectRelativeRootError); !gotRelativeRootErr {
483 t.Fatalf("want error %v, got %v", test.err, err)
484 }
Colin Cross7ddd08a2022-08-15 15:47:41 -0700485 } else if _, wantConflictingFileError := test.err.(ConflictingFileError); wantConflictingFileError {
486 if _, gotConflictingFileError := err.(ConflictingFileError); !gotConflictingFileError {
487 t.Fatalf("want error %v, got %v", test.err, err)
488 }
Colin Cross05518bc2018-09-27 15:06:19 -0700489 } else {
490 t.Fatalf("want error %v, got %v", test.err, err)
491 }
492 return
493 }
494
495 br := bytes.NewReader(buf.Bytes())
496 zr, err := zip.NewReader(br, int64(br.Len()))
497 if err != nil {
498 t.Fatal(err)
499 }
500
501 var files []zip.FileHeader
502 for _, f := range zr.File {
503 r, err := f.Open()
504 if err != nil {
505 t.Fatalf("error when opening %s: %s", f.Name, err)
506 }
507
508 crc := crc32.NewIEEE()
509 len, err := io.Copy(crc, r)
510 r.Close()
511 if err != nil {
512 t.Fatalf("error when reading %s: %s", f.Name, err)
513 }
514
515 if uint64(len) != f.UncompressedSize64 {
516 t.Errorf("incorrect length for %s, want %d got %d", f.Name, f.UncompressedSize64, len)
517 }
518
519 if crc.Sum32() != f.CRC32 {
520 t.Errorf("incorrect crc for %s, want %x got %x", f.Name, f.CRC32, crc)
521 }
522
523 files = append(files, f.FileHeader)
524 }
525
526 if len(files) != len(test.files) {
527 t.Fatalf("want %d files, got %d", len(test.files), len(files))
528 }
529
530 for i := range files {
531 want := test.files[i]
532 got := files[i]
533
534 if want.Name != got.Name {
535 t.Errorf("incorrect file %d want %q got %q", i, want.Name, got.Name)
536 continue
537 }
538
539 if want.UncompressedSize64 != got.UncompressedSize64 {
540 t.Errorf("incorrect file %s length want %v got %v", want.Name,
541 want.UncompressedSize64, got.UncompressedSize64)
542 }
543
544 if want.ExternalAttrs != got.ExternalAttrs {
545 t.Errorf("incorrect file %s attrs want %x got %x", want.Name,
546 want.ExternalAttrs, got.ExternalAttrs)
547 }
548
549 if want.CRC32 != got.CRC32 {
550 t.Errorf("incorrect file %s crc want %v got %v", want.Name,
551 want.CRC32, got.CRC32)
552 }
553
554 if want.Method != got.Method {
555 t.Errorf("incorrect file %s method want %v got %v", want.Name,
556 want.Method, got.Method)
557 }
558 }
559 })
560 }
561}
562
Colin Cross9cb51db2019-06-17 14:12:41 -0700563func TestSrcJar(t *testing.T) {
564 mockFs := pathtools.MockFs(map[string][]byte{
565 "wrong_package.java": []byte("package foo;"),
566 "foo/correct_package.java": []byte("package foo;"),
567 "src/no_package.java": nil,
568 "src2/parse_error.java": []byte("error"),
569 })
570
571 want := []string{
572 "foo/",
573 "foo/wrong_package.java",
574 "foo/correct_package.java",
575 "no_package.java",
576 "src2/",
577 "src2/parse_error.java",
578 }
579
580 args := ZipArgs{}
581 args.FileArgs = NewFileArgsBuilder().File("**/*.java").FileArgs()
582
583 args.SrcJar = true
584 args.AddDirectoryEntriesToZip = true
585 args.Filesystem = mockFs
586 args.Stderr = &bytes.Buffer{}
587
588 buf := &bytes.Buffer{}
Sasha Smundak8eedba62020-11-16 19:00:27 -0800589 err := zipTo(args, buf)
Colin Cross9cb51db2019-06-17 14:12:41 -0700590 if err != nil {
591 t.Fatalf("got error %v", err)
592 }
593
594 br := bytes.NewReader(buf.Bytes())
595 zr, err := zip.NewReader(br, int64(br.Len()))
596 if err != nil {
597 t.Fatal(err)
598 }
599
600 var got []string
601 for _, f := range zr.File {
602 r, err := f.Open()
603 if err != nil {
604 t.Fatalf("error when opening %s: %s", f.Name, err)
605 }
606
607 crc := crc32.NewIEEE()
608 len, err := io.Copy(crc, r)
609 r.Close()
610 if err != nil {
611 t.Fatalf("error when reading %s: %s", f.Name, err)
612 }
613
614 if uint64(len) != f.UncompressedSize64 {
615 t.Errorf("incorrect length for %s, want %d got %d", f.Name, f.UncompressedSize64, len)
616 }
617
618 if crc.Sum32() != f.CRC32 {
619 t.Errorf("incorrect crc for %s, want %x got %x", f.Name, f.CRC32, crc)
620 }
621
622 got = append(got, f.Name)
623 }
624
625 if !reflect.DeepEqual(want, got) {
626 t.Errorf("want files %q, got %q", want, got)
627 }
628}