fix: Fixed race-conditions related to treesitter parsers.

This commit is contained in:
Spencer Brower
2026-08-31 18:44:57 -04:00
parent b932b22f15
commit 2f180f0e4c
9 changed files with 381 additions and 267 deletions
+47 -43
View File
@@ -1,59 +1,49 @@
#+test
package main
import ts "treesitter"
import "core:strings"
import "core:sync"
import "core:testing"
import "treesitter"
// The treesitter parser store is normally initialized by main() via
// treesitter.init_persistent(); the test runner never calls main. init_once
// ensures it happens exactly once no matter which test thread arrives first,
// and preloads the builtin parsers on that single thread — after which the
// concurrent tests only *read* the cache (the unlocked writes in ensure_parser
// are what corrupt it, not reads).
// The treesitter registry is thread-safe and self-initializing (grammar() lazily
// binds it to the heap), and parsers are caller-owned. Each test opens its own
// parser(s) via ts.open_parser, passes them to minify_*, and frees the returned
// string — so the multi-threaded test runner needs no shared state or locking.
@(private = "file")
init_once: sync.Once
setup_treesitter :: proc() {
sync.once_do(&init_once, proc() {
treesitter.init_persistent()
treesitter.ensure_parser("html")
treesitter.ensure_parser("css")
})
open_parsers :: proc(t: ^testing.T) -> (html_parser, css_parser: ts.Parser) {
hg, hok := ts.grammar("html")
cg, cok := ts.grammar("css")
testing.expect(t, hok, "html grammar should load")
testing.expect(t, cok, "css grammar should load")
return ts.open_parser(hg), ts.open_parser(cg)
}
// The builtin parsers are cached and shared per-language, and tree-sitter
// parsers are not reentrant — concurrent parser_parse_string corrupts their
// internal subtree pool. The multi-threaded test runner would otherwise race,
// so serialize parser use behind this mutex. (Production renders on a single
// thread, so it needs no such lock.)
@(private = "file")
parse_mu: sync.Mutex
ts_minify_html :: proc(s: string) -> string {
sync.mutex_lock(&parse_mu)
defer sync.mutex_unlock(&parse_mu)
return minify_html(s)
}
ts_minify_css :: proc(s: string) -> string {
sync.mutex_lock(&parse_mu)
defer sync.mutex_unlock(&parse_mu)
return minify_css(s)
expect_css :: proc(t: ^testing.T, parser: ts.Parser, input, want: string) {
got, ok := minify_css(parser, input)
testing.expect(t, ok)
defer delete(got)
testing.expect_value(t, got, want)
}
// --- minify_html: inline <style> bodies get CSS-minified ---
@(test)
test_minify_html_inline_style :: proc(t: ^testing.T) {
setup_treesitter()
hp, cp := open_parsers(t)
defer ts.parser_delete(hp)
defer ts.parser_delete(cp)
input := `<html><head><style>
body {
color: red;
}
</style></head><body><p>hi</p></body></html>`
result := ts_minify_html(input)
result, ok := minify_html(hp, cp, input)
testing.expect(t, ok)
defer delete(result)
// The CSS body is collapsed by minify_css (delimiters lose surrounding
// whitespace)...
@@ -65,10 +55,15 @@ body {
@(test)
test_minify_html_empty_style :: proc(t: ^testing.T) {
setup_treesitter()
hp, cp := open_parsers(t)
defer ts.parser_delete(hp)
defer ts.parser_delete(cp)
// An empty <style></style> has no raw_text child; must not crash.
input := `<html><head><style></style></head><body></body></html>`
result := ts_minify_html(input)
result, ok := minify_html(hp, cp, input)
testing.expect(t, ok)
defer delete(result)
testing.expect(t, strings.contains(result, "<style></style>"))
}
@@ -76,19 +71,28 @@ test_minify_html_empty_style :: proc(t: ^testing.T) {
@(test)
test_minify_css_leading_and_comment :: proc(t: ^testing.T) {
setup_treesitter()
cg, cok := ts.grammar("css")
testing.expect(t, cok, "css grammar should load")
cp := ts.open_parser(cg)
defer ts.parser_delete(cp)
// Leading whitespace and a comment between two whitespace runs must not
// leave a leading space or a doubled space.
testing.expect_value(t, ts_minify_css("\n/* c */\na {}"), "a{}")
testing.expect_value(t, ts_minify_css(" a {}"), "a{}")
testing.expect_value(t, ts_minify_css("a {}\n/* trailing */"), "a{}")
expect_css(t, cp, "\n/* c */\na {}", "a{}")
expect_css(t, cp, " a {}", "a{}")
expect_css(t, cp, "a {}\n/* trailing */", "a{}")
}
@(test)
test_minify_html_script_preserved :: proc(t: ^testing.T) {
setup_treesitter()
hp, cp := open_parsers(t)
defer ts.parser_delete(hp)
defer ts.parser_delete(cp)
// <script> stays byte-for-byte (whitespace-significant JS).
input := "<html><body><script>\nconst s = \"a b\";\n</script></body></html>"
result := ts_minify_html(input)
result, ok := minify_html(hp, cp, input)
testing.expect(t, ok)
defer delete(result)
testing.expect(t, strings.contains(result, "const s = \"a b\";"))
}