1#!/usr/bin/env python
2# Copyright (c) 2012 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6'''Unit tests for ChromeScaledImage.'''
7
8
9import re
10import struct
11import unittest
12import zlib
13
14from grit import exception
15from grit import util
16from grit.format import data_pack
17from grit.tool import build
18
19
20_OUTFILETYPES = [
21  ('.h', 'rc_header'),
22  ('_map.cc', 'resource_map_source'),
23  ('_map.h', 'resource_map_header'),
24  ('.pak', 'data_package'),
25  ('.rc', 'rc_all'),
26]
27
28
29_PNG_HEADER = (
30    '\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52'
31    '\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90\x77\x53'
32    '\xde')
33_PNG_FOOTER = (
34    '\x00\x00\x00\x0c\x49\x44\x41\x54\x18\x57\x63\xf8\xff\xff\x3f\x00'
35    '\x05\xfe\x02\xfe\xa7\x35\x81\x84\x00\x00\x00\x00\x49\x45\x4e\x44'
36    '\xae\x42\x60\x82')
37
38
39def _MakePNG(chunks):
40  pack_int32 = struct.Struct('>i').pack
41  chunks = [pack_int32(len(payload)) + type + payload + pack_int32(zlib.crc32(type + payload))
42            for type, payload in chunks]
43  return _PNG_HEADER + ''.join(chunks) + _PNG_FOOTER
44
45
46def _GetFilesInPak(pakname):
47  '''Get a set of the files that were actually included in the .pak output.
48  '''
49  return set(data_pack.DataPack.ReadDataPack(pakname).resources.values())
50
51
52def _GetFilesInRc(rcname, tmp_dir, contents):
53  '''Get a set of the files that were actually included in the .rc output.
54  '''
55  data = util.ReadFile(rcname, util.BINARY).decode('utf-16')
56  contents = dict((tmp_dir.GetPath(k), v) for k, v in contents.items())
57  return set(contents[m.group(1)]
58             for m in re.finditer(ur'(?m)^\w+\s+BINDATA\s+"([^"]+)"$', data))
59
60
61def _MakeFallbackAttr(fallback):
62  if fallback is None:
63    return ''
64  else:
65    return ' fallback_to_low_resolution="%s"' % ('false', 'true')[fallback]
66
67
68def _Structures(fallback, *body):
69  return '<structures%s>\n%s\n</structures>' % (
70      _MakeFallbackAttr(fallback), '\n'.join(body))
71
72
73def _Structure(name, file, fallback=None):
74  return '<structure name="%s" file="%s" type="chrome_scaled_image"%s />' % (
75      name, file, _MakeFallbackAttr(fallback))
76
77
78def _If(expr, *body):
79  return '<if expr="%s">\n%s\n</if>' % (expr, '\n'.join(body))
80
81
82def _RunBuildTest(self, structures, inputs, expected_outputs, skip_rc=False):
83  outputs = '\n'.join('<output filename="out/%s%s" type="%s" context="%s" />'
84                              % (context, ext, type, context)
85                      for ext, type in _OUTFILETYPES
86                      for context in expected_outputs)
87  infiles = {
88    'in/in.grd': '''<?xml version="1.0" encoding="UTF-8"?>
89      <grit latest_public_release="0" current_release="1">
90        <outputs>
91          %s
92        </outputs>
93        <release seq="1">
94          %s
95        </release>
96      </grit>
97      ''' % (outputs, structures),
98  }
99  for pngpath, pngdata in inputs.items():
100    infiles['in/' + pngpath] = pngdata
101  class Options(object):
102    pass
103  with util.TempDir(infiles) as tmp_dir:
104    with tmp_dir.AsCurrentDir():
105      options = Options()
106      options.input = tmp_dir.GetPath('in/in.grd')
107      options.verbose = False
108      options.extra_verbose = False
109      build.RcBuilder().Run(options, [])
110    for context, expected_data in expected_outputs.items():
111      self.assertEquals(expected_data,
112                        _GetFilesInPak(tmp_dir.GetPath('out/%s.pak' % context)))
113      if not skip_rc:
114        self.assertEquals(expected_data,
115                          _GetFilesInRc(tmp_dir.GetPath('out/%s.rc' % context),
116                                        tmp_dir, infiles))
117
118
119class ChromeScaledImageUnittest(unittest.TestCase):
120  def testNormalFallback(self):
121    d123a = _MakePNG([('AbCd', '')])
122    t123a = _MakePNG([('EfGh', '')])
123    d123b = _MakePNG([('IjKl', '')])
124    _RunBuildTest(self,
125        _Structures(None,
126            _Structure('IDR_A', 'a.png'),
127            _Structure('IDR_B', 'b.png'),
128        ),
129        {'default_123_percent/a.png': d123a,
130         'tactile_123_percent/a.png': t123a,
131         'default_123_percent/b.png': d123b,
132        },
133        {'default_123_percent': set([d123a, d123b]),
134         'tactile_123_percent': set([t123a, d123b]),
135        })
136
137  def testNormalFallbackFailure(self):
138    self.assertRaises(exception.FileNotFound,
139        _RunBuildTest, self,
140            _Structures(None,
141                _Structure('IDR_A', 'a.png'),
142            ),
143            {'default_100_percent/a.png': _MakePNG([('AbCd', '')]),
144             'tactile_100_percent/a.png': _MakePNG([('EfGh', '')]),
145            },
146            {'tactile_123_percent': 'should fail before using this'})
147
148  def testLowresFallback(self):
149    png = _MakePNG([('Abcd', '')])
150    png_with_csCl = _MakePNG([('csCl', ''),('Abcd', '')])
151    for outer in (None, False, True):
152      for inner in (None, False, True):
153        args = (
154            self,
155            _Structures(outer,
156                _Structure('IDR_A', 'a.png', inner),
157            ),
158            {'default_100_percent/a.png': png},
159            {'tactile_200_percent': set([png_with_csCl])})
160        if inner or (inner is None and outer):
161          # should fall back to 100%
162          _RunBuildTest(*args, skip_rc=True)
163        else:
164          # shouldn't fall back
165          self.assertRaises(exception.FileNotFound, _RunBuildTest, *args)
166
167    # Test fallback failure with fallback_to_low_resolution=True
168    self.assertRaises(exception.FileNotFound,
169        _RunBuildTest, self,
170            _Structures(True,
171                _Structure('IDR_A', 'a.png'),
172            ),
173            {},  # no files
174            {'tactile_123_percent': 'should fail before using this'})
175