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