-
Notifications
You must be signed in to change notification settings - Fork 88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Shared Resources] Add JS API tests #1954
Merged
Merged
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
b71d78f
Add basic compiler tests
jerivas e955667
Test for active compilations when disposing
jerivas 6145b4e
Fix "after disposed" tests
jerivas d1fe650
review
jgerigmeyer 89ef7e5
Ignore whitespace when comparing
jerivas 95fd558
Remove extra wrapper fn
jerivas 032e045
More robust compiler tests
jerivas 36db21b
Test more concurrent compilations
jerivas 7c17ae5
Test compilations in callbacks
jerivas 386167f
Stricter tests for dispose()
jerivas 2ac64d2
Split node-specific compiler tests
jerivas a2e6f0c
Address review
jerivas 25562de
Lint
jerivas 286e7fa
Increase timeout for slow CI
jerivas 27dbeab
Merge branch 'main' of /~https://github.com/sass/sass-spec into feature…
jamesnw 6dcdd38
Move from slowImporter to triggeredImporter
jamesnw 0a14b6f
Update types, localize run const
jamesnw 0e3b3d9
Test compilers throw if constructed
jamesnw 76e676a
Update constructor tests
jamesnw 2730112
Test compiler still works after a compilation failure
jamesnw b1fec2d
Merge branch 'main' into feature.shared-resources
jgerigmeyer 60fc0f9
review
jgerigmeyer b3f1e67
remove unnecessary types
jgerigmeyer 6962fcb
Merge branch 'main' of /~https://github.com/sass/sass-spec into feature…
jamesnw File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
// 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. | ||
|
||
import type {AsyncCompiler, Compiler, CompileResult, Importer} from 'sass'; | ||
import {initAsyncCompiler, initCompiler} from 'sass'; | ||
|
||
import { | ||
asyncImporters, | ||
functions, | ||
getLogger, | ||
getTriggeredImporter, | ||
importers, | ||
} 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: Importer = { | ||
canonicalize: () => new URL('foo:bar'), | ||
load: () => ({ | ||
contents: compiler.compile(dir('input-nested.scss')).css, | ||
syntax: 'scss', | ||
}), | ||
}; | ||
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; | ||
|
||
beforeEach(async () => { | ||
compiler = await initAsyncCompiler(); | ||
}); | ||
|
||
afterEach(async () => { | ||
await compiler.dispose(); | ||
}); | ||
|
||
describe('compileAsync', () => { | ||
it( | ||
'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) | ||
.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) | ||
.forEach((result, i) => { | ||
expect(result).toEqualIgnoringWhitespace( | ||
`.import {value: ${i};} .fn {value: "${i}";}` | ||
); | ||
}); | ||
expect(logger.debug).toHaveBeenCalledTimes(runs); | ||
}), | ||
40_000 // Increase timeout for slow CI | ||
); | ||
|
||
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 => { | ||
let completed = false; | ||
dir.write({'input.scss': '@import "slow"'}); | ||
const {importer, triggerComplete} = getTriggeredImporter( | ||
() => (completed = true) | ||
); | ||
const compilation = compiler.compileAsync(dir('input.scss'), { | ||
importers: [importer], | ||
}); | ||
const disposalPromise = compiler.dispose(); | ||
expect(completed).toBeFalse(); | ||
triggerComplete(); | ||
|
||
await disposalPromise; | ||
expect(completed).toBeTrue(); | ||
await expectAsync(compilation).toBeResolved(); | ||
})); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
// 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. | ||
|
||
import type {CompileResult, Importer} from 'sass'; | ||
import { | ||
initAsyncCompiler, | ||
initCompiler, | ||
SassString, | ||
AsyncCompiler, | ||
Compiler, | ||
} from 'sass'; | ||
|
||
import {spy, URL} from './utils'; | ||
|
||
export const functions = { | ||
'foo($args)': (args: unknown) => new SassString(`${args}`), | ||
}; | ||
|
||
export const importers: Array<Importer> = [ | ||
{ | ||
canonicalize: url => new URL(`u:${url}`), | ||
load: url => ({ | ||
contents: `.import {value: ${url.pathname}} @debug "imported";`, | ||
syntax: 'scss' as const, | ||
}), | ||
}, | ||
]; | ||
|
||
export const asyncImporters: Array<Importer> = [ | ||
{ | ||
canonicalize: url => Promise.resolve(new URL(`u:${url}`)), | ||
load: url => Promise.resolve(importers[0].load(url)), | ||
}, | ||
]; | ||
|
||
export const getLogger = () => ({debug: spy(() => {})}); | ||
|
||
/* A triggered 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; | ||
|
||
beforeEach(() => { | ||
compiler = initCompiler(); | ||
}); | ||
|
||
afterEach(() => { | ||
compiler.dispose(); | ||
}); | ||
|
||
describe('compileString', () => { | ||
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('performs compilations in callbacks', () => { | ||
const nestedImporter: Importer = { | ||
canonicalize: () => new URL('foo:bar'), | ||
load: () => ({ | ||
contents: compiler.compileString('x {y: z}').css, | ||
syntax: 'scss', | ||
}), | ||
}; | ||
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(); | ||
}); | ||
|
||
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', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Blank line before test case There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added in 76e676a |
||
// 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/ | ||
); | ||
}); | ||
}); | ||
|
||
describe('AsyncCompiler', () => { | ||
let compiler: AsyncCompiler; | ||
const runs = 1000; // Number of concurrent compilations to run | ||
|
||
beforeEach(async () => { | ||
compiler = await initAsyncCompiler(); | ||
}); | ||
|
||
afterEach(async () => { | ||
await compiler.dispose(); | ||
}); | ||
|
||
describe('compileStringAsync', () => { | ||
it('handles multiple concurrent compilations', async () => { | ||
const logger = getLogger(); | ||
const compilations = Array(runs) | ||
.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) | ||
.forEach((result, i) => { | ||
expect(result).toEqualIgnoringWhitespace( | ||
`.import {value: ${i};} .fn {value: "${i}";}` | ||
); | ||
}); | ||
expect(logger.debug).toHaveBeenCalledTimes(runs); | ||
}, 15_000); // Increase timeout for slow CI | ||
|
||
it('throws after being disposed', async () => { | ||
await compiler.dispose(); | ||
expect(() => | ||
compiler.compileStringAsync('$a: b; c {d: $a}') | ||
).toThrowError(); | ||
}); | ||
|
||
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: [importer], | ||
}); | ||
|
||
const disposalPromise = compiler.dispose(); | ||
expect(completed).toBeFalse(); | ||
triggerComplete(); | ||
|
||
await disposalPromise; | ||
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', () => { | ||
// 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/ | ||
); | ||
}); | ||
}); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I 100% understand the request, but this is my attempt to do it by using importers and functions similar to the async case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want to testing running multiple synchronous compilations at the same time. This means you'll need to start a second compilation within a callback for the first one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added in 7c17ae5