From b71d78fac39975a3109fba5bf473cc18d679bfde Mon Sep 17 00:00:00 2001 From: Ed Rivas Date: Tue, 14 Nov 2023 21:44:04 +0000 Subject: [PATCH 01/21] Add basic compiler tests --- js-api-spec/compiler.test.ts | 93 ++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 js-api-spec/compiler.test.ts diff --git a/js-api-spec/compiler.test.ts b/js-api-spec/compiler.test.ts new file mode 100644 index 000000000..54e4bad67 --- /dev/null +++ b/js-api-spec/compiler.test.ts @@ -0,0 +1,93 @@ +// Copyright 2023 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import {initCompiler, initAsyncCompiler} from 'sass'; +import type {Compiler, AsyncCompiler} from 'sass'; +import {sandbox} from './sandbox'; + +describe('Compiler', () => { + let compiler: Compiler; + beforeEach(() => { + compiler = initCompiler(); + }); + describe('compileString', () => { + it('performs multiple compilations', () => { + expect(compiler.compileString('$a: b; c {d: $a}').css).toBe( + 'c {\n d: b;\n}' + ); + expect(compiler.compileString('$a: 1; c {d: $a}').css).toBe( + 'c {\n d: 1;\n}' + ); + }); + it('throws after being disposed', () => { + compiler.dispose(); + expect(() => compiler.compileString('$a: b; c {d: $a}')).toThrow(); + }); + }); + describe('compile', () => { + it('performs multiple compilations', () => { + sandbox(dir => { + dir.write({'input.scss': '$a: b; c {d: $a}'}); + dir.write({'input2.scss': '$a: 1; c {d: $a}'}); + expect(compiler.compile(dir('input.scss')).css).toBe('c {\n d: b;\n}'); + expect(compiler.compile(dir('input2.scss')).css).toBe( + 'c {\n d: 1;\n}' + ); + }); + }); + it('throws after being disposed', () => { + sandbox(dir => { + dir.write({'input.scss': '$a: b; c {d: $a}'}); + compiler.dispose(); + expect(() => compiler.compile(dir('input.scss'))).toThrow(); + }); + }); + }); +}); + +describe('AsyncCompiler', () => { + let compiler: AsyncCompiler; + beforeEach(async () => { + compiler = await initAsyncCompiler(); + }); + describe('compileStringAsync', () => { + it('performs multiple compilations', async () => { + expect((await compiler.compileStringAsync('$a: b; c {d: $a}')).css).toBe( + 'c {\n d: b;\n}' + ); + expect((await compiler.compileStringAsync('$a: 1; c {d: $a}')).css).toBe( + 'c {\n d: 1;\n}' + ); + }); + it('throws after being disposed', async () => { + await compiler.dispose(); + await expectAsync( + compiler.compileStringAsync('$a: b; c {d: $a}') + ).toThrowSassException({line: 0}); + }); + }); + describe('compileAsync', () => { + it('performs multiple compilations', async () => { + await sandbox(async dir => { + dir.write({'input.scss': '$a: b; c {d: $a}'}); + dir.write({'input2.scss': '$a: 1; c {d: $a}'}); + expect((await compiler.compileAsync(dir('input.scss'))).css).toBe( + 'c {\n d: b;\n}' + ); + expect((await compiler.compileAsync(dir('input2.scss'))).css).toBe( + 'c {\n d: 1;\n}' + ); + }); + }); + it('throws after being disposed', async () => { + await sandbox(async dir => { + dir.write({'input.scss': '$a: b; c {d: $a}'}); + await compiler.dispose(); + await expectAsync( + compiler.compileAsync(dir('input.scss')) + ).toThrowSassException({line: 0}); + }); + }); + }); +}); From e955667bccc6fd5bc2abc815644c537941cf305b Mon Sep 17 00:00:00 2001 From: Ed Rivas Date: Tue, 14 Nov 2023 22:29:10 +0000 Subject: [PATCH 02/21] Test for active compilations when disposing --- js-api-spec/compiler.test.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/js-api-spec/compiler.test.ts b/js-api-spec/compiler.test.ts index 54e4bad67..17d3ea5a6 100644 --- a/js-api-spec/compiler.test.ts +++ b/js-api-spec/compiler.test.ts @@ -66,6 +66,11 @@ describe('AsyncCompiler', () => { compiler.compileStringAsync('$a: b; c {d: $a}') ).toThrowSassException({line: 0}); }); + it('waits for compilations to finish before disposing', async () => { + const compilation = compiler.compileStringAsync('$a: b; c {d: $a}'); + await compiler.dispose(); + await expectAsync(compilation).toBeResolved(); + }); }); describe('compileAsync', () => { it('performs multiple compilations', async () => { @@ -86,7 +91,15 @@ describe('AsyncCompiler', () => { await compiler.dispose(); await expectAsync( compiler.compileAsync(dir('input.scss')) - ).toThrowSassException({line: 0}); + ).toBeRejected(); + }); + }); + it('waits for compilations to finish before disposing', async () => { + await sandbox(async dir => { + dir.write({'input.scss': '$a: b; c {d: $a}'}); + const compilation = compiler.compileAsync(dir('input.scss')); + await compiler.dispose(); + await expectAsync(compilation).toBeResolved(); }); }); }); From 6145b4e73263402227b91140c94d9067b257906f Mon Sep 17 00:00:00 2001 From: Ed Rivas Date: Wed, 15 Nov 2023 17:53:23 +0000 Subject: [PATCH 03/21] Fix "after disposed" tests --- js-api-spec/compiler.test.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/js-api-spec/compiler.test.ts b/js-api-spec/compiler.test.ts index 17d3ea5a6..e513cb5f9 100644 --- a/js-api-spec/compiler.test.ts +++ b/js-api-spec/compiler.test.ts @@ -11,6 +11,9 @@ describe('Compiler', () => { beforeEach(() => { compiler = initCompiler(); }); + afterEach(() => { + compiler.dispose(); + }); describe('compileString', () => { it('performs multiple compilations', () => { expect(compiler.compileString('$a: b; c {d: $a}').css).toBe( @@ -51,6 +54,9 @@ describe('AsyncCompiler', () => { beforeEach(async () => { compiler = await initAsyncCompiler(); }); + afterEach(async () => { + await compiler.dispose(); + }); describe('compileStringAsync', () => { it('performs multiple compilations', async () => { expect((await compiler.compileStringAsync('$a: b; c {d: $a}')).css).toBe( @@ -62,9 +68,7 @@ describe('AsyncCompiler', () => { }); it('throws after being disposed', async () => { await compiler.dispose(); - await expectAsync( - compiler.compileStringAsync('$a: b; c {d: $a}') - ).toThrowSassException({line: 0}); + expect(() => compiler.compileStringAsync('$a: b; c {d: $a}')).toThrow(); }); it('waits for compilations to finish before disposing', async () => { const compilation = compiler.compileStringAsync('$a: b; c {d: $a}'); @@ -86,12 +90,10 @@ describe('AsyncCompiler', () => { }); }); it('throws after being disposed', async () => { - await sandbox(async dir => { + sandbox(async dir => { dir.write({'input.scss': '$a: b; c {d: $a}'}); await compiler.dispose(); - await expectAsync( - compiler.compileAsync(dir('input.scss')) - ).toBeRejected(); + expect(() => compiler.compileAsync(dir('input.scss'))).toThrow(); }); }); it('waits for compilations to finish before disposing', async () => { From d1fe650a61d41300dc36264205da1560fe97e793 Mon Sep 17 00:00:00 2001 From: Jonny Gerig Meyer Date: Wed, 15 Nov 2023 15:10:31 -0500 Subject: [PATCH 04/21] review --- js-api-spec/compiler.test.ts | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/js-api-spec/compiler.test.ts b/js-api-spec/compiler.test.ts index e513cb5f9..5cb1b6781 100644 --- a/js-api-spec/compiler.test.ts +++ b/js-api-spec/compiler.test.ts @@ -4,16 +4,20 @@ import {initCompiler, initAsyncCompiler} from 'sass'; import type {Compiler, AsyncCompiler} from 'sass'; + import {sandbox} from './sandbox'; describe('Compiler', () => { let compiler: Compiler; + beforeEach(() => { compiler = initCompiler(); }); + afterEach(() => { compiler.dispose(); }); + describe('compileString', () => { it('performs multiple compilations', () => { expect(compiler.compileString('$a: b; c {d: $a}').css).toBe( @@ -23,11 +27,13 @@ describe('Compiler', () => { 'c {\n d: 1;\n}' ); }); + it('throws after being disposed', () => { compiler.dispose(); - expect(() => compiler.compileString('$a: b; c {d: $a}')).toThrow(); + expect(() => compiler.compileString('$a: b; c {d: $a}')).toThrowError(); }); }); + describe('compile', () => { it('performs multiple compilations', () => { sandbox(dir => { @@ -39,11 +45,12 @@ describe('Compiler', () => { ); }); }); + it('throws after being disposed', () => { sandbox(dir => { dir.write({'input.scss': '$a: b; c {d: $a}'}); compiler.dispose(); - expect(() => compiler.compile(dir('input.scss'))).toThrow(); + expect(() => compiler.compile(dir('input.scss'))).toThrowError(); }); }); }); @@ -51,12 +58,15 @@ describe('Compiler', () => { describe('AsyncCompiler', () => { let compiler: AsyncCompiler; + beforeEach(async () => { compiler = await initAsyncCompiler(); }); + afterEach(async () => { await compiler.dispose(); }); + describe('compileStringAsync', () => { it('performs multiple compilations', async () => { expect((await compiler.compileStringAsync('$a: b; c {d: $a}')).css).toBe( @@ -66,16 +76,21 @@ describe('AsyncCompiler', () => { 'c {\n d: 1;\n}' ); }); + it('throws after being disposed', async () => { await compiler.dispose(); - expect(() => compiler.compileStringAsync('$a: b; c {d: $a}')).toThrow(); + expect(() => + compiler.compileStringAsync('$a: b; c {d: $a}') + ).toThrowError(); }); + it('waits for compilations to finish before disposing', async () => { const compilation = compiler.compileStringAsync('$a: b; c {d: $a}'); await compiler.dispose(); await expectAsync(compilation).toBeResolved(); }); }); + describe('compileAsync', () => { it('performs multiple compilations', async () => { await sandbox(async dir => { @@ -89,13 +104,15 @@ describe('AsyncCompiler', () => { ); }); }); + it('throws after being disposed', async () => { sandbox(async dir => { dir.write({'input.scss': '$a: b; c {d: $a}'}); await compiler.dispose(); - expect(() => compiler.compileAsync(dir('input.scss'))).toThrow(); + expect(() => compiler.compileAsync(dir('input.scss'))).toThrowError(); }); }); + it('waits for compilations to finish before disposing', async () => { await sandbox(async dir => { dir.write({'input.scss': '$a: b; c {d: $a}'}); From 89ef7e5775b6a89e13e36c03f253e3bef169ccc8 Mon Sep 17 00:00:00 2001 From: Ed Rivas Date: Thu, 16 Nov 2023 19:05:46 +0000 Subject: [PATCH 05/21] Ignore whitespace when comparing --- js-api-spec/compiler.test.ts | 46 +++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/js-api-spec/compiler.test.ts b/js-api-spec/compiler.test.ts index 5cb1b6781..7dfbe4b1d 100644 --- a/js-api-spec/compiler.test.ts +++ b/js-api-spec/compiler.test.ts @@ -20,12 +20,12 @@ describe('Compiler', () => { describe('compileString', () => { it('performs multiple compilations', () => { - expect(compiler.compileString('$a: b; c {d: $a}').css).toBe( - 'c {\n d: b;\n}' - ); - expect(compiler.compileString('$a: 1; c {d: $a}').css).toBe( - 'c {\n d: 1;\n}' - ); + expect( + compiler.compileString('$a: b; c {d: $a}').css + ).toEqualIgnoringWhitespace('c {d: b;}'); + expect( + compiler.compileString('$a: 1; c {d: $a}').css + ).toEqualIgnoringWhitespace('c {d: 1;}'); }); it('throws after being disposed', () => { @@ -39,10 +39,12 @@ describe('Compiler', () => { sandbox(dir => { dir.write({'input.scss': '$a: b; c {d: $a}'}); dir.write({'input2.scss': '$a: 1; c {d: $a}'}); - expect(compiler.compile(dir('input.scss')).css).toBe('c {\n d: b;\n}'); - expect(compiler.compile(dir('input2.scss')).css).toBe( - 'c {\n d: 1;\n}' - ); + expect( + compiler.compile(dir('input.scss')).css + ).toEqualIgnoringWhitespace('c {d: b;}'); + expect( + compiler.compile(dir('input2.scss')).css + ).toEqualIgnoringWhitespace('c {d: 1;}'); }); }); @@ -69,12 +71,12 @@ describe('AsyncCompiler', () => { describe('compileStringAsync', () => { it('performs multiple compilations', async () => { - expect((await compiler.compileStringAsync('$a: b; c {d: $a}')).css).toBe( - 'c {\n d: b;\n}' - ); - expect((await compiler.compileStringAsync('$a: 1; c {d: $a}')).css).toBe( - 'c {\n d: 1;\n}' - ); + expect( + (await compiler.compileStringAsync('$a: b; c {d: $a}')).css + ).toEqualIgnoringWhitespace('c {d: b;}'); + expect( + (await compiler.compileStringAsync('$a: 1; c {d: $a}')).css + ).toEqualIgnoringWhitespace('c {d: 1;}'); }); it('throws after being disposed', async () => { @@ -96,12 +98,12 @@ describe('AsyncCompiler', () => { await sandbox(async dir => { dir.write({'input.scss': '$a: b; c {d: $a}'}); dir.write({'input2.scss': '$a: 1; c {d: $a}'}); - expect((await compiler.compileAsync(dir('input.scss'))).css).toBe( - 'c {\n d: b;\n}' - ); - expect((await compiler.compileAsync(dir('input2.scss'))).css).toBe( - 'c {\n d: 1;\n}' - ); + expect( + (await compiler.compileAsync(dir('input.scss'))).css + ).toEqualIgnoringWhitespace('c {d: b;}'); + expect( + (await compiler.compileAsync(dir('input2.scss'))).css + ).toEqualIgnoringWhitespace('c {d: 1;}'); }); }); From 95fd55894208bcd297024c2ba99fafb91cfec8f0 Mon Sep 17 00:00:00 2001 From: Ed Rivas Date: Thu, 16 Nov 2023 19:21:12 +0000 Subject: [PATCH 06/21] Remove extra wrapper fn --- js-api-spec/compiler.test.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/js-api-spec/compiler.test.ts b/js-api-spec/compiler.test.ts index 7dfbe4b1d..ff08248f4 100644 --- a/js-api-spec/compiler.test.ts +++ b/js-api-spec/compiler.test.ts @@ -94,8 +94,8 @@ describe('AsyncCompiler', () => { }); describe('compileAsync', () => { - it('performs multiple compilations', async () => { - await sandbox(async dir => { + it('performs multiple compilations', () => + sandbox(async dir => { dir.write({'input.scss': '$a: b; c {d: $a}'}); dir.write({'input2.scss': '$a: 1; c {d: $a}'}); expect( @@ -104,8 +104,7 @@ describe('AsyncCompiler', () => { expect( (await compiler.compileAsync(dir('input2.scss'))).css ).toEqualIgnoringWhitespace('c {d: 1;}'); - }); - }); + })); it('throws after being disposed', async () => { sandbox(async dir => { @@ -115,13 +114,12 @@ describe('AsyncCompiler', () => { }); }); - it('waits for compilations to finish before disposing', async () => { - await sandbox(async dir => { + it('waits for compilations to finish before disposing', () => + sandbox(async dir => { dir.write({'input.scss': '$a: b; c {d: $a}'}); const compilation = compiler.compileAsync(dir('input.scss')); await compiler.dispose(); await expectAsync(compilation).toBeResolved(); - }); - }); + })); }); }); From 032e045caa95b811717cc78987bac54375d96c9a Mon Sep 17 00:00:00 2001 From: Ed Rivas Date: Fri, 17 Nov 2023 01:16:08 +0000 Subject: [PATCH 07/21] More robust compiler tests --- js-api-spec/compiler.test.ts | 129 ++++++++++++++++++++++++----------- 1 file changed, 90 insertions(+), 39 deletions(-) diff --git a/js-api-spec/compiler.test.ts b/js-api-spec/compiler.test.ts index ff08248f4..2116f309b 100644 --- a/js-api-spec/compiler.test.ts +++ b/js-api-spec/compiler.test.ts @@ -2,10 +2,30 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import {initCompiler, initAsyncCompiler} from 'sass'; -import type {Compiler, AsyncCompiler} from 'sass'; +import type {AsyncCompiler, Compiler, CompileResult} from 'sass'; +import {initAsyncCompiler, initCompiler, SassString} from 'sass'; import {sandbox} from './sandbox'; +import {spy, URL} from './utils'; + +const functions = {'foo($args)': (args: unknown) => new SassString(`${args}`)}; +const importers = [ + { + canonicalize: (url: string) => new URL(`u:${url}`), + load: (url: typeof URL) => ({ + contents: `.import {value: ${url.pathname}} @debug "imported";`, + syntax: 'scss' as const, + }), + }, +]; +const asyncImporters = [ + { + canonicalize: (url: string) => + Promise.resolve(importers[0].canonicalize(url)), + load: (url: typeof URL) => Promise.resolve(importers[0].load(url)), + }, +]; +const getLogger = () => ({debug: spy(() => {})}); describe('Compiler', () => { let compiler: Compiler; @@ -19,13 +39,16 @@ describe('Compiler', () => { }); describe('compileString', () => { - it('performs multiple compilations', () => { - expect( - compiler.compileString('$a: b; c {d: $a}').css - ).toEqualIgnoringWhitespace('c {d: b;}'); - expect( - compiler.compileString('$a: 1; c {d: $a}').css - ).toEqualIgnoringWhitespace('c {d: 1;}'); + it('performs complete compilations', () => { + const logger = getLogger(); + const result = compiler.compileString( + '@import "bar"; .fn {value: foo(baz)}', + {importers, functions, logger} + ); + expect(result.css).toEqualIgnoringWhitespace( + '.import {value: bar;} .fn {value: "baz";}' + ); + expect(logger.debug).toHaveBeenCalledTimes(1); }); it('throws after being disposed', () => { @@ -35,26 +58,27 @@ describe('Compiler', () => { }); describe('compile', () => { - it('performs multiple compilations', () => { + it('performs complete compilations', () => sandbox(dir => { - dir.write({'input.scss': '$a: b; c {d: $a}'}); - dir.write({'input2.scss': '$a: 1; c {d: $a}'}); - expect( - compiler.compile(dir('input.scss')).css - ).toEqualIgnoringWhitespace('c {d: b;}'); - expect( - compiler.compile(dir('input2.scss')).css - ).toEqualIgnoringWhitespace('c {d: 1;}'); - }); - }); + const logger = getLogger(); + dir.write({'input.scss': '@import "bar"; .fn {value: foo(bar)}'}); + const result = compiler.compile(dir('input.scss'), { + importers, + functions, + logger, + }); + expect(result.css).toEqualIgnoringWhitespace( + '.import {value: bar;} .fn {value: "bar";}' + ); + expect(logger.debug).toHaveBeenCalledTimes(1); + })); - it('throws after being disposed', () => { + it('throws after being disposed', () => sandbox(dir => { dir.write({'input.scss': '$a: b; c {d: $a}'}); compiler.dispose(); expect(() => compiler.compile(dir('input.scss'))).toThrowError(); - }); - }); + })); }); }); @@ -70,13 +94,25 @@ describe('AsyncCompiler', () => { }); describe('compileStringAsync', () => { - it('performs multiple compilations', async () => { - expect( - (await compiler.compileStringAsync('$a: b; c {d: $a}')).css - ).toEqualIgnoringWhitespace('c {d: b;}'); - expect( - (await compiler.compileStringAsync('$a: 1; c {d: $a}')).css - ).toEqualIgnoringWhitespace('c {d: 1;}'); + it('handles multiple concurrent compilations', async () => { + const logger = getLogger(); + const compilations = Array(5) + .fill(0) + .map((_, i) => + compiler.compileStringAsync( + `@import "${i}"; .fn {value: foo(${i})}`, + {importers: asyncImporters, functions, logger} + ) + ); + Array.from(await Promise.all(compilations)) + .map((result: CompileResult) => result.css) + .sort() + .forEach((result, i) => { + expect(result).toEqualIgnoringWhitespace( + `.import {value: ${i};} .fn {value: "${i}";}` + ); + }); + expect(logger.debug).toHaveBeenCalledTimes(compilations.length); }); it('throws after being disposed', async () => { @@ -94,16 +130,31 @@ describe('AsyncCompiler', () => { }); describe('compileAsync', () => { - it('performs multiple compilations', () => + it('handles multiple concurrent compilations', () => sandbox(async dir => { - dir.write({'input.scss': '$a: b; c {d: $a}'}); - dir.write({'input2.scss': '$a: 1; c {d: $a}'}); - expect( - (await compiler.compileAsync(dir('input.scss'))).css - ).toEqualIgnoringWhitespace('c {d: b;}'); - expect( - (await compiler.compileAsync(dir('input2.scss'))).css - ).toEqualIgnoringWhitespace('c {d: 1;}'); + const logger = getLogger(); + const compilations = Array(5) + .fill(0) + .map((_, i) => { + const filename = `input-${i}.scss`; + dir.write({ + [filename]: `@import "${i}"; .fn {value: foo(${i})}`, + }); + return compiler.compileAsync(dir(filename), { + importers: asyncImporters, + functions, + logger, + }); + }); + Array.from(await Promise.all(compilations)) + .map((result: CompileResult) => result.css) + .sort() + .forEach((result, i) => { + expect(result).toEqualIgnoringWhitespace( + `.import {value: ${i};} .fn {value: "${i}";}` + ); + }); + expect(logger.debug).toHaveBeenCalledTimes(compilations.length); })); it('throws after being disposed', async () => { From 36db21b77999bb27cf7085cde9e7b88b8c5c0252 Mon Sep 17 00:00:00 2001 From: Ed Rivas Date: Tue, 21 Nov 2023 01:29:40 +0000 Subject: [PATCH 08/21] Test more concurrent compilations --- js-api-spec/compiler.test.ts | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/js-api-spec/compiler.test.ts b/js-api-spec/compiler.test.ts index 2116f309b..c3ca8ed5f 100644 --- a/js-api-spec/compiler.test.ts +++ b/js-api-spec/compiler.test.ts @@ -9,6 +9,7 @@ import {sandbox} from './sandbox'; import {spy, URL} from './utils'; const functions = {'foo($args)': (args: unknown) => new SassString(`${args}`)}; + const importers = [ { canonicalize: (url: string) => new URL(`u:${url}`), @@ -18,6 +19,7 @@ const importers = [ }), }, ]; + const asyncImporters = [ { canonicalize: (url: string) => @@ -25,8 +27,21 @@ const asyncImporters = [ load: (url: typeof URL) => Promise.resolve(importers[0].load(url)), }, ]; + const getLogger = () => ({debug: spy(() => {})}); +/* Sort the output of the example CSS so it can be compared */ +const sortCompiled = (a: string, b: string) => { + const aMatch = a.match(/value: (\d+);/); + const bMatch = b.match(/value: (\d+);/); + if (!aMatch || !bMatch) { + throw new Error( + `Failed to parse ${a} or ${b} as numbers to determine sort order` + ); + } + return Number(aMatch[1]) - Number(bMatch[1]); +}; + describe('Compiler', () => { let compiler: Compiler; @@ -84,6 +99,7 @@ describe('Compiler', () => { describe('AsyncCompiler', () => { let compiler: AsyncCompiler; + const runs = 1000; // Number of concurrent compilations to run beforeEach(async () => { compiler = await initAsyncCompiler(); @@ -96,7 +112,7 @@ describe('AsyncCompiler', () => { describe('compileStringAsync', () => { it('handles multiple concurrent compilations', async () => { const logger = getLogger(); - const compilations = Array(5) + const compilations = Array(runs) .fill(0) .map((_, i) => compiler.compileStringAsync( @@ -106,13 +122,13 @@ describe('AsyncCompiler', () => { ); Array.from(await Promise.all(compilations)) .map((result: CompileResult) => result.css) - .sort() + .sort(sortCompiled) .forEach((result, i) => { expect(result).toEqualIgnoringWhitespace( `.import {value: ${i};} .fn {value: "${i}";}` ); }); - expect(logger.debug).toHaveBeenCalledTimes(compilations.length); + expect(logger.debug).toHaveBeenCalledTimes(runs); }); it('throws after being disposed', async () => { @@ -133,7 +149,7 @@ describe('AsyncCompiler', () => { it('handles multiple concurrent compilations', () => sandbox(async dir => { const logger = getLogger(); - const compilations = Array(5) + const compilations = Array(runs) .fill(0) .map((_, i) => { const filename = `input-${i}.scss`; @@ -148,13 +164,13 @@ describe('AsyncCompiler', () => { }); Array.from(await Promise.all(compilations)) .map((result: CompileResult) => result.css) - .sort() + .sort(sortCompiled) .forEach((result, i) => { expect(result).toEqualIgnoringWhitespace( `.import {value: ${i};} .fn {value: "${i}";}` ); }); - expect(logger.debug).toHaveBeenCalledTimes(compilations.length); + expect(logger.debug).toHaveBeenCalledTimes(runs); })); it('throws after being disposed', async () => { From 7c17ae557364320231b793c9296c004127de15a8 Mon Sep 17 00:00:00 2001 From: Ed Rivas Date: Tue, 21 Nov 2023 17:50:14 +0000 Subject: [PATCH 09/21] Test compilations in callbacks --- js-api-spec/compiler.test.ts | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/js-api-spec/compiler.test.ts b/js-api-spec/compiler.test.ts index c3ca8ed5f..a44c64884 100644 --- a/js-api-spec/compiler.test.ts +++ b/js-api-spec/compiler.test.ts @@ -66,6 +66,20 @@ describe('Compiler', () => { expect(logger.debug).toHaveBeenCalledTimes(1); }); + it('performs compilations in callbacks', () => { + const nestedImporter = { + canonicalize: (url: string) => new URL(`u:${url}`), + load: (url: typeof URL) => ({ + contents: compiler.compileString('x {y: z}').css, + syntax: 'scss' as const, + }), + }; + const result = compiler.compileString('@import "nested"; a {b: c}', { + importers: [nestedImporter], + }); + expect(result.css).toEqualIgnoringWhitespace('x {y: z;} a {b: c;}'); + }); + it('throws after being disposed', () => { compiler.dispose(); expect(() => compiler.compileString('$a: b; c {d: $a}')).toThrowError(); @@ -88,6 +102,23 @@ describe('Compiler', () => { expect(logger.debug).toHaveBeenCalledTimes(1); })); + it('performs compilations in callbacks', () => + sandbox(dir => { + dir.write({'input-nested.scss': 'x {y: z}'}); + const nestedImporter = { + canonicalize: (url: string) => new URL(`u:${url}`), + load: (url: typeof URL) => ({ + contents: compiler.compile(dir('input-nested.scss')).css, + syntax: 'scss' as const, + }), + }; + dir.write({'input.scss': '@import "nested"; a {b: c}'}); + const result = compiler.compile(dir('input.scss'), { + importers: [nestedImporter], + }); + expect(result.css).toEqualIgnoringWhitespace('x {y: z;} a {b: c;}'); + })); + it('throws after being disposed', () => sandbox(dir => { dir.write({'input.scss': '$a: b; c {d: $a}'}); From 386167f365f53d3d39905af9d109ae2ba6ffd2dd Mon Sep 17 00:00:00 2001 From: Ed Rivas Date: Wed, 22 Nov 2023 00:12:51 +0000 Subject: [PATCH 10/21] Stricter tests for dispose() --- js-api-spec/compiler.test.ts | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/js-api-spec/compiler.test.ts b/js-api-spec/compiler.test.ts index a44c64884..8215ed376 100644 --- a/js-api-spec/compiler.test.ts +++ b/js-api-spec/compiler.test.ts @@ -132,6 +132,16 @@ describe('AsyncCompiler', () => { let compiler: AsyncCompiler; const runs = 1000; // Number of concurrent compilations to run + // A slow importer that executes a callback after a delay + const getSlowImporter = (callback: () => void) => ({ + canonicalize: async (url: string) => new URL('foo:bar'), + load: async (url: typeof URL) => { + await new Promise(resolve => setTimeout(resolve, 100)); + callback(); + return {contents: '', syntax: 'scss' as const}; + }, + }); + beforeEach(async () => { compiler = await initAsyncCompiler(); }); @@ -170,8 +180,13 @@ describe('AsyncCompiler', () => { }); it('waits for compilations to finish before disposing', async () => { - const compilation = compiler.compileStringAsync('$a: b; c {d: $a}'); + let completed = false; + const compilation = compiler.compileStringAsync('@import "slow"', { + importers: [getSlowImporter(() => (completed = true))], + }); + expect(completed).toBeFalse(); await compiler.dispose(); + expect(completed).toBeTrue(); await expectAsync(compilation).toBeResolved(); }); }); @@ -214,9 +229,14 @@ describe('AsyncCompiler', () => { it('waits for compilations to finish before disposing', () => sandbox(async dir => { - dir.write({'input.scss': '$a: b; c {d: $a}'}); - const compilation = compiler.compileAsync(dir('input.scss')); + let completed = false; + dir.write({'input.scss': '@import "slow"'}); + const compilation = compiler.compileAsync(dir('input.scss'), { + importers: [getSlowImporter(() => (completed = true))], + }); + expect(completed).toBeFalse(); await compiler.dispose(); + expect(completed).toBeTrue(); await expectAsync(compilation).toBeResolved(); })); }); From 2ac64d29a35350ce797da51e6f006b7e5ed3b87e Mon Sep 17 00:00:00 2001 From: Ed Rivas Date: Wed, 22 Nov 2023 00:35:18 +0000 Subject: [PATCH 11/21] Split node-specific compiler tests --- js-api-spec/compiler.node.test.ts | 142 ++++++++++++++++++++++++++++++ js-api-spec/compiler.test.ts | 124 ++++---------------------- 2 files changed, 159 insertions(+), 107 deletions(-) create mode 100644 js-api-spec/compiler.node.test.ts diff --git a/js-api-spec/compiler.node.test.ts b/js-api-spec/compiler.node.test.ts new file mode 100644 index 000000000..93c6e1ee7 --- /dev/null +++ b/js-api-spec/compiler.node.test.ts @@ -0,0 +1,142 @@ +// Copyright 2023 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import type {AsyncCompiler, Compiler, CompileResult} from 'sass'; +import {initAsyncCompiler, initCompiler} from 'sass'; + +import { + asyncImporters, + functions, + getLogger, + importers, + sortCompiled, +} from './compiler.test'; +import {sandbox} from './sandbox'; +import {URL} from './utils'; + +describe('Compiler', () => { + let compiler: Compiler; + + beforeEach(() => { + compiler = initCompiler(); + }); + + afterEach(() => { + compiler.dispose(); + }); + + describe('compile', () => { + it('performs complete compilations', () => + sandbox(dir => { + const logger = getLogger(); + dir.write({'input.scss': '@import "bar"; .fn {value: foo(bar)}'}); + const result = compiler.compile(dir('input.scss'), { + importers, + functions, + logger, + }); + expect(result.css).toEqualIgnoringWhitespace( + '.import {value: bar;} .fn {value: "bar";}' + ); + expect(logger.debug).toHaveBeenCalledTimes(1); + })); + + it('performs compilations in callbacks', () => + sandbox(dir => { + dir.write({'input-nested.scss': 'x {y: z}'}); + const nestedImporter = { + canonicalize: (url: string) => new URL(`u:${url}`), + load: (url: typeof URL) => ({ + contents: compiler.compile(dir('input-nested.scss')).css, + syntax: 'scss' as const, + }), + }; + dir.write({'input.scss': '@import "nested"; a {b: c}'}); + const result = compiler.compile(dir('input.scss'), { + importers: [nestedImporter], + }); + expect(result.css).toEqualIgnoringWhitespace('x {y: z;} a {b: c;}'); + })); + + it('throws after being disposed', () => + sandbox(dir => { + dir.write({'input.scss': '$a: b; c {d: $a}'}); + compiler.dispose(); + expect(() => compiler.compile(dir('input.scss'))).toThrowError(); + })); + }); +}); + +describe('AsyncCompiler', () => { + let compiler: AsyncCompiler; + const runs = 1000; // Number of concurrent compilations to run + + // A slow importer that executes a callback after a delay + const getSlowImporter = (callback: () => void) => ({ + canonicalize: async (url: string) => new URL('foo:bar'), + load: async (url: typeof URL) => { + await new Promise(resolve => setTimeout(resolve, 100)); + callback(); + return {contents: '', syntax: 'scss' as const}; + }, + }); + + beforeEach(async () => { + compiler = await initAsyncCompiler(); + }); + + afterEach(async () => { + await compiler.dispose(); + }); + + describe('compileAsync', () => { + it('handles multiple concurrent compilations', () => + sandbox(async dir => { + const logger = getLogger(); + const compilations = Array(runs) + .fill(0) + .map((_, i) => { + const filename = `input-${i}.scss`; + dir.write({ + [filename]: `@import "${i}"; .fn {value: foo(${i})}`, + }); + return compiler.compileAsync(dir(filename), { + importers: asyncImporters, + functions, + logger, + }); + }); + Array.from(await Promise.all(compilations)) + .map((result: CompileResult) => result.css) + .sort(sortCompiled) + .forEach((result, i) => { + expect(result).toEqualIgnoringWhitespace( + `.import {value: ${i};} .fn {value: "${i}";}` + ); + }); + expect(logger.debug).toHaveBeenCalledTimes(runs); + })); + + it('throws after being disposed', async () => { + sandbox(async dir => { + dir.write({'input.scss': '$a: b; c {d: $a}'}); + await compiler.dispose(); + expect(() => compiler.compileAsync(dir('input.scss'))).toThrowError(); + }); + }); + + it('waits for compilations to finish before disposing', () => + sandbox(async dir => { + let completed = false; + dir.write({'input.scss': '@import "slow"'}); + const compilation = compiler.compileAsync(dir('input.scss'), { + importers: [getSlowImporter(() => (completed = true))], + }); + expect(completed).toBeFalse(); + await compiler.dispose(); + expect(completed).toBeTrue(); + await expectAsync(compilation).toBeResolved(); + })); + }); +}); diff --git a/js-api-spec/compiler.test.ts b/js-api-spec/compiler.test.ts index 8215ed376..a9d60fc32 100644 --- a/js-api-spec/compiler.test.ts +++ b/js-api-spec/compiler.test.ts @@ -5,12 +5,13 @@ import type {AsyncCompiler, Compiler, CompileResult} from 'sass'; import {initAsyncCompiler, initCompiler, SassString} from 'sass'; -import {sandbox} from './sandbox'; import {spy, URL} from './utils'; -const functions = {'foo($args)': (args: unknown) => new SassString(`${args}`)}; +export const functions = { + 'foo($args)': (args: unknown) => new SassString(`${args}`), +}; -const importers = [ +export const importers = [ { canonicalize: (url: string) => new URL(`u:${url}`), load: (url: typeof URL) => ({ @@ -20,7 +21,7 @@ const importers = [ }, ]; -const asyncImporters = [ +export const asyncImporters = [ { canonicalize: (url: string) => Promise.resolve(importers[0].canonicalize(url)), @@ -28,10 +29,20 @@ const asyncImporters = [ }, ]; -const getLogger = () => ({debug: spy(() => {})}); +export const getLogger = () => ({debug: spy(() => {})}); + +/* A slow importer that executes a callback after a delay */ +export const getSlowImporter = (callback: () => void) => ({ + canonicalize: async (url: string) => new URL('foo:bar'), + load: async (url: typeof URL) => { + await new Promise(resolve => setTimeout(resolve, 100)); + callback(); + return {contents: '', syntax: 'scss' as const}; + }, +}); /* Sort the output of the example CSS so it can be compared */ -const sortCompiled = (a: string, b: string) => { +export const sortCompiled = (a: string, b: string) => { const aMatch = a.match(/value: (\d+);/); const bMatch = b.match(/value: (\d+);/); if (!aMatch || !bMatch) { @@ -85,63 +96,12 @@ describe('Compiler', () => { expect(() => compiler.compileString('$a: b; c {d: $a}')).toThrowError(); }); }); - - describe('compile', () => { - it('performs complete compilations', () => - sandbox(dir => { - const logger = getLogger(); - dir.write({'input.scss': '@import "bar"; .fn {value: foo(bar)}'}); - const result = compiler.compile(dir('input.scss'), { - importers, - functions, - logger, - }); - expect(result.css).toEqualIgnoringWhitespace( - '.import {value: bar;} .fn {value: "bar";}' - ); - expect(logger.debug).toHaveBeenCalledTimes(1); - })); - - it('performs compilations in callbacks', () => - sandbox(dir => { - dir.write({'input-nested.scss': 'x {y: z}'}); - const nestedImporter = { - canonicalize: (url: string) => new URL(`u:${url}`), - load: (url: typeof URL) => ({ - contents: compiler.compile(dir('input-nested.scss')).css, - syntax: 'scss' as const, - }), - }; - dir.write({'input.scss': '@import "nested"; a {b: c}'}); - const result = compiler.compile(dir('input.scss'), { - importers: [nestedImporter], - }); - expect(result.css).toEqualIgnoringWhitespace('x {y: z;} a {b: c;}'); - })); - - it('throws after being disposed', () => - sandbox(dir => { - dir.write({'input.scss': '$a: b; c {d: $a}'}); - compiler.dispose(); - expect(() => compiler.compile(dir('input.scss'))).toThrowError(); - })); - }); }); describe('AsyncCompiler', () => { let compiler: AsyncCompiler; const runs = 1000; // Number of concurrent compilations to run - // A slow importer that executes a callback after a delay - const getSlowImporter = (callback: () => void) => ({ - canonicalize: async (url: string) => new URL('foo:bar'), - load: async (url: typeof URL) => { - await new Promise(resolve => setTimeout(resolve, 100)); - callback(); - return {contents: '', syntax: 'scss' as const}; - }, - }); - beforeEach(async () => { compiler = await initAsyncCompiler(); }); @@ -190,54 +150,4 @@ describe('AsyncCompiler', () => { await expectAsync(compilation).toBeResolved(); }); }); - - describe('compileAsync', () => { - it('handles multiple concurrent compilations', () => - sandbox(async dir => { - const logger = getLogger(); - const compilations = Array(runs) - .fill(0) - .map((_, i) => { - const filename = `input-${i}.scss`; - dir.write({ - [filename]: `@import "${i}"; .fn {value: foo(${i})}`, - }); - return compiler.compileAsync(dir(filename), { - importers: asyncImporters, - functions, - logger, - }); - }); - Array.from(await Promise.all(compilations)) - .map((result: CompileResult) => result.css) - .sort(sortCompiled) - .forEach((result, i) => { - expect(result).toEqualIgnoringWhitespace( - `.import {value: ${i};} .fn {value: "${i}";}` - ); - }); - expect(logger.debug).toHaveBeenCalledTimes(runs); - })); - - it('throws after being disposed', async () => { - sandbox(async dir => { - dir.write({'input.scss': '$a: b; c {d: $a}'}); - await compiler.dispose(); - expect(() => compiler.compileAsync(dir('input.scss'))).toThrowError(); - }); - }); - - it('waits for compilations to finish before disposing', () => - sandbox(async dir => { - let completed = false; - dir.write({'input.scss': '@import "slow"'}); - const compilation = compiler.compileAsync(dir('input.scss'), { - importers: [getSlowImporter(() => (completed = true))], - }); - expect(completed).toBeFalse(); - await compiler.dispose(); - expect(completed).toBeTrue(); - await expectAsync(compilation).toBeResolved(); - })); - }); }); From a2e6f0cd1f141522ad9a592953d9cd68931ad44f Mon Sep 17 00:00:00 2001 From: Ed Rivas Date: Mon, 27 Nov 2023 21:23:09 +0000 Subject: [PATCH 12/21] Address review --- js-api-spec/compiler.node.test.ts | 13 +------------ js-api-spec/compiler.test.ts | 13 ------------- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/js-api-spec/compiler.node.test.ts b/js-api-spec/compiler.node.test.ts index 93c6e1ee7..34994833d 100644 --- a/js-api-spec/compiler.node.test.ts +++ b/js-api-spec/compiler.node.test.ts @@ -9,8 +9,8 @@ import { asyncImporters, functions, getLogger, + getSlowImporter, importers, - sortCompiled, } from './compiler.test'; import {sandbox} from './sandbox'; import {URL} from './utils'; @@ -72,16 +72,6 @@ describe('AsyncCompiler', () => { let compiler: AsyncCompiler; const runs = 1000; // Number of concurrent compilations to run - // A slow importer that executes a callback after a delay - const getSlowImporter = (callback: () => void) => ({ - canonicalize: async (url: string) => new URL('foo:bar'), - load: async (url: typeof URL) => { - await new Promise(resolve => setTimeout(resolve, 100)); - callback(); - return {contents: '', syntax: 'scss' as const}; - }, - }); - beforeEach(async () => { compiler = await initAsyncCompiler(); }); @@ -109,7 +99,6 @@ describe('AsyncCompiler', () => { }); Array.from(await Promise.all(compilations)) .map((result: CompileResult) => result.css) - .sort(sortCompiled) .forEach((result, i) => { expect(result).toEqualIgnoringWhitespace( `.import {value: ${i};} .fn {value: "${i}";}` diff --git a/js-api-spec/compiler.test.ts b/js-api-spec/compiler.test.ts index a9d60fc32..22de250ae 100644 --- a/js-api-spec/compiler.test.ts +++ b/js-api-spec/compiler.test.ts @@ -41,18 +41,6 @@ export const getSlowImporter = (callback: () => void) => ({ }, }); -/* Sort the output of the example CSS so it can be compared */ -export const sortCompiled = (a: string, b: string) => { - const aMatch = a.match(/value: (\d+);/); - const bMatch = b.match(/value: (\d+);/); - if (!aMatch || !bMatch) { - throw new Error( - `Failed to parse ${a} or ${b} as numbers to determine sort order` - ); - } - return Number(aMatch[1]) - Number(bMatch[1]); -}; - describe('Compiler', () => { let compiler: Compiler; @@ -123,7 +111,6 @@ describe('AsyncCompiler', () => { ); Array.from(await Promise.all(compilations)) .map((result: CompileResult) => result.css) - .sort(sortCompiled) .forEach((result, i) => { expect(result).toEqualIgnoringWhitespace( `.import {value: ${i};} .fn {value: "${i}";}` From 25562de91ebff38ae235f8219559e98dc52107e4 Mon Sep 17 00:00:00 2001 From: Ed Rivas Date: Mon, 27 Nov 2023 21:34:59 +0000 Subject: [PATCH 13/21] Lint --- js-api-spec/compiler.node.test.ts | 4 ++-- js-api-spec/compiler.test.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/js-api-spec/compiler.node.test.ts b/js-api-spec/compiler.node.test.ts index 34994833d..a54d20a70 100644 --- a/js-api-spec/compiler.node.test.ts +++ b/js-api-spec/compiler.node.test.ts @@ -46,8 +46,8 @@ describe('Compiler', () => { sandbox(dir => { dir.write({'input-nested.scss': 'x {y: z}'}); const nestedImporter = { - canonicalize: (url: string) => new URL(`u:${url}`), - load: (url: typeof URL) => ({ + canonicalize: () => new URL('foo:bar'), + load: () => ({ contents: compiler.compile(dir('input-nested.scss')).css, syntax: 'scss' as const, }), diff --git a/js-api-spec/compiler.test.ts b/js-api-spec/compiler.test.ts index 22de250ae..9aad39fd4 100644 --- a/js-api-spec/compiler.test.ts +++ b/js-api-spec/compiler.test.ts @@ -33,8 +33,8 @@ export const getLogger = () => ({debug: spy(() => {})}); /* A slow importer that executes a callback after a delay */ export const getSlowImporter = (callback: () => void) => ({ - canonicalize: async (url: string) => new URL('foo:bar'), - load: async (url: typeof URL) => { + canonicalize: async () => new URL('foo:bar'), + load: async () => { await new Promise(resolve => setTimeout(resolve, 100)); callback(); return {contents: '', syntax: 'scss' as const}; @@ -67,8 +67,8 @@ describe('Compiler', () => { it('performs compilations in callbacks', () => { const nestedImporter = { - canonicalize: (url: string) => new URL(`u:${url}`), - load: (url: typeof URL) => ({ + canonicalize: () => new URL('foo:bar'), + load: () => ({ contents: compiler.compileString('x {y: z}').css, syntax: 'scss' as const, }), From 286e7fafd73d3428b8e51d85beb10a097d1bed18 Mon Sep 17 00:00:00 2001 From: Ed Rivas Date: Mon, 27 Nov 2023 21:38:30 +0000 Subject: [PATCH 14/21] Increase timeout for slow CI --- js-api-spec/compiler.node.test.ts | 55 ++++++++++++++++--------------- js-api-spec/compiler.test.ts | 2 +- 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/js-api-spec/compiler.node.test.ts b/js-api-spec/compiler.node.test.ts index a54d20a70..f41284502 100644 --- a/js-api-spec/compiler.node.test.ts +++ b/js-api-spec/compiler.node.test.ts @@ -81,39 +81,42 @@ describe('AsyncCompiler', () => { }); describe('compileAsync', () => { - it('handles multiple concurrent compilations', () => - sandbox(async dir => { - const logger = getLogger(); - const compilations = Array(runs) - .fill(0) - .map((_, i) => { - const filename = `input-${i}.scss`; - dir.write({ - [filename]: `@import "${i}"; .fn {value: foo(${i})}`, + it( + 'handles multiple concurrent compilations', + () => + sandbox(async dir => { + const logger = getLogger(); + const compilations = Array(runs) + .fill(0) + .map((_, i) => { + const filename = `input-${i}.scss`; + dir.write({ + [filename]: `@import "${i}"; .fn {value: foo(${i})}`, + }); + return compiler.compileAsync(dir(filename), { + importers: asyncImporters, + functions, + logger, + }); }); - return compiler.compileAsync(dir(filename), { - importers: asyncImporters, - functions, - logger, + Array.from(await Promise.all(compilations)) + .map((result: CompileResult) => result.css) + .forEach((result, i) => { + expect(result).toEqualIgnoringWhitespace( + `.import {value: ${i};} .fn {value: "${i}";}` + ); }); - }); - Array.from(await Promise.all(compilations)) - .map((result: CompileResult) => result.css) - .forEach((result, i) => { - expect(result).toEqualIgnoringWhitespace( - `.import {value: ${i};} .fn {value: "${i}";}` - ); - }); - expect(logger.debug).toHaveBeenCalledTimes(runs); - })); + expect(logger.debug).toHaveBeenCalledTimes(runs); + }), + 40_000 // Increase timeout for slow CI + ); - it('throws after being disposed', async () => { + it('throws after being disposed', () => sandbox(async dir => { dir.write({'input.scss': '$a: b; c {d: $a}'}); await compiler.dispose(); expect(() => compiler.compileAsync(dir('input.scss'))).toThrowError(); - }); - }); + })); it('waits for compilations to finish before disposing', () => sandbox(async dir => { diff --git a/js-api-spec/compiler.test.ts b/js-api-spec/compiler.test.ts index 9aad39fd4..b80145400 100644 --- a/js-api-spec/compiler.test.ts +++ b/js-api-spec/compiler.test.ts @@ -117,7 +117,7 @@ describe('AsyncCompiler', () => { ); }); expect(logger.debug).toHaveBeenCalledTimes(runs); - }); + }, 15_000); // Increase timeout for slow CI it('throws after being disposed', async () => { await compiler.dispose(); From 6dcdd3824f575e495c25bd5e21d2d406453fe2f2 Mon Sep 17 00:00:00 2001 From: James Stuckey Weber Date: Tue, 12 Dec 2023 12:43:42 -0500 Subject: [PATCH 15/21] Move from slowImporter to triggeredImporter --- js-api-spec/compiler.node.test.ts | 12 ++++++--- js-api-spec/compiler.test.ts | 43 ++++++++++++++++++++++--------- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/js-api-spec/compiler.node.test.ts b/js-api-spec/compiler.node.test.ts index f41284502..16a0402b4 100644 --- a/js-api-spec/compiler.node.test.ts +++ b/js-api-spec/compiler.node.test.ts @@ -9,7 +9,7 @@ import { asyncImporters, functions, getLogger, - getSlowImporter, + getTriggeredImporter, importers, } from './compiler.test'; import {sandbox} from './sandbox'; @@ -122,11 +122,17 @@ describe('AsyncCompiler', () => { sandbox(async dir => { let completed = false; dir.write({'input.scss': '@import "slow"'}); + const {importer, triggerComplete} = getTriggeredImporter( + () => (completed = true) + ); const compilation = compiler.compileAsync(dir('input.scss'), { - importers: [getSlowImporter(() => (completed = true))], + importers: [importer], }); + const disposalPromise = compiler.dispose(); expect(completed).toBeFalse(); - await compiler.dispose(); + triggerComplete(); + + await disposalPromise; expect(completed).toBeTrue(); await expectAsync(compilation).toBeResolved(); })); diff --git a/js-api-spec/compiler.test.ts b/js-api-spec/compiler.test.ts index b80145400..e873a2414 100644 --- a/js-api-spec/compiler.test.ts +++ b/js-api-spec/compiler.test.ts @@ -2,7 +2,7 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import type {AsyncCompiler, Compiler, CompileResult} from 'sass'; +import type {AsyncCompiler, Compiler, CompileResult, Importer} from 'sass'; import {initAsyncCompiler, initCompiler, SassString} from 'sass'; import {spy, URL} from './utils'; @@ -31,15 +31,27 @@ export const asyncImporters = [ export const getLogger = () => ({debug: spy(() => {})}); -/* A slow importer that executes a callback after a delay */ -export const getSlowImporter = (callback: () => void) => ({ - canonicalize: async () => new URL('foo:bar'), - load: async () => { - await new Promise(resolve => setTimeout(resolve, 100)); - callback(); - return {contents: '', syntax: 'scss' as const}; - }, -}); +/* A trigged importer that executes a callback after a trigger is called */ +export function getTriggeredImporter(callback: () => void): { + importer: Importer; + triggerComplete: () => void; +} { + let promiseResolve: (value: unknown) => void; + const awaitedPromise = new Promise(resolve => { + promiseResolve = resolve; + }); + return { + importer: { + canonicalize: async () => new URL('foo:bar'), + load: async () => { + await awaitedPromise; + callback(); + return {contents: '', syntax: 'scss' as const}; + }, + }, + triggerComplete: () => promiseResolve(undefined), + }; +} describe('Compiler', () => { let compiler: Compiler; @@ -128,11 +140,18 @@ describe('AsyncCompiler', () => { it('waits for compilations to finish before disposing', async () => { let completed = false; + const {importer, triggerComplete} = getTriggeredImporter( + () => (completed = true) + ); const compilation = compiler.compileStringAsync('@import "slow"', { - importers: [getSlowImporter(() => (completed = true))], + importers: [importer], }); + + const disposalPromise = compiler.dispose(); expect(completed).toBeFalse(); - await compiler.dispose(); + triggerComplete(); + + await disposalPromise; expect(completed).toBeTrue(); await expectAsync(compilation).toBeResolved(); }); From 0a14b6fe9c3b41bc7a7bc5bbf11710843648ad0b Mon Sep 17 00:00:00 2001 From: James Stuckey Weber Date: Thu, 14 Dec 2023 10:15:21 -0500 Subject: [PATCH 16/21] Update types, localize run const --- js-api-spec/compiler.node.test.ts | 8 ++++---- js-api-spec/compiler.test.ts | 11 +++++------ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/js-api-spec/compiler.node.test.ts b/js-api-spec/compiler.node.test.ts index 16a0402b4..5544ece78 100644 --- a/js-api-spec/compiler.node.test.ts +++ b/js-api-spec/compiler.node.test.ts @@ -2,7 +2,7 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import type {AsyncCompiler, Compiler, CompileResult} from 'sass'; +import type {AsyncCompiler, Compiler, CompileResult, Importer} from 'sass'; import {initAsyncCompiler, initCompiler} from 'sass'; import { @@ -45,11 +45,11 @@ describe('Compiler', () => { it('performs compilations in callbacks', () => sandbox(dir => { dir.write({'input-nested.scss': 'x {y: z}'}); - const nestedImporter = { + const nestedImporter: Importer = { canonicalize: () => new URL('foo:bar'), load: () => ({ contents: compiler.compile(dir('input-nested.scss')).css, - syntax: 'scss' as const, + syntax: 'scss', }), }; dir.write({'input.scss': '@import "nested"; a {b: c}'}); @@ -70,7 +70,6 @@ describe('Compiler', () => { describe('AsyncCompiler', () => { let compiler: AsyncCompiler; - const runs = 1000; // Number of concurrent compilations to run beforeEach(async () => { compiler = await initAsyncCompiler(); @@ -85,6 +84,7 @@ describe('AsyncCompiler', () => { 'handles multiple concurrent compilations', () => sandbox(async dir => { + const runs = 1000; // Number of concurrent compilations to run const logger = getLogger(); const compilations = Array(runs) .fill(0) diff --git a/js-api-spec/compiler.test.ts b/js-api-spec/compiler.test.ts index e873a2414..2986bc956 100644 --- a/js-api-spec/compiler.test.ts +++ b/js-api-spec/compiler.test.ts @@ -11,7 +11,7 @@ export const functions = { 'foo($args)': (args: unknown) => new SassString(`${args}`), }; -export const importers = [ +export const importers: Array = [ { canonicalize: (url: string) => new URL(`u:${url}`), load: (url: typeof URL) => ({ @@ -21,10 +21,9 @@ export const importers = [ }, ]; -export const asyncImporters = [ +export const asyncImporters: Array = [ { - canonicalize: (url: string) => - Promise.resolve(importers[0].canonicalize(url)), + canonicalize: (url: string) => Promise.resolve(new URL(`u:${url}`)), load: (url: typeof URL) => Promise.resolve(importers[0].load(url)), }, ]; @@ -78,11 +77,11 @@ describe('Compiler', () => { }); it('performs compilations in callbacks', () => { - const nestedImporter = { + const nestedImporter: Importer = { canonicalize: () => new URL('foo:bar'), load: () => ({ contents: compiler.compileString('x {y: z}').css, - syntax: 'scss' as const, + syntax: 'scss', }), }; const result = compiler.compileString('@import "nested"; a {b: c}', { From 0e3b3d99b7150ae991e0b962ed97ee26722da213 Mon Sep 17 00:00:00 2001 From: James Stuckey Weber Date: Thu, 14 Dec 2023 14:27:28 -0500 Subject: [PATCH 17/21] Test compilers throw if constructed --- js-api-spec/compiler.test.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/js-api-spec/compiler.test.ts b/js-api-spec/compiler.test.ts index 2986bc956..5add626d0 100644 --- a/js-api-spec/compiler.test.ts +++ b/js-api-spec/compiler.test.ts @@ -2,8 +2,14 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import type {AsyncCompiler, Compiler, CompileResult, Importer} from 'sass'; -import {initAsyncCompiler, initCompiler, SassString} from 'sass'; +import type {CompileResult, Importer} from 'sass'; +import { + initAsyncCompiler, + initCompiler, + SassString, + AsyncCompiler, + Compiler, +} from 'sass'; import {spy, URL} from './utils'; @@ -95,6 +101,11 @@ describe('Compiler', () => { expect(() => compiler.compileString('$a: b; c {d: $a}')).toThrowError(); }); }); + it('errors if constructor invoked directly', () => { + expect(() => new Compiler()).toThrowError( + /Compiler can not be directly constructed/ + ); + }); }); describe('AsyncCompiler', () => { @@ -155,4 +166,9 @@ describe('AsyncCompiler', () => { await expectAsync(compilation).toBeResolved(); }); }); + it('errors if constructor invoked directly', () => { + expect(() => new AsyncCompiler()).toThrowError( + /AsyncCompiler can not be directly constructed/ + ); + }); }); From 76e676aaddc313aa5a53835cd0c883f54e530314 Mon Sep 17 00:00:00 2001 From: James Stuckey Weber Date: Fri, 15 Dec 2023 09:47:13 -0500 Subject: [PATCH 18/21] Update constructor tests --- js-api-spec/compiler.test.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/js-api-spec/compiler.test.ts b/js-api-spec/compiler.test.ts index 5add626d0..9d9f457ae 100644 --- a/js-api-spec/compiler.test.ts +++ b/js-api-spec/compiler.test.ts @@ -101,8 +101,12 @@ describe('Compiler', () => { expect(() => compiler.compileString('$a: b; c {d: $a}')).toThrowError(); }); }); + it('errors if constructor invoked directly', () => { - expect(() => new Compiler()).toThrowError( + // Strip types to allow calling private constructor. + class Untyped {} + const UntypedCompiler = Compiler as unknown as typeof Untyped; + expect(() => new UntypedCompiler()).toThrowError( /Compiler can not be directly constructed/ ); }); @@ -166,8 +170,12 @@ describe('AsyncCompiler', () => { await expectAsync(compilation).toBeResolved(); }); }); + it('errors if constructor invoked directly', () => { - expect(() => new AsyncCompiler()).toThrowError( + // Strip types to allow calling private constructor. + class Untyped {} + const UntypedAsyncCompiler = AsyncCompiler as unknown as typeof Untyped; + expect(() => new UntypedAsyncCompiler()).toThrowError( /AsyncCompiler can not be directly constructed/ ); }); From 2730112fb2a85205209a33b99842e5c430ae8818 Mon Sep 17 00:00:00 2001 From: James Stuckey Weber Date: Fri, 15 Dec 2023 10:22:14 -0500 Subject: [PATCH 19/21] Test compiler still works after a compilation failure --- js-api-spec/compiler.test.ts | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/js-api-spec/compiler.test.ts b/js-api-spec/compiler.test.ts index 9d9f457ae..a92f66a44 100644 --- a/js-api-spec/compiler.test.ts +++ b/js-api-spec/compiler.test.ts @@ -19,8 +19,8 @@ export const functions = { export const importers: Array = [ { - canonicalize: (url: string) => new URL(`u:${url}`), - load: (url: typeof URL) => ({ + canonicalize: url => new URL(`u:${url}`), + load: url => ({ contents: `.import {value: ${url.pathname}} @debug "imported";`, syntax: 'scss' as const, }), @@ -29,8 +29,8 @@ export const importers: Array = [ export const asyncImporters: Array = [ { - canonicalize: (url: string) => Promise.resolve(new URL(`u:${url}`)), - load: (url: typeof URL) => Promise.resolve(importers[0].load(url)), + canonicalize: url => Promise.resolve(new URL(`u:${url}`)), + load: url => Promise.resolve(importers[0].load(url)), }, ]; @@ -100,6 +100,14 @@ describe('Compiler', () => { compiler.dispose(); expect(() => compiler.compileString('$a: b; c {d: $a}')).toThrowError(); }); + + it('succeeds after a compilation failure', () => { + expect(() => compiler.compileString('a')).toThrowSassException({ + includes: 'expected "{"', + }); + const result2 = compiler.compileString('x {y: z}'); + expect(result2.css).toEqualIgnoringWhitespace('x {y: z;}'); + }); }); it('errors if constructor invoked directly', () => { @@ -169,6 +177,17 @@ describe('AsyncCompiler', () => { expect(completed).toBeTrue(); await expectAsync(compilation).toBeResolved(); }); + + it('succeeds after a compilation failure', async () => { + expectAsync( + async () => await compiler.compileStringAsync('a') + ).toThrowSassException({ + includes: 'expected "{"', + }); + + const result2 = await compiler.compileStringAsync('x {y: z}'); + expect(result2.css).toEqualIgnoringWhitespace('x {y: z;}'); + }); }); it('errors if constructor invoked directly', () => { From 60fc0f9445fe6669e8f565e84be7175f6fa75451 Mon Sep 17 00:00:00 2001 From: Jonny Gerig Meyer Date: Mon, 8 Jan 2024 14:28:33 -0500 Subject: [PATCH 20/21] review --- js-api-spec/compiler.node.test.ts | 2 +- js-api-spec/compiler.test.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/js-api-spec/compiler.node.test.ts b/js-api-spec/compiler.node.test.ts index 5544ece78..8af5c13b8 100644 --- a/js-api-spec/compiler.node.test.ts +++ b/js-api-spec/compiler.node.test.ts @@ -1,4 +1,4 @@ -// Copyright 2023 Google Inc. Use of this source code is governed by an +// Copyright 2024 Google Inc. Use of this source code is governed by an // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. diff --git a/js-api-spec/compiler.test.ts b/js-api-spec/compiler.test.ts index a92f66a44..0d04a5c09 100644 --- a/js-api-spec/compiler.test.ts +++ b/js-api-spec/compiler.test.ts @@ -1,4 +1,4 @@ -// Copyright 2023 Google Inc. Use of this source code is governed by an +// Copyright 2024 Google Inc. Use of this source code is governed by an // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. @@ -19,8 +19,8 @@ export const functions = { export const importers: Array = [ { - canonicalize: url => new URL(`u:${url}`), - load: url => ({ + canonicalize: (url: string) => new URL(`u:${url}`), + load: (url: typeof URL) => ({ contents: `.import {value: ${url.pathname}} @debug "imported";`, syntax: 'scss' as const, }), @@ -29,14 +29,14 @@ export const importers: Array = [ export const asyncImporters: Array = [ { - canonicalize: url => Promise.resolve(new URL(`u:${url}`)), - load: url => Promise.resolve(importers[0].load(url)), + canonicalize: (url: string) => Promise.resolve(new URL(`u:${url}`)), + load: (url: typeof URL) => Promise.resolve(importers[0].load(url)), }, ]; export const getLogger = () => ({debug: spy(() => {})}); -/* A trigged importer that executes a callback after a trigger is called */ +/* A triggered importer that executes a callback after a trigger is called */ export function getTriggeredImporter(callback: () => void): { importer: Importer; triggerComplete: () => void; From b3f1e67a25a4484043fb51cf5e691fb7c2e0eeb5 Mon Sep 17 00:00:00 2001 From: Jonny Gerig Meyer Date: Mon, 8 Jan 2024 14:29:35 -0500 Subject: [PATCH 21/21] remove unnecessary types --- js-api-spec/compiler.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/js-api-spec/compiler.test.ts b/js-api-spec/compiler.test.ts index 0d04a5c09..065f3e7e4 100644 --- a/js-api-spec/compiler.test.ts +++ b/js-api-spec/compiler.test.ts @@ -19,8 +19,8 @@ export const functions = { export const importers: Array = [ { - canonicalize: (url: string) => new URL(`u:${url}`), - load: (url: typeof URL) => ({ + canonicalize: url => new URL(`u:${url}`), + load: url => ({ contents: `.import {value: ${url.pathname}} @debug "imported";`, syntax: 'scss' as const, }), @@ -29,8 +29,8 @@ export const importers: Array = [ export const asyncImporters: Array = [ { - canonicalize: (url: string) => Promise.resolve(new URL(`u:${url}`)), - load: (url: typeof URL) => Promise.resolve(importers[0].load(url)), + canonicalize: url => Promise.resolve(new URL(`u:${url}`)), + load: url => Promise.resolve(importers[0].load(url)), }, ];