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