1# -*- coding: utf-8 -*-
2
3import base64
4import contextlib
5import functools
6import glob
7import json
8import io
9import os
10import os.path
11import re
12import shutil
13import subprocess
14import sys
15import tempfile
16import traceback
17import unittest
18
19import pytest
20from six import StringIO, b, string_types, text_type
21from werkzeug.test import Client
22from werkzeug.wrappers import Response
23
24import pysassc
25import sass
26import sassc
27from sassutils._compat import collections_abc
28from sassutils.builder import Manifest, build_directory
29from sassutils.wsgi import SassMiddleware
30
31
32if os.sep != '/' and os.altsep:  # pragma: no cover (windows)
33    def normalize_path(path):
34        path = os.path.abspath(os.path.normpath(path))
35        return path.replace(os.sep, os.altsep)
36else:   # pragma: no cover (non-windows)
37    def normalize_path(path):
38        return path
39
40
41@pytest.fixture(scope='session', autouse=True)
42def set_coverage_instrumentation():
43    if 'PWD' in os.environ:  # pragma: no branch
44        rcfile = os.path.join(os.environ['PWD'], '.coveragerc')
45        os.environ['COVERAGE_PROCESS_START'] = rcfile
46
47
48A_EXPECTED_CSS = '''\
49body {
50  background-color: green; }
51  body a {
52    color: blue; }
53'''
54
55A_EXPECTED_CSS_WITH_MAP = '''\
56body {
57  background-color: green; }
58  body a {
59    color: blue; }
60
61/*# sourceMappingURL=../a.scss.css.map */'''
62
63A_EXPECTED_MAP = {
64    'version': 3,
65    'file': 'test/a.css',
66    'sources': ['test/a.scss'],
67    'names': [],
68    'mappings': (
69        'AAKA,AAAA,IAAI,CAAC;EAHH,gBAAgB,EAAE,KAAK,GAQxB;EALD,AAEE,IAFE,CAEF,'
70        'CAAC,CAAC;IACA,KAAK,EAAE,IAAI,GACZ'
71    ),
72}
73
74with io.open('test/a.scss', newline='') as f:
75    A_EXPECTED_MAP_CONTENTS = dict(A_EXPECTED_MAP, sourcesContent=[f.read()])
76
77B_EXPECTED_CSS = '''\
78b i {
79  font-size: 20px; }
80'''
81
82B_EXPECTED_CSS_WITH_MAP = '''\
83b i {
84  font-size: 20px; }
85
86/*# sourceMappingURL=../css/b.scss.css.map */'''
87
88C_EXPECTED_CSS = '''\
89body {
90  background-color: green; }
91  body a {
92    color: blue; }
93
94h1 a {
95  color: green; }
96'''
97
98D_EXPECTED_CSS = u'''\
99@charset "UTF-8";
100body {
101  background-color: green; }
102  body a {
103    font: '나눔고딕', sans-serif; }
104'''
105
106D_EXPECTED_CSS_WITH_MAP = u'''\
107@charset "UTF-8";
108body {
109  background-color: green; }
110  body a {
111    font: '나눔고딕', sans-serif; }
112
113/*# sourceMappingURL=../css/d.scss.css.map */'''
114
115E_EXPECTED_CSS = '''\
116a {
117  color: red; }
118'''
119
120G_EXPECTED_CSS = '''\
121body {
122  font: 100% Helvetica, sans-serif;
123  color: #333;
124  height: 1.42857; }
125'''
126
127G_EXPECTED_CSS_WITH_PRECISION_8 = '''\
128body {
129  font: 100% Helvetica, sans-serif;
130  color: #333;
131  height: 1.42857143; }
132'''
133
134H_EXPECTED_CSS = '''\
135a b {
136  color: blue; }
137'''
138
139SUBDIR_RECUR_EXPECTED_CSS = '''\
140body p {
141  color: blue; }
142'''
143
144
145re_sourcemap_url = re.compile(r'/\*# sourceMappingURL=([^\s]+?) \*/')
146re_base64_data_uri = re.compile(r'^data:[^;]*?;base64,(.+)$')
147
148
149def _map_in_output_dir(s):
150    def cb(match):
151        filename = os.path.basename(match.group(1))
152        return '/*# sourceMappingURL={} */'.format(filename)
153
154    return re_sourcemap_url.sub(cb, s)
155
156
157@pytest.fixture(autouse=True)
158def no_warnings(recwarn):
159    yield
160    assert len(recwarn) == 0
161
162
163class BaseTestCase(unittest.TestCase):
164
165    def assert_source_map_equal(self, expected, actual):
166        if isinstance(expected, string_types):
167            expected = json.loads(expected)
168        if isinstance(actual, string_types):
169            actual = json.loads(actual)
170        assert expected == actual
171
172    def assert_source_map_file(self, expected, filename):
173        with open(filename) as f:
174            try:
175                tree = json.load(f)
176            except ValueError as e:  # pragma: no cover
177                f.seek(0)
178                msg = '{!s}\n\n{}:\n\n{}'.format(e, filename, f.read())
179                raise ValueError(msg)
180        self.assert_source_map_equal(expected, tree)
181
182    def assert_source_map_embed(self, expected, src):
183        url_matches = re_sourcemap_url.search(src)
184        assert url_matches is not None
185        embed_url = url_matches.group(1)
186        b64_matches = re_base64_data_uri.match(embed_url)
187        assert b64_matches is not None
188        decoded = base64.b64decode(b64_matches.group(1)).decode('utf-8')
189        actual = json.loads(decoded)
190        self.assert_source_map_equal(expected, actual)
191
192
193class SassTestCase(BaseTestCase):
194
195    def test_version(self):
196        assert re.match(r'^\d+\.\d+\.\d+$', sass.__version__)
197
198    def test_output_styles(self):
199        assert isinstance(sass.OUTPUT_STYLES, collections_abc.Mapping)
200        assert 'nested' in sass.OUTPUT_STYLES
201
202    def test_and_join(self):
203        self.assertEqual(
204            'Korea, Japan, China, and Taiwan',
205            sass.and_join(['Korea', 'Japan', 'China', 'Taiwan']),
206        )
207        self.assertEqual(
208            'Korea, and Japan',
209            sass.and_join(['Korea', 'Japan']),
210        )
211        assert 'Korea' == sass.and_join(['Korea'])
212        assert '' == sass.and_join([])
213
214
215class CompileTestCase(BaseTestCase):
216
217    def test_compile_required_arguments(self):
218        self.assertRaises(TypeError, sass.compile)
219
220    def test_compile_takes_only_keywords(self):
221        self.assertRaises(TypeError, sass.compile, 'a { color: blue; }')
222
223    def test_compile_exclusive_arguments(self):
224        self.assertRaises(
225            TypeError, sass.compile,
226            string='a { color: blue; }', filename='test/a.scss',
227        )
228        self.assertRaises(
229            TypeError, sass.compile,
230            string='a { color: blue; }', dirname='test/',
231        )
232        self.assertRaises(
233            TypeError,  sass.compile,
234            filename='test/a.scss', dirname='test/',
235        )
236
237    def test_compile_invalid_output_style(self):
238        self.assertRaises(
239            TypeError, sass.compile,
240            string='a { color: blue; }',
241            output_style=['compact'],
242        )
243        self.assertRaises(
244            TypeError,  sass.compile,
245            string='a { color: blue; }', output_style=123j,
246        )
247        self.assertRaises(
248            ValueError,  sass.compile,
249            string='a { color: blue; }', output_style='invalid',
250        )
251
252    def test_compile_invalid_source_comments(self):
253        self.assertRaises(
254            TypeError, sass.compile,
255            string='a { color: blue; }',
256            source_comments=['line_numbers'],
257        )
258        self.assertRaises(
259            TypeError,  sass.compile,
260            string='a { color: blue; }', source_comments=123j,
261        )
262        self.assertRaises(
263            TypeError,  sass.compile,
264            string='a { color: blue; }',
265            source_comments='invalid',
266        )
267
268    def test_compile_disallows_arbitrary_arguments(self):
269        for args in (
270                {'string': 'a{b:c}'},
271                {'filename': 'test/a.scss'},
272                {'dirname': ('test', '/dev/null')},
273        ):
274            with pytest.raises(TypeError) as excinfo:
275                sass.compile(herp='derp', harp='darp', **args)
276            msg, = excinfo.value.args
277            assert msg == (
278                "compile() got unexpected keyword argument(s) 'harp', 'herp'"
279            )
280
281    def test_compile_string(self):
282        actual = sass.compile(string='a { b { color: blue; } }')
283        assert actual == 'a b {\n  color: blue; }\n'
284        commented = sass.compile(
285            string='''a {
286            b { color: blue; }
287            color: red;
288        }''', source_comments=True,
289        )
290        assert commented == '''/* line 1, stdin */
291a {
292  color: red; }
293  /* line 2, stdin */
294  a b {
295    color: blue; }
296'''
297        actual = sass.compile(string=u'a { color: blue; } /* 유니코드 */')
298        self.assertEqual(
299            u'''@charset "UTF-8";
300a {
301  color: blue; }
302
303/* 유니코드 */
304''',
305            actual,
306        )
307        self.assertRaises(
308            sass.CompileError, sass.compile,
309            string='a { b { color: blue; }',
310        )
311        # sass.CompileError should be a subtype of ValueError
312        self.assertRaises(
313            ValueError, sass.compile,
314            string='a { b { color: blue; }',
315        )
316        self.assertRaises(TypeError, sass.compile, string=1234)
317        self.assertRaises(TypeError, sass.compile, string=[])
318
319    def test_compile_string_sass_style(self):
320        actual = sass.compile(
321            string='a\n\tb\n\t\tcolor: blue;',
322            indented=True,
323        )
324        assert actual == 'a b {\n  color: blue; }\n'
325
326    def test_compile_file_sass_style(self):
327        actual = sass.compile(filename='test/h.sass')
328        assert actual == 'a b {\n  color: blue; }\n'
329
330    def test_importer_one_arg(self):
331        """Demonstrates one-arg importers + chaining."""
332        def importer_returning_one_argument(path):
333            assert type(path) is text_type
334            return (
335                # Trigger the import of an actual file
336                ('test/b.scss',),
337                (path, '.{0}-one-arg {{ color: blue; }}'.format(path)),
338            )
339
340        ret = sass.compile(
341            string="@import 'foo';",
342            importers=((0, importer_returning_one_argument),),
343            output_style='compressed',
344        )
345        assert ret == 'b i{font-size:20px}.foo-one-arg{color:blue}\n'
346
347    def test_importer_prev_path(self):
348        def importer(path, prev):
349            assert path in ('a', 'b')
350            if path == 'a':
351                assert prev == 'stdin'
352                return ((path, '@import "b";'),)
353            elif path == 'b':
354                assert prev == 'a'
355                return ((path, 'a { color: red; }'),)
356
357        ret = sass.compile(
358            string='@import "a";',
359            importers=((0, importer),),
360            output_style='compressed',
361        )
362        assert ret == 'a{color:red}\n'
363
364    def test_importer_prev_path_partial(self):
365        def importer(a_css, path, prev):
366            assert path in ('a', 'b')
367            if path == 'a':
368                assert prev == 'stdin'
369                return ((path, '@import "b";'),)
370            elif path == 'b':
371                assert prev == 'a'
372                return ((path, a_css),)
373
374        ret = sass.compile(
375            string='@import "a";',
376            importers=((0, functools.partial(importer, 'a { color: red; }')),),
377            output_style='compressed',
378        )
379        assert ret == 'a{color:red}\n'
380
381    def test_importer_does_not_handle_returns_None(self):
382        def importer_one(path):
383            if path == 'one':
384                return ((path, 'a { color: red; }'),)
385
386        def importer_two(path):
387            assert path == 'two'
388            return ((path, 'b { color: blue; }'),)
389
390        ret = sass.compile(
391            string='@import "one"; @import "two";',
392            importers=((0, importer_one), (0, importer_two)),
393            output_style='compressed',
394        )
395        assert ret == 'a{color:red}b{color:blue}\n'
396
397    def test_importers_other_iterables(self):
398        def importer_one(path):
399            if path == 'one':
400                # Need to do this to avoid returning empty generator
401                def gen():
402                    yield (path, 'a { color: red; }')
403                    yield (path + 'other', 'b { color: orange; }')
404                return gen()
405
406        def importer_two(path):
407            assert path == 'two'
408            # List of lists
409            return [
410                [path, 'c { color: yellow; }'],
411                [path + 'other', 'd { color: green; }'],
412            ]
413
414        ret = sass.compile(
415            string='@import "one"; @import "two";',
416            # Importers can also be lists
417            importers=[[0, importer_one], [0, importer_two]],
418            output_style='compressed',
419        )
420        assert ret == (
421            'a{color:red}b{color:orange}c{color:yellow}d{color:green}\n'
422        )
423
424    def test_importers_srcmap(self):
425        def importer_with_srcmap(path):
426            return (
427                (
428                    path,
429                    'a { color: red; }',
430                    json.dumps({
431                        "version": 3,
432                        "sources": [
433                            path + ".db",
434                        ],
435                        "mappings": ";AAAA,CAAC,CAAC;EAAE,KAAK,EAAE,GAAI,GAAI",
436                    }),
437                ),
438            )
439
440        # This exercises the code, but I don't know what the outcome is
441        # supposed to be.
442        ret = sass.compile(
443            string='@import "test";',
444            importers=((0, importer_with_srcmap),),
445            output_style='compressed',
446        )
447        assert ret == 'a{color:red}\n'
448
449    def test_importers_raises_exception(self):
450        def importer(path):
451            raise ValueError('Bad path: {}'.format(path))
452
453        with assert_raises_compile_error(
454            RegexMatcher(
455                    r'^Error: \n'
456                    r'       Traceback \(most recent call last\):\n'
457                    r'.+'
458                    r'ValueError: Bad path: hi\n'
459                    r'        on line 1:9 of stdin\n'
460                    r'>> @import "hi";\n'
461                    r'   --------\^\n',
462            ),
463        ):
464            sass.compile(string='@import "hi";', importers=((0, importer),))
465
466    def test_importer_returns_wrong_tuple_size_zero(self):
467        def importer(path):
468            return ((),)
469
470        with assert_raises_compile_error(
471            RegexMatcher(
472                    r'^Error: \n'
473                    r'       Traceback \(most recent call last\):\n'
474                    r'.+'
475                    r'ValueError: Expected importer result to be a tuple of '
476                    r'length \(1, 2, 3\) but got 0: \(\)\n'
477                    r'        on line 1:9 of stdin\n'
478                    r'>> @import "hi";\n'
479                    r'   --------\^\n',
480            ),
481        ):
482            sass.compile(string='@import "hi";', importers=((0, importer),))
483
484    def test_importer_returns_wrong_tuple_size_too_big(self):
485        def importer(path):
486            return (('a', 'b', 'c', 'd'),)
487
488        with assert_raises_compile_error(
489            RegexMatcher(
490                    r'^Error: \n'
491                    r'       Traceback \(most recent call last\):\n'
492                    r'.+'
493                    r'ValueError: Expected importer result to be a tuple of '
494                    r"length \(1, 2, 3\) but got 4: \('a', 'b', 'c', 'd'\)\n"
495                    r'        on line 1:9 of stdin\n'
496                    r'>> @import "hi";\n'
497                    r'   --------\^\n',
498            ),
499        ):
500            sass.compile(string='@import "hi";', importers=((0, importer),))
501
502    def test_compile_string_deprecated_source_comments_line_numbers(self):
503        source = '''a {
504            b { color: blue; }
505            color: red;
506        }'''
507        expected = sass.compile(string=source, source_comments=True)
508        with pytest.warns(FutureWarning):
509            actual = sass.compile(
510                string=source,
511                source_comments='line_numbers',
512            )
513        assert expected == actual
514
515    def test_compile_filename(self):
516        actual = sass.compile(filename='test/a.scss')
517        assert actual == A_EXPECTED_CSS
518        actual = sass.compile(filename='test/c.scss')
519        assert actual == C_EXPECTED_CSS
520        actual = sass.compile(filename='test/d.scss')
521        assert D_EXPECTED_CSS == actual
522        actual = sass.compile(filename='test/e.scss')
523        assert actual == E_EXPECTED_CSS
524        self.assertRaises(
525            IOError, sass.compile,
526            filename='test/not-exist.sass',
527        )
528        self.assertRaises(TypeError, sass.compile, filename=1234)
529        self.assertRaises(TypeError, sass.compile, filename=[])
530
531    def test_compile_source_map(self):
532        filename = 'test/a.scss'
533        actual, source_map = sass.compile(
534            filename=filename,
535            source_map_filename='a.scss.css.map',
536        )
537        assert A_EXPECTED_CSS_WITH_MAP == actual
538        self.assert_source_map_equal(A_EXPECTED_MAP, source_map)
539
540    def test_compile_source_map_root(self):
541        filename = 'test/a.scss'
542        actual, source_map = sass.compile(
543            filename=filename,
544            source_map_filename='a.scss.css.map',
545            source_map_root='/',
546        )
547        assert A_EXPECTED_CSS_WITH_MAP == actual
548        expected = dict(A_EXPECTED_MAP, sourceRoot='/')
549        self.assert_source_map_equal(expected, source_map)
550
551    def test_compile_source_map_omit_source_url(self):
552        filename = 'test/a.scss'
553        actual, source_map = sass.compile(
554            filename=filename,
555            source_map_filename='a.scss.css.map',
556            omit_source_map_url=True,
557        )
558        assert A_EXPECTED_CSS == actual
559        self.assert_source_map_equal(A_EXPECTED_MAP, source_map)
560
561    def test_compile_source_map_source_map_contents(self):
562        filename = 'test/a.scss'
563        actual, source_map = sass.compile(
564            filename=filename,
565            source_map_filename='a.scss.css.map',
566            source_map_contents=True,
567        )
568        assert A_EXPECTED_CSS_WITH_MAP == actual
569        self.assert_source_map_equal(A_EXPECTED_MAP_CONTENTS, source_map)
570
571    def test_compile_source_map_embed(self):
572        filename = 'test/a.scss'
573        actual, source_map = sass.compile(
574            filename=filename,
575            source_map_filename='a.scss.css.map',
576            source_map_embed=True,
577        )
578        self.assert_source_map_embed(A_EXPECTED_MAP, actual)
579
580    def test_compile_source_map_deprecated_source_comments_map(self):
581        filename = 'test/a.scss'
582        expected, expected_map = sass.compile(
583            filename=filename,
584            source_map_filename='a.scss.css.map',
585        )
586        with pytest.warns(FutureWarning):
587            actual, actual_map = sass.compile(
588                filename=filename,
589                source_comments='map',
590                source_map_filename='a.scss.css.map',
591            )
592        assert expected == actual
593        self.assert_source_map_equal(expected_map, actual_map)
594
595    def test_compile_with_precision(self):
596        actual = sass.compile(filename='test/g.scss')
597        assert actual == G_EXPECTED_CSS
598        actual = sass.compile(filename='test/g.scss', precision=8)
599        assert actual == G_EXPECTED_CSS_WITH_PRECISION_8
600
601    def test_regression_issue_2(self):
602        actual = sass.compile(
603            string='''
604            @media (min-width: 980px) {
605                a {
606                    color: red;
607                }
608            }
609        ''',
610        )
611        normalized = re.sub(r'\s+', '', actual)
612        assert normalized == '@media(min-width:980px){a{color:red;}}'
613
614    def test_regression_issue_11(self):
615        actual = sass.compile(
616            string='''
617            $foo: 3;
618            @media (max-width: $foo) {
619                body { color: black; }
620            }
621        ''',
622        )
623        normalized = re.sub(r'\s+', '', actual)
624        assert normalized == '@media(max-width:3){body{color:black;}}'
625
626
627class BuilderTestCase(BaseTestCase):
628
629    def setUp(self):
630        self.temp_path = tempfile.mkdtemp()
631        self.sass_path = os.path.join(self.temp_path, 'sass')
632        self.css_path = os.path.join(self.temp_path, 'css')
633        shutil.copytree('test', self.sass_path)
634
635    def tearDown(self):
636        shutil.rmtree(self.temp_path)
637
638    def test_builder_build_directory(self):
639        css_path = self.css_path
640        result_files = build_directory(self.sass_path, css_path)
641        assert len(result_files) == 8
642        assert 'a.scss.css' == result_files['a.scss']
643        with io.open(
644            os.path.join(css_path, 'a.scss.css'), encoding='UTF-8',
645        ) as f:
646            css = f.read()
647        assert A_EXPECTED_CSS == css
648        assert 'b.scss.css' == result_files['b.scss']
649        with io.open(
650            os.path.join(css_path, 'b.scss.css'), encoding='UTF-8',
651        ) as f:
652            css = f.read()
653        assert B_EXPECTED_CSS == css
654        assert 'c.scss.css' == result_files['c.scss']
655        with io.open(
656            os.path.join(css_path, 'c.scss.css'), encoding='UTF-8',
657        ) as f:
658            css = f.read()
659        assert C_EXPECTED_CSS == css
660        assert 'd.scss.css' == result_files['d.scss']
661        with io.open(
662            os.path.join(css_path, 'd.scss.css'), encoding='UTF-8',
663        ) as f:
664            css = f.read()
665        assert D_EXPECTED_CSS == css
666        assert 'e.scss.css' == result_files['e.scss']
667        with io.open(
668            os.path.join(css_path, 'e.scss.css'), encoding='UTF-8',
669        ) as f:
670            css = f.read()
671        assert E_EXPECTED_CSS == css
672        self.assertEqual(
673            os.path.join('subdir', 'recur.scss.css'),
674            result_files[os.path.join('subdir', 'recur.scss')],
675        )
676        with io.open(
677            os.path.join(css_path, 'g.scss.css'), encoding='UTF-8',
678        ) as f:
679            css = f.read()
680        assert G_EXPECTED_CSS == css
681        self.assertEqual(
682            os.path.join('subdir', 'recur.scss.css'),
683            result_files[os.path.join('subdir', 'recur.scss')],
684        )
685        assert 'h.sass.css' == result_files['h.sass']
686        with io.open(
687            os.path.join(css_path, 'h.sass.css'), encoding='UTF-8',
688        ) as f:
689            css = f.read()
690        assert H_EXPECTED_CSS == css
691        with io.open(
692            os.path.join(css_path, 'subdir', 'recur.scss.css'),
693            encoding='UTF-8',
694        ) as f:
695            css = f.read()
696        assert SUBDIR_RECUR_EXPECTED_CSS == css
697
698    def test_output_style(self):
699        css_path = self.css_path
700        result_files = build_directory(
701            self.sass_path, css_path,
702            output_style='compressed',
703        )
704        assert len(result_files) == 8
705        assert 'a.scss.css' == result_files['a.scss']
706        with io.open(
707            os.path.join(css_path, 'a.scss.css'), encoding='UTF-8',
708        ) as f:
709            css = f.read()
710        self.assertEqual(
711            'body{background-color:green}body a{color:blue}\n',
712            css,
713        )
714
715
716class ManifestTestCase(BaseTestCase):
717
718    def test_normalize_manifests(self):
719        with pytest.warns(FutureWarning) as warninfo:
720            manifests = Manifest.normalize_manifests({
721                'package': 'sass/path',
722                'package.name': ('sass/path', 'css/path'),
723                'package.name2': Manifest('sass/path', 'css/path'),
724                'package.name3': {
725                    'sass_path': 'sass/path',
726                    'css_path': 'css/path',
727                    'strip_extension': True,
728                },
729            })
730        assert len(warninfo) == 3
731        assert len(manifests) == 4
732        assert isinstance(manifests['package'], Manifest)
733        assert manifests['package'].sass_path == 'sass/path'
734        assert manifests['package'].css_path == 'sass/path'
735        assert isinstance(manifests['package.name'], Manifest)
736        assert manifests['package.name'].sass_path == 'sass/path'
737        assert manifests['package.name'].css_path == 'css/path'
738        assert isinstance(manifests['package.name2'], Manifest)
739        assert manifests['package.name2'].sass_path == 'sass/path'
740        assert manifests['package.name2'].css_path == 'css/path'
741        assert isinstance(manifests['package.name3'], Manifest)
742        assert manifests['package.name3'].sass_path == 'sass/path'
743        assert manifests['package.name3'].css_path == 'css/path'
744        assert manifests['package.name3'].strip_extension is True
745
746    def test_build_one(self):
747        with tempdir() as d:
748            src_path = os.path.join(d, 'test')
749
750            shutil.copytree('test', src_path)
751            with pytest.warns(FutureWarning):
752                m = Manifest(sass_path='test', css_path='css')
753
754            m.build_one(d, 'a.scss')
755            with open(os.path.join(d, 'css', 'a.scss.css')) as f:
756                assert A_EXPECTED_CSS == f.read()
757            m.build_one(d, 'b.scss', source_map=True)
758            with io.open(
759                os.path.join(d, 'css', 'b.scss.css'), encoding='UTF-8',
760            ) as f:
761                assert f.read() == _map_in_output_dir(B_EXPECTED_CSS_WITH_MAP)
762            self.assert_source_map_file(
763                {
764                    'version': 3,
765                    'file': 'b.scss.css',
766                    'sources': ['../test/b.scss'],
767                    'names': [],
768                    'mappings': (
769                        'AAAA,AACE,CADD,CACC,CAAC,CAAC;EACA,SAAS,EAAE,IAAI,'
770                        'GAChB'
771                    ),
772                },
773                os.path.join(d, 'css', 'b.scss.css.map'),
774            )
775            m.build_one(d, 'd.scss', source_map=True)
776            with io.open(
777                os.path.join(d, 'css', 'd.scss.css'), encoding='UTF-8',
778            ) as f:
779                assert f.read() == _map_in_output_dir(D_EXPECTED_CSS_WITH_MAP)
780            self.assert_source_map_file(
781                {
782                    'version': 3,
783                    'file': 'd.scss.css',
784                    'sources': ['../test/d.scss'],
785                    'names': [],
786                    'mappings': (
787                        ';AAKA,AAAA,IAAI,CAAC;EAHH,gBAAgB,EAAE,KAAK,GAQxB;'
788                        'EALD,AAEE,IAFE,CAEF,CAAC,CAAC;IACA,IAAI,EAAE,kBAAkB,'
789                        'GACzB'
790                    ),
791                },
792                os.path.join(d, 'css', 'd.scss.css.map'),
793            )
794
795
796def test_manifest_build_one_strip_extension(tmpdir):
797    src = tmpdir.join('test').ensure_dir()
798    src.join('a.scss').write('a{b: c;}')
799
800    m = Manifest(sass_path='test', css_path='css', strip_extension=True)
801    m.build_one(str(tmpdir), 'a.scss')
802
803    assert tmpdir.join('css/a.css').read() == 'a {\n  b: c; }\n'
804
805
806def test_manifest_build_strip_extension(tmpdir):
807    src = tmpdir.join('test').ensure_dir()
808    src.join('x.scss').write('a{b: c;}')
809
810    m = Manifest(sass_path='test', css_path='css', strip_extension=True)
811    m.build(package_dir=str(tmpdir))
812
813    assert tmpdir.join('css/x.css').read() == 'a {\n  b: c; }\n'
814
815
816class WsgiTestCase(BaseTestCase):
817
818    @staticmethod
819    def sample_wsgi_app(environ, start_response):
820        start_response('200 OK', [('Content-Type', 'text/plain')])
821        return environ['PATH_INFO'],
822
823    def test_wsgi_sass_middleware(self):
824        with tempdir() as css_dir:
825            src_dir = os.path.join(css_dir, 'src')
826            shutil.copytree('test', src_dir)
827            with pytest.warns(FutureWarning):
828                app = SassMiddleware(
829                    self.sample_wsgi_app, {
830                        __name__: (src_dir, css_dir, '/static'),
831                    },
832                )
833            client = Client(app, Response)
834            r = client.get('/asdf')
835            assert r.status_code == 200
836            self.assertEqual(b'/asdf', r.data)
837            assert r.mimetype == 'text/plain'
838            r = client.get('/static/a.scss.css')
839            assert r.status_code == 200
840            self.assertEqual(
841                b(_map_in_output_dir(A_EXPECTED_CSS_WITH_MAP)),
842                r.data,
843            )
844            assert r.mimetype == 'text/css'
845            r = client.get('/static/not-exists.sass.css')
846            assert r.status_code == 200
847            self.assertEqual(b'/static/not-exists.sass.css', r.data)
848            assert r.mimetype == 'text/plain'
849
850    def test_wsgi_sass_middleware_without_extension(self):
851        with tempdir() as css_dir:
852            src_dir = os.path.join(css_dir, 'src')
853            shutil.copytree('test', src_dir)
854            app = SassMiddleware(
855                self.sample_wsgi_app, {
856                    __name__: {
857                        'sass_path': src_dir,
858                        'css_path': css_dir,
859                        'wsgi_path': '/static',
860                        'strip_extension': True,
861                    },
862                },
863            )
864            client = Client(app, Response)
865            r = client.get('/static/a.css')
866            assert r.status_code == 200
867            expected = A_EXPECTED_CSS_WITH_MAP
868            expected = expected.replace('.scss.css', '.css')
869            expected = _map_in_output_dir(expected)
870            self.assertEqual(expected.encode(), r.data)
871            assert r.mimetype == 'text/css'
872
873    def test_wsgi_sass_middleware_without_extension_sass(self):
874        with tempdir() as css_dir:
875            app = SassMiddleware(
876                self.sample_wsgi_app, {
877                    __name__: {
878                        'sass_path': 'test',
879                        'css_path': css_dir,
880                        'wsgi_path': '/static',
881                        'strip_extension': True,
882                    },
883                },
884            )
885            client = Client(app, Response)
886            r = client.get('/static/h.css')
887            assert r.status_code == 200
888            expected = (
889                'a b {\n  color: blue; }\n\n'
890                '/*# sourceMappingURL=h.css.map */'
891            )
892            self.assertEqual(expected.encode(), r.data)
893            assert r.mimetype == 'text/css'
894
895
896class DistutilsTestCase(BaseTestCase):
897
898    def tearDown(self):
899        for filename in self.list_built_css():
900            os.remove(filename)
901
902    def css_path(self, *args):
903        return os.path.join(
904            os.path.dirname(__file__),
905            'testpkg', 'testpkg', 'static', 'css',
906            *args
907        )
908
909    def list_built_css(self):
910        return glob.glob(self.css_path('*.scss.css'))
911
912    def build_sass(self, *args):
913        testpkg_path = os.path.join(os.path.dirname(__file__), 'testpkg')
914        return subprocess.call(
915            [sys.executable, 'setup.py', 'build_sass'] + list(args),
916            cwd=os.path.abspath(testpkg_path),
917        )
918
919    def test_build_sass(self):
920        rv = self.build_sass()
921        assert rv == 0
922        self.assertEqual(
923            ['a.scss.css'],
924            list(map(os.path.basename, self.list_built_css())),
925        )
926        with open(self.css_path('a.scss.css')) as f:
927            self.assertEqual(
928                'p a {\n  color: red; }\n\np b {\n  color: blue; }\n',
929                f.read(),
930            )
931
932    def test_output_style(self):
933        rv = self.build_sass('--output-style', 'compressed')
934        assert rv == 0
935        with open(self.css_path('a.scss.css')) as f:
936            self.assertEqual(
937                'p a{color:red}p b{color:blue}\n',
938                f.read(),
939            )
940
941
942class SasscTestCase(BaseTestCase):
943
944    def setUp(self):
945        self.out = StringIO()
946        self.err = StringIO()
947
948    def test_no_args(self):
949        exit_code = pysassc.main(['pysassc'], self.out, self.err)
950        assert exit_code == 2
951        err = self.err.getvalue()
952        assert err.strip().endswith('error: too few arguments'), \
953            'actual error message is: ' + repr(err)
954        assert '' == self.out.getvalue()
955
956    def test_three_args(self):
957        exit_code = pysassc.main(
958            ['pysassc', 'a.scss', 'b.scss', 'c.scss'],
959            self.out, self.err,
960        )
961        assert exit_code == 2
962        err = self.err.getvalue()
963        assert err.strip().endswith('error: too many arguments'), \
964            'actual error message is: ' + repr(err)
965        assert self.out.getvalue() == ''
966
967    def test_pysassc_stdout(self):
968        exit_code = pysassc.main(
969            ['pysassc', 'test/a.scss'],
970            self.out, self.err,
971        )
972        assert exit_code == 0
973        assert self.err.getvalue() == ''
974        assert A_EXPECTED_CSS.strip() == self.out.getvalue().strip()
975
976    def test_sassc_stdout(self):
977        with pytest.warns(FutureWarning) as warninfo:
978            exit_code = sassc.main(
979                ['sassc', 'test/a.scss'],
980                self.out, self.err,
981            )
982        assert 'use `pysassc`' in warninfo[0].message.args[0]
983        assert exit_code == 0
984        assert self.err.getvalue() == ''
985        assert A_EXPECTED_CSS.strip() == self.out.getvalue().strip()
986
987    def test_pysassc_output(self):
988        fd, tmp = tempfile.mkstemp('.css')
989        try:
990            os.close(fd)
991            exit_code = pysassc.main(
992                ['pysassc', 'test/a.scss', tmp],
993                self.out, self.err,
994            )
995            assert exit_code == 0
996            assert self.err.getvalue() == ''
997            assert self.out.getvalue() == ''
998            with io.open(tmp, encoding='UTF-8', newline='') as f:
999                assert A_EXPECTED_CSS.strip() == f.read().strip()
1000        finally:
1001            os.remove(tmp)
1002
1003    def test_pysassc_output_unicode(self):
1004        fd, tmp = tempfile.mkstemp('.css')
1005        try:
1006            os.close(fd)
1007            exit_code = pysassc.main(
1008                ['pysassc', 'test/d.scss', tmp],
1009                self.out, self.err,
1010            )
1011            assert exit_code == 0
1012            assert self.err.getvalue() == ''
1013            assert self.out.getvalue() == ''
1014            with io.open(tmp, encoding='UTF-8') as f:
1015                assert D_EXPECTED_CSS.strip() == f.read().strip()
1016        finally:
1017            os.remove(tmp)
1018
1019    def test_pysassc_source_map_without_css_filename(self):
1020        exit_code = pysassc.main(
1021            ['pysassc', '-m', 'a.scss'],
1022            self.out, self.err,
1023        )
1024        assert exit_code == 2
1025        err = self.err.getvalue()
1026        assert err.strip().endswith(
1027            'error: -m/-g/--sourcemap requires '
1028            'the second argument, the output css '
1029            'filename.',
1030        ), \
1031            'actual error message is: ' + repr(err)
1032        assert self.out.getvalue() == ''
1033
1034    def test_pysassc_warning_import_extensions(self):
1035        with pytest.warns(FutureWarning):
1036            pysassc.main(
1037                ['pysassc', os.devnull, '--import-extensions', '.css'],
1038            )
1039
1040
1041@contextlib.contextmanager
1042def tempdir():
1043    tmpdir = tempfile.mkdtemp()
1044    try:
1045        yield tmpdir
1046    finally:
1047        shutil.rmtree(tmpdir)
1048
1049
1050def write_file(filename, contents):
1051    with open(filename, 'w') as f:
1052        f.write(contents)
1053
1054
1055class CompileDirectoriesTest(unittest.TestCase):
1056
1057    def test_directory_does_not_exist(self):
1058        with pytest.raises(OSError):
1059            sass.compile(dirname=('i_dont_exist_lol', 'out'))
1060
1061    def test_successful(self):
1062        with tempdir() as tmpdir:
1063            input_dir = os.path.join(tmpdir, 'input')
1064            output_dir = os.path.join(tmpdir, 'output')
1065            os.makedirs(os.path.join(input_dir, 'foo'))
1066            write_file(
1067                os.path.join(input_dir, 'f1.scss'),
1068                'a { b { width: 100%; } }',
1069            )
1070            write_file(
1071                os.path.join(input_dir, 'foo/f2.scss'),
1072                'foo { width: 100%; }',
1073            )
1074            # Make sure we don't compile non-scss files
1075            write_file(os.path.join(input_dir, 'baz.txt'), 'Hello der')
1076
1077            sass.compile(dirname=(input_dir, output_dir))
1078            assert os.path.exists(output_dir)
1079            assert os.path.exists(os.path.join(output_dir, 'foo'))
1080            assert os.path.exists(os.path.join(output_dir, 'f1.css'))
1081            assert os.path.exists(os.path.join(output_dir, 'foo/f2.css'))
1082            assert not os.path.exists(os.path.join(output_dir, 'baz.txt'))
1083
1084            with open(os.path.join(output_dir, 'f1.css')) as f:
1085                contentsf1 = f.read()
1086            with open(os.path.join(output_dir, 'foo/f2.css')) as f:
1087                contentsf2 = f.read()
1088            assert contentsf1 == 'a b {\n  width: 100%; }\n'
1089            assert contentsf2 == 'foo {\n  width: 100%; }\n'
1090
1091    def test_compile_directories_unicode(self):
1092        with tempdir() as tmpdir:
1093            input_dir = os.path.join(tmpdir, 'input')
1094            output_dir = os.path.join(tmpdir, 'output')
1095            os.makedirs(input_dir)
1096            with io.open(
1097                os.path.join(input_dir, 'test.scss'), 'w', encoding='UTF-8',
1098            ) as f:
1099                f.write(u'a { content: "☃"; }')
1100            # Raised a UnicodeEncodeError in py2 before #82 (issue #72)
1101            # Also raised a UnicodeEncodeError in py3 if the default encoding
1102            # couldn't represent it (such as cp1252 on windows)
1103            sass.compile(dirname=(input_dir, output_dir))
1104            assert os.path.exists(os.path.join(output_dir, 'test.css'))
1105
1106    def test_ignores_underscored_files(self):
1107        with tempdir() as tmpdir:
1108            input_dir = os.path.join(tmpdir, 'input')
1109            output_dir = os.path.join(tmpdir, 'output')
1110            os.mkdir(input_dir)
1111            write_file(os.path.join(input_dir, 'f1.scss'), '@import "f2";')
1112            write_file(os.path.join(input_dir, '_f2.scss'), 'a{color:red}')
1113
1114            sass.compile(dirname=(input_dir, output_dir))
1115            assert not os.path.exists(os.path.join(output_dir, '_f2.css'))
1116
1117    def test_error(self):
1118        with tempdir() as tmpdir:
1119            input_dir = os.path.join(tmpdir, 'input')
1120            os.makedirs(input_dir)
1121            write_file(os.path.join(input_dir, 'bad.scss'), 'a {')
1122
1123            with pytest.raises(sass.CompileError) as excinfo:
1124                sass.compile(
1125                    dirname=(input_dir, os.path.join(tmpdir, 'output')),
1126                )
1127            msg, = excinfo.value.args
1128            assert msg.startswith('Error: Invalid CSS after ')
1129
1130
1131class SassFunctionTest(unittest.TestCase):
1132
1133    def test_from_lambda(self):
1134        lambda_ = lambda abc, d: None  # pragma: no branch  # noqa: E731
1135        sf = sass.SassFunction.from_lambda('func_name', lambda_)
1136        assert 'func_name' == sf.name
1137        assert ('$abc', '$d') == sf.arguments
1138        assert sf.callable_ is lambda_
1139
1140    def test_from_named_function(self):
1141        sf = sass.SassFunction.from_named_function(identity)
1142        assert 'identity' == sf.name
1143        assert ('$x',) == sf.arguments
1144        assert sf.callable_ is identity
1145
1146    def test_sigature(self):
1147        sf = sass.SassFunction(  # pragma: no branch (doesn't run lambda)
1148            'func-name',
1149            ('$a', '$bc', '$d'),
1150            lambda a, bc, d: None,
1151        )
1152        assert 'func-name($a, $bc, $d)' == sf.signature
1153        assert sf.signature == str(sf)
1154
1155
1156@pytest.mark.parametrize(  # pragma: no branch (never runs lambdas)
1157    'func',
1158    (lambda bar='womp': None, lambda *args: None, lambda **kwargs: None),
1159)
1160def test_sass_func_type_errors(func):
1161    with pytest.raises(TypeError):
1162        sass.SassFunction.from_lambda('funcname', func)
1163
1164
1165class SassTypesTest(unittest.TestCase):
1166    def test_number_no_conversion(self):
1167        num = sass.SassNumber(123., u'px')
1168        assert type(num.value) is float, type(num.value)
1169        assert type(num.unit) is text_type, type(num.unit)
1170
1171    def test_number_conversion(self):
1172        num = sass.SassNumber(123, b'px')
1173        assert type(num.value) is float, type(num.value)
1174        assert type(num.unit) is text_type, type(num.unit)
1175
1176    def test_color_no_conversion(self):
1177        color = sass.SassColor(1., 2., 3., .5)
1178        assert type(color.r) is float, type(color.r)
1179        assert type(color.g) is float, type(color.g)
1180        assert type(color.b) is float, type(color.b)
1181        assert type(color.a) is float, type(color.a)
1182
1183    def test_color_conversion(self):
1184        color = sass.SassColor(1, 2, 3, 1)
1185        assert type(color.r) is float, type(color.r)
1186        assert type(color.g) is float, type(color.g)
1187        assert type(color.b) is float, type(color.b)
1188        assert type(color.a) is float, type(color.a)
1189
1190    def test_sass_list_no_conversion(self):
1191        lst = sass.SassList(('foo', 'bar'), sass.SASS_SEPARATOR_COMMA)
1192        assert type(lst.items) is tuple, type(lst.items)
1193        assert lst.separator is sass.SASS_SEPARATOR_COMMA, lst.separator
1194
1195    def test_sass_list_conversion(self):
1196        lst = sass.SassList(['foo', 'bar'], sass.SASS_SEPARATOR_SPACE)
1197        assert type(lst.items) is tuple, type(lst.items)
1198        assert lst.separator is sass.SASS_SEPARATOR_SPACE, lst.separator
1199
1200    def test_sass_warning_no_conversion(self):
1201        warn = sass.SassWarning(u'error msg')
1202        assert type(warn.msg) is text_type, type(warn.msg)
1203
1204    def test_sass_warning_no_conversion_bytes_message(self):
1205        warn = sass.SassWarning(b'error msg')
1206        assert type(warn.msg) is text_type, type(warn.msg)
1207
1208    def test_sass_error_no_conversion(self):
1209        err = sass.SassError(u'error msg')
1210        assert type(err.msg) is text_type, type(err.msg)
1211
1212    def test_sass_error_conversion(self):
1213        err = sass.SassError(b'error msg')
1214        assert type(err.msg) is text_type, type(err.msg)
1215
1216
1217def raises():
1218    raise AssertionError('foo')
1219
1220
1221def returns_warning():
1222    return sass.SassWarning('This is a warning')
1223
1224
1225def returns_error():
1226    return sass.SassError('This is an error')
1227
1228
1229def returns_unknown():
1230    """Tuples are a not-supported type."""
1231    return 1, 2, 3
1232
1233
1234def returns_true():
1235    return True
1236
1237
1238def returns_false():
1239    return False
1240
1241
1242def returns_none():
1243    return None
1244
1245
1246def returns_unicode():
1247    return u'☃'
1248
1249
1250def returns_bytes():
1251    return u'☃'.encode('UTF-8')
1252
1253
1254def returns_number():
1255    return sass.SassNumber(5, 'px')
1256
1257
1258def returns_color():
1259    return sass.SassColor(1, 2, 3, .5)
1260
1261
1262def returns_comma_list():
1263    return sass.SassList(('Arial', 'sans-serif'), sass.SASS_SEPARATOR_COMMA)
1264
1265
1266def returns_space_list():
1267    return sass.SassList(('medium', 'none'), sass.SASS_SEPARATOR_SPACE)
1268
1269
1270def returns_bracketed_list():
1271    return sass.SassList(
1272        ('hello', 'ohai'), sass.SASS_SEPARATOR_SPACE, bracketed=True,
1273    )
1274
1275
1276def returns_py_dict():
1277    return {'foo': 'bar'}
1278
1279
1280def returns_map():
1281    return sass.SassMap([('foo', 'bar')])
1282
1283
1284def identity(x):
1285    """This has the side-effect of bubbling any exceptions we failed to process
1286    in C land
1287
1288    """
1289    import sys  # noqa
1290    return x
1291
1292
1293custom_functions = frozenset([
1294    sass.SassFunction('raises', (), raises),
1295    sass.SassFunction('returns_warning', (), returns_warning),
1296    sass.SassFunction('returns_error', (), returns_error),
1297    sass.SassFunction('returns_unknown', (), returns_unknown),
1298    sass.SassFunction('returns_true', (), returns_true),
1299    sass.SassFunction('returns_false', (), returns_false),
1300    sass.SassFunction('returns_none', (), returns_none),
1301    sass.SassFunction('returns_unicode', (), returns_unicode),
1302    sass.SassFunction('returns_bytes', (), returns_bytes),
1303    sass.SassFunction('returns_number', (), returns_number),
1304    sass.SassFunction('returns_color', (), returns_color),
1305    sass.SassFunction('returns_comma_list', (), returns_comma_list),
1306    sass.SassFunction('returns_space_list', (), returns_space_list),
1307    sass.SassFunction('returns_bracketed_list', (), returns_bracketed_list),
1308    sass.SassFunction('returns_py_dict', (), returns_py_dict),
1309    sass.SassFunction('returns_map', (), returns_map),
1310    sass.SassFunction('identity', ('$x',), identity),
1311])
1312
1313custom_function_map = {
1314    'raises': raises,
1315    'returns_warning': returns_warning,
1316    'returns_error': returns_error,
1317    'returns_unknown': returns_unknown,
1318    'returns_true': returns_true,
1319    'returns_false': returns_false,
1320    'returns_none': returns_none,
1321    'returns_unicode': returns_unicode,
1322    'returns_bytes': returns_bytes,
1323    'returns_number': returns_number,
1324    'returns_color': returns_color,
1325    'returns_comma_list': returns_comma_list,
1326    'returns_space_list': returns_space_list,
1327    'returns_bracketed_list': returns_bracketed_list,
1328    'returns_py_dict': returns_py_dict,
1329    'returns_map': returns_map,
1330    'identity': identity,
1331}
1332
1333custom_function_set = frozenset([
1334    raises,
1335    returns_warning,
1336    returns_error,
1337    returns_unknown,
1338    returns_true,
1339    returns_false,
1340    returns_none,
1341    returns_unicode,
1342    returns_bytes,
1343    returns_number,
1344    returns_color,
1345    returns_comma_list,
1346    returns_space_list,
1347    returns_bracketed_list,
1348    returns_py_dict,
1349    returns_map,
1350    identity,
1351])
1352
1353
1354def compile_with_func(s):
1355    result = sass.compile(
1356        string=s,
1357        custom_functions=custom_functions,
1358        output_style='compressed',
1359    )
1360    map_result = sass.compile(
1361        string=s,
1362        custom_functions=custom_function_map,
1363        output_style='compressed',
1364    )
1365    assert result == map_result
1366    set_result = sass.compile(
1367        string=s,
1368        custom_functions=custom_function_set,
1369        output_style='compressed',
1370    )
1371    assert map_result == set_result
1372    return result
1373
1374
1375@contextlib.contextmanager
1376def assert_raises_compile_error(expected):
1377    with pytest.raises(sass.CompileError) as excinfo:
1378        yield
1379    msg, = excinfo.value.args
1380    assert msg == expected, (msg, expected)
1381
1382
1383class RegexMatcher(object):
1384    def __init__(self, reg, flags=None):
1385        self.reg = re.compile(reg, re.MULTILINE | re.DOTALL)
1386
1387    def __eq__(self, other):
1388        return bool(self.reg.match(other))
1389
1390
1391class CustomFunctionsTest(unittest.TestCase):
1392
1393    def test_raises(self):
1394        with assert_raises_compile_error(
1395            RegexMatcher(
1396                    r'^Error: error in C function raises: \n'
1397                    r'       Traceback \(most recent call last\):\n'
1398                    r'.+'
1399                    r'AssertionError: foo\n'
1400                    r'        on line 1:14 of stdin, in function `raises`\n'
1401                    r'        from line 1:14 of stdin\n'
1402                    r'>> a { content: raises\(\); }\n'
1403                    r'   -------------\^\n$',
1404            ),
1405        ):
1406            compile_with_func('a { content: raises(); }')
1407
1408    def test_warning(self):
1409        with assert_raises_compile_error(
1410                'Error: warning in C function returns_warning: '
1411                'This is a warning\n'
1412                '        on line 1:14 of stdin, '
1413                'in function `returns_warning`\n'
1414                '        from line 1:14 of stdin\n'
1415                '>> a { content: returns_warning(); }\n'
1416                '   -------------^\n',
1417        ):
1418            compile_with_func('a { content: returns_warning(); }')
1419
1420    def test_error(self):
1421        with assert_raises_compile_error(
1422                'Error: error in C function returns_error: '
1423                'This is an error\n'
1424                '        on line 1:14 of stdin, in function `returns_error`\n'
1425                '        from line 1:14 of stdin\n'
1426                '>> a { content: returns_error(); }\n'
1427                '   -------------^\n',
1428        ):
1429            compile_with_func('a { content: returns_error(); }')
1430
1431    def test_returns_unknown_object(self):
1432        with assert_raises_compile_error(
1433                'Error: error in C function returns_unknown: '
1434                'Unexpected type: `tuple`.\n'
1435                '       Expected one of:\n'
1436                '       - None\n'
1437                '       - bool\n'
1438                '       - str\n'
1439                '       - SassNumber\n'
1440                '       - SassColor\n'
1441                '       - SassList\n'
1442                '       - dict\n'
1443                '       - SassMap\n'
1444                '       - SassWarning\n'
1445                '       - SassError\n'
1446                '        on line 1:14 of stdin, '
1447                'in function `returns_unknown`\n'
1448                '        from line 1:14 of stdin\n'
1449                '>> a { content: returns_unknown(); }\n'
1450                '   -------------^\n',
1451        ):
1452            compile_with_func('a { content: returns_unknown(); }')
1453
1454    def test_none(self):
1455        self.assertEqual(
1456            compile_with_func('a {color: #fff; content: returns_none();}'),
1457            'a{color:#fff}\n',
1458        )
1459
1460    def test_true(self):
1461        self.assertEqual(
1462            compile_with_func('a { content: returns_true(); }'),
1463            'a{content:true}\n',
1464        )
1465
1466    def test_false(self):
1467        self.assertEqual(
1468            compile_with_func('a { content: returns_false(); }'),
1469            'a{content:false}\n',
1470        )
1471
1472    def test_unicode(self):
1473        self.assertEqual(
1474            compile_with_func('a { content: returns_unicode(); }'),
1475            u'\ufeffa{content:☃}\n',
1476        )
1477
1478    def test_bytes(self):
1479        self.assertEqual(
1480            compile_with_func('a { content: returns_bytes(); }'),
1481            u'\ufeffa{content:☃}\n',
1482        )
1483
1484    def test_number(self):
1485        self.assertEqual(
1486            compile_with_func('a { width: returns_number(); }'),
1487            'a{width:5px}\n',
1488        )
1489
1490    def test_color(self):
1491        self.assertEqual(
1492            compile_with_func('a { color: returns_color(); }'),
1493            'a{color:rgba(1,2,3,0.5)}\n',
1494        )
1495
1496    def test_comma_list(self):
1497        self.assertEqual(
1498            compile_with_func('a { font-family: returns_comma_list(); }'),
1499            'a{font-family:Arial,sans-serif}\n',
1500        )
1501
1502    def test_space_list(self):
1503        self.assertEqual(
1504            compile_with_func('a { border-right: returns_space_list(); }'),
1505            'a{border-right:medium none}\n',
1506        )
1507
1508    def test_bracketed_list(self):
1509        self.assertEqual(
1510            compile_with_func('a { content: returns_bracketed_list(); }'),
1511            'a{content:[hello ohai]}\n',
1512        )
1513
1514    def test_py_dict(self):
1515        self.assertEqual(
1516            compile_with_func(
1517                'a { content: map-get(returns_py_dict(), foo); }',
1518            ),
1519            'a{content:bar}\n',
1520        )
1521
1522    def test_map(self):
1523        self.assertEqual(
1524            compile_with_func(
1525                'a { content: map-get(returns_map(), foo); }',
1526            ),
1527            'a{content:bar}\n',
1528        )
1529
1530    def test_identity_none(self):
1531        self.assertEqual(
1532            compile_with_func(
1533                'a {color: #fff; content: identity(returns_none());}',
1534            ),
1535            'a{color:#fff}\n',
1536        )
1537
1538    def test_identity_true(self):
1539        self.assertEqual(
1540            compile_with_func('a { content: identity(returns_true()); }'),
1541            'a{content:true}\n',
1542        )
1543
1544    def test_identity_false(self):
1545        self.assertEqual(
1546            compile_with_func('a { content: identity(returns_false()); }'),
1547            'a{content:false}\n',
1548        )
1549
1550    def test_identity_strings(self):
1551        self.assertEqual(
1552            compile_with_func('a { content: identity(returns_unicode()); }'),
1553            u'\ufeffa{content:☃}\n',
1554        )
1555
1556    def test_identity_number(self):
1557        self.assertEqual(
1558            compile_with_func('a { width: identity(returns_number()); }'),
1559            'a{width:5px}\n',
1560        )
1561
1562    def test_identity_color(self):
1563        self.assertEqual(
1564            compile_with_func('a { color: identity(returns_color()); }'),
1565            'a{color:rgba(1,2,3,0.5)}\n',
1566        )
1567
1568    def test_identity_comma_list(self):
1569        self.assertEqual(
1570            compile_with_func(
1571                'a { font-family: identity(returns_comma_list()); }',
1572            ),
1573            'a{font-family:Arial,sans-serif}\n',
1574        )
1575
1576    def test_identity_space_list(self):
1577        self.assertEqual(
1578            compile_with_func(
1579                'a { border-right: identity(returns_space_list()); }',
1580            ),
1581            'a{border-right:medium none}\n',
1582        )
1583
1584    def test_identity_bracketed_list(self):
1585        self.assertEqual(
1586            compile_with_func(
1587                'a { content: identity(returns_bracketed_list()); }',
1588            ),
1589            'a{content:[hello ohai]}\n',
1590        )
1591
1592    def test_identity_py_dict(self):
1593        self.assertEqual(
1594            compile_with_func(
1595                'a { content: map-get(identity(returns_py_dict()), foo); }',
1596            ),
1597            'a{content:bar}\n',
1598        )
1599
1600    def test_identity_map(self):
1601        self.assertEqual(
1602            compile_with_func(
1603                'a { content: map-get(identity(returns_map()), foo); }',
1604            ),
1605            'a{content:bar}\n',
1606        )
1607
1608    def test_list_with_map_item(self):
1609        self.assertEqual(
1610            compile_with_func(
1611                'a{content: '
1612                'map-get(nth(identity(((foo: bar), (baz: womp))), 1), foo)'
1613                '}',
1614            ),
1615            'a{content:bar}\n',
1616        )
1617
1618    def test_map_with_map_key(self):
1619        self.assertEqual(
1620            compile_with_func(
1621                'a{content: map-get(identity(((foo: bar): baz)), (foo: bar))}',
1622            ),
1623            'a{content:baz}\n',
1624        )
1625
1626
1627def test_stack_trace_formatting():
1628    try:
1629        sass.compile(string=u'a{☃')
1630        raise AssertionError('expected to raise CompileError')
1631    except sass.CompileError:
1632        tb = traceback.format_exc()
1633    # TODO: https://github.com/sass/libsass/issues/3092
1634    assert tb.endswith(
1635        'CompileError: Error: Invalid CSS after "a{☃": expected "{", was ""\n'
1636        '        on line 1:4 of stdin\n'
1637        '>> a{☃\n'
1638        '   ---^\n\n',
1639    )
1640
1641
1642def test_source_comments():
1643    out = sass.compile(string='a{color: red}', source_comments=True)
1644    assert out == '/* line 1, stdin */\na {\n  color: red; }\n'
1645
1646
1647def test_pysassc_sourcemap(tmpdir):
1648    src_file = tmpdir.join('src').ensure_dir().join('a.scss')
1649    out_file = tmpdir.join('a.scss.css')
1650    out_map_file = tmpdir.join('a.scss.css.map')
1651
1652    src_file.write('.c { font-size: 5px + 5px; }')
1653
1654    exit_code = pysassc.main([
1655        'pysassc', '-m', src_file.strpath, out_file.strpath,
1656    ])
1657    assert exit_code == 0
1658
1659    contents = out_file.read()
1660    assert contents == (
1661        '.c {\n'
1662        '  font-size: 10px; }\n'
1663        '\n'
1664        '/*# sourceMappingURL=a.scss.css.map */'
1665    )
1666    source_map_json = json.loads(out_map_file.read())
1667    assert source_map_json == {
1668        'sources': ['src/a.scss'],
1669        'version': 3,
1670        'names': [],
1671        'file': 'a.scss.css',
1672        'mappings': 'AAAA,AAAA,EAAE,CAAC;EAAE,SAAS,EAAE,IAAS,GAAI',
1673    }
1674
1675
1676def test_imports_from_cwd(tmpdir):
1677    scss_dir = tmpdir.join('scss').ensure_dir()
1678    scss_dir.join('_variables.scss').ensure()
1679    main_scss = scss_dir.join('main.scss')
1680    main_scss.write("@import 'scss/variables';")
1681    with tmpdir.as_cwd():
1682        out = sass.compile(filename=main_scss.strpath)
1683        assert out == ''
1684
1685
1686def test_import_css(tmpdir):
1687    tmpdir.join('other.css').write('body {color: green}')
1688    main_scss = tmpdir.join('main.scss')
1689    main_scss.write("@import 'other';")
1690    out = sass.compile(filename=main_scss.strpath)
1691    assert out == 'body {\n  color: green; }\n'
1692
1693
1694def test_import_css_string(tmpdir):
1695    tmpdir.join('other.css').write('body {color: green}')
1696    with tmpdir.as_cwd():
1697        out = sass.compile(string="@import 'other';")
1698    assert out == 'body {\n  color: green; }\n'
1699
1700
1701def test_custom_import_extensions_warning():
1702    with pytest.warns(FutureWarning):
1703        sass.compile(string='a{b: c}', custom_import_extensions=['.css'])
1704