1package godartsass 2 3import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "io/ioutil" 8 "os" 9 "path/filepath" 10 "strings" 11 "sync" 12 "testing" 13 14 qt "github.com/frankban/quicktest" 15) 16 17const ( 18 sassSample = `nav { 19 ul { 20 margin: 0; 21 padding: 0; 22 list-style: none; 23 } 24 25 li { display: inline-block; } 26 27 a { 28 display: block; 29 padding: 6px 12px; 30 text-decoration: none; 31 } 32}` 33 sassSampleTranspiled = "nav ul {\n margin: 0;\n padding: 0;\n list-style: none;\n}\nnav li {\n display: inline-block;\n}\nnav a {\n display: block;\n padding: 6px 12px;\n text-decoration: none;\n}" 34) 35 36type testImportResolver struct { 37 name string 38 content string 39 40 failOnCanonicalizeURL bool 41 failOnLoad bool 42} 43 44func (t testImportResolver) CanonicalizeURL(url string) (string, error) { 45 if t.failOnCanonicalizeURL { 46 return "", errors.New("failed") 47 } 48 if url != t.name { 49 return "", nil 50 } 51 52 return "file:/my" + t.name + "/scss/" + url + "_myfile.scss", nil 53} 54 55func (t testImportResolver) Load(url string) (string, error) { 56 if t.failOnLoad { 57 return "", errors.New("failed") 58 } 59 if !strings.Contains(url, t.name) { 60 panic("protocol error") 61 } 62 return t.content, nil 63} 64 65func TestTranspilerVariants(t *testing.T) { 66 c := qt.New(t) 67 68 colorsResolver := testImportResolver{ 69 name: "colors", 70 content: `$white: #ffff`, 71 } 72 73 for _, test := range []struct { 74 name string 75 opts Options 76 args Args 77 expect interface{} 78 }{ 79 {"Output style compressed", Options{}, Args{Source: "div { color: #ccc; }", OutputStyle: OutputStyleCompressed}, Result{CSS: "div{color:#ccc}"}}, 80 {"Enable Source Map", Options{}, Args{Source: "div{color:blue;}", URL: "file://myproject/main.scss", OutputStyle: OutputStyleCompressed, EnableSourceMap: true}, Result{CSS: "div{color:blue}", SourceMap: "{\"version\":3,\"sourceRoot\":\"\",\"sources\":[\"file://myproject/main.scss\"],\"names\":[],\"mappings\":\"AAAA\"}"}}, 81 {"Sass syntax", Options{}, Args{ 82 Source: `$font-stack: Helvetica, sans-serif 83$primary-color: #333 84 85body 86 font: 100% $font-stack 87 color: $primary-color 88`, 89 OutputStyle: OutputStyleCompressed, 90 SourceSyntax: SourceSyntaxSASS, 91 }, Result{CSS: "body{font:100% Helvetica,sans-serif;color:#333}"}}, 92 {"Import resolver with source map", Options{}, Args{Source: "@import \"colors\";\ndiv { p { color: $white; } }", EnableSourceMap: true, ImportResolver: colorsResolver}, Result{CSS: "div p {\n color: #ffff;\n}", SourceMap: "{\"version\":3,\"sourceRoot\":\"\",\"sources\":[\"data:;charset=utf-8,@import%20%22colors%22;%0Adiv%20%7B%20p%20%7B%20color:%20$white;%20%7D%20%7D\",\"file:///mycolors/scss/colors_myfile.scss\"],\"names\":[],\"mappings\":\"AACM;EAAI,OCDC\"}"}}, 93 94 // Error cases 95 {"Invalid syntax", Options{}, Args{Source: "div { color: $white; }"}, false}, 96 {"Import not found", Options{}, Args{Source: "@import \"foo\""}, false}, 97 {"Import with ImportResolver, not found", Options{}, Args{Source: "@import \"foo\"", ImportResolver: colorsResolver}, false}, 98 {"Error in ImportResolver.CanonicalizeURL", Options{}, Args{Source: "@import \"colors\";", ImportResolver: testImportResolver{name: "colors", failOnCanonicalizeURL: true}}, false}, 99 {"Error in ImportResolver.Load", Options{}, Args{Source: "@import \"colors\";", ImportResolver: testImportResolver{name: "colors", failOnLoad: true}}, false}, 100 {"Invalid OutputStyle", Options{}, Args{Source: "a", OutputStyle: "asdf"}, false}, 101 {"Invalid SourceSyntax", Options{}, Args{Source: "a", SourceSyntax: "asdf"}, false}, 102 } { 103 104 test := test 105 c.Run(test.name, func(c *qt.C) { 106 b, ok := test.expect.(bool) 107 shouldFail := ok && !b 108 transpiler, clean := newTestTranspiler(c, test.opts) 109 defer clean() 110 result, err := transpiler.Execute(test.args) 111 if shouldFail { 112 c.Assert(err, qt.Not(qt.IsNil)) 113 // Verify that the communication is still up and running. 114 _, err2 := transpiler.Execute(test.args) 115 c.Assert(err2.Error(), qt.Equals, err.Error()) 116 } else { 117 expectedResult := test.expect.(Result) 118 c.Assert(err, qt.IsNil) 119 //printJSON(result.SourceMap) 120 c.Assert(result, qt.Equals, expectedResult) 121 122 } 123 }) 124 125 } 126} 127 128func TestIncludePaths(t *testing.T) { 129 dir1, _ := ioutil.TempDir(os.TempDir(), "libsass-test-include-paths-dir1") 130 defer os.RemoveAll(dir1) 131 dir2, _ := ioutil.TempDir(os.TempDir(), "libsass-test-include-paths-dir2") 132 defer os.RemoveAll(dir2) 133 134 colors := filepath.Join(dir1, "_colors.scss") 135 content := filepath.Join(dir2, "_content.scss") 136 137 ioutil.WriteFile(colors, []byte(` 138$moo: #f442d1 !default; 139`), 0644) 140 141 ioutil.WriteFile(content, []byte(` 142content { color: #ccc; } 143`), 0644) 144 145 c := qt.New(t) 146 src := ` 147@import "colors"; 148@import "content"; 149div { p { color: $moo; } }` 150 151 transpiler, clean := newTestTranspiler(c, Options{}) 152 defer clean() 153 154 result, err := transpiler.Execute( 155 Args{ 156 Source: src, 157 OutputStyle: OutputStyleCompressed, 158 IncludePaths: []string{dir1, dir2}, 159 }, 160 ) 161 c.Assert(err, qt.IsNil) 162 c.Assert(result.CSS, qt.Equals, "content{color:#ccc}div p{color:#f442d1}") 163 164} 165 166func TestTranspilerParallel(t *testing.T) { 167 c := qt.New(t) 168 transpiler, clean := newTestTranspiler(c, Options{}) 169 defer clean() 170 var wg sync.WaitGroup 171 172 for i := 0; i < 10; i++ { 173 wg.Add(1) 174 go func(num int) { 175 defer wg.Done() 176 for j := 0; j < 4; j++ { 177 src := fmt.Sprintf(` 178$primary-color: #%03d; 179 180div { color: $primary-color; }`, num) 181 182 result, err := transpiler.Execute(Args{Source: src}) 183 c.Check(err, qt.IsNil) 184 c.Check(result.CSS, qt.Equals, fmt.Sprintf("div {\n color: #%03d;\n}", num)) 185 if c.Failed() { 186 return 187 } 188 } 189 }(i) 190 } 191 wg.Wait() 192} 193 194func TestTranspilerParallelImportResolver(t *testing.T) { 195 c := qt.New(t) 196 197 createImportResolver := func(width int) ImportResolver { 198 199 return testImportResolver{ 200 name: "widths", 201 content: fmt.Sprintf(`$width: %d`, width), 202 } 203 204 } 205 206 transpiler, clean := newTestTranspiler(c, Options{}) 207 defer clean() 208 209 var wg sync.WaitGroup 210 211 for i := 0; i < 10; i++ { 212 wg.Add(1) 213 go func(i int) { 214 defer wg.Done() 215 216 for j := 0; j < 10; j++ { 217 218 for k := 0; k < 20; k++ { 219 args := Args{ 220 OutputStyle: OutputStyleCompressed, 221 ImportResolver: createImportResolver(j + i), 222 Source: ` 223@import "widths"; 224 225div { p { width: $width; } }`, 226 } 227 228 result, err := transpiler.Execute(args) 229 c.Check(err, qt.IsNil) 230 c.Check(result.CSS, qt.Equals, fmt.Sprintf("div p{width:%d}", j+i)) 231 if c.Failed() { 232 return 233 } 234 } 235 } 236 }(i) 237 } 238 239 wg.Wait() 240 241} 242 243func TestTranspilerClose(t *testing.T) { 244 c := qt.New(t) 245 transpiler, _ := newTestTranspiler(c, Options{}) 246 var wg sync.WaitGroup 247 248 for i := 0; i < 10; i++ { 249 wg.Add(1) 250 go func(gor int) { 251 defer wg.Done() 252 for j := 0; j < 4; j++ { 253 src := fmt.Sprintf(` 254$primary-color: #%03d; 255 256div { color: $primary-color; }`, gor) 257 258 num := gor + j 259 260 if num == 10 { 261 err := transpiler.Close() 262 if err != nil { 263 c.Check(err, qt.Equals, ErrShutdown) 264 } 265 } 266 267 result, err := transpiler.Execute(Args{Source: src}) 268 269 if err != nil { 270 c.Check(err, qt.Equals, ErrShutdown) 271 } else { 272 c.Check(err, qt.IsNil) 273 c.Check(result.CSS, qt.Equals, fmt.Sprintf("div {\n color: #%03d;\n}", gor)) 274 } 275 276 if c.Failed() { 277 return 278 } 279 } 280 }(i) 281 } 282 wg.Wait() 283 284 for _, p := range transpiler.pending { 285 c.Assert(p.Error, qt.Equals, ErrShutdown) 286 } 287} 288 289func BenchmarkTranspiler(b *testing.B) { 290 type tester struct { 291 src string 292 expect string 293 transpiler *Transpiler 294 clean func() 295 } 296 297 newTester := func(b *testing.B, opts Options) tester { 298 c := qt.New(b) 299 transpiler, clean := newTestTranspiler(c, Options{}) 300 301 return tester{ 302 transpiler: transpiler, 303 clean: clean, 304 } 305 } 306 307 runBench := func(b *testing.B, t tester) { 308 defer t.clean() 309 b.ResetTimer() 310 for n := 0; n < b.N; n++ { 311 result, err := t.transpiler.Execute(Args{Source: t.src}) 312 if err != nil { 313 b.Fatal(err) 314 } 315 if result.CSS != t.expect { 316 b.Fatalf("Got: %q\n", result.CSS) 317 } 318 } 319 } 320 321 b.Run("SCSS", func(b *testing.B) { 322 t := newTester(b, Options{}) 323 t.src = sassSample 324 t.expect = sassSampleTranspiled 325 runBench(b, t) 326 }) 327 328 // This is the obviously much slower way of doing it. 329 b.Run("Start and Execute", func(b *testing.B) { 330 for n := 0; n < b.N; n++ { 331 t := newTester(b, Options{}) 332 t.src = sassSample 333 t.expect = sassSampleTranspiled 334 result, err := t.transpiler.Execute(Args{Source: t.src}) 335 if err != nil { 336 b.Fatal(err) 337 } 338 if result.CSS != t.expect { 339 b.Fatalf("Got: %q\n", result.CSS) 340 } 341 t.transpiler.Close() 342 } 343 }) 344 345 b.Run("SCSS Parallel", func(b *testing.B) { 346 t := newTester(b, Options{}) 347 t.src = sassSample 348 t.expect = sassSampleTranspiled 349 defer t.clean() 350 b.RunParallel(func(pb *testing.PB) { 351 for pb.Next() { 352 result, err := t.transpiler.Execute(Args{Source: t.src}) 353 if err != nil { 354 b.Fatal(err) 355 } 356 if result.CSS != t.expect { 357 b.Fatalf("Got: %q\n", result.CSS) 358 } 359 } 360 }) 361 }) 362} 363 364func TestHasScheme(t *testing.T) { 365 c := qt.New(t) 366 367 c.Assert(hasScheme("file:foo"), qt.Equals, true) 368 c.Assert(hasScheme("http:foo"), qt.Equals, true) 369 c.Assert(hasScheme("http://foo"), qt.Equals, true) 370 c.Assert(hasScheme("123:foo"), qt.Equals, false) 371 c.Assert(hasScheme("foo"), qt.Equals, false) 372 373} 374 375func newTestTranspiler(c *qt.C, opts Options) (*Transpiler, func()) { 376 opts.DartSassEmbeddedFilename = getSassEmbeddedFilename() 377 transpiler, err := Start(opts) 378 c.Assert(err, qt.IsNil) 379 380 return transpiler, func() { 381 err := transpiler.Close() 382 c.Assert(err, qt.IsNil) 383 } 384} 385 386func getSassEmbeddedFilename() string { 387 // https://github.com/sass/dart-sass-embedded/releases 388 if filename := os.Getenv("DART_SASS_EMBEDDED_BINARY"); filename != "" { 389 return filename 390 } 391 392 return defaultDartSassEmbeddedFilename 393} 394 395// used for debugging 396func printJSON(s string) { 397 m := make(map[string]interface{}) 398 json.Unmarshal([]byte(s), &m) 399 b, _ := json.MarshalIndent(m, "", " ") 400 fmt.Printf("%s", b) 401 402} 403