feat: Added tools feature for CSS pre/post processors.

This commit is contained in:
Spencer Brower
2026-09-08 13:21:58 -04:00
parent 2f997e89bb
commit 122627c51c
8 changed files with 535 additions and 12 deletions
+154
View File
@@ -0,0 +1,154 @@
#+test
package mustache
import "core:fmt"
import "core:os"
import "core:strings"
import "core:testing"
// tool_test_dirs makes a fresh in/out dir pair under the system temp dir and
// returns their paths (unique per name).
tool_test_dirs :: proc(name: string) -> (in_dir, out_dir: string) {
base, _ := os.temp_directory(context.temp_allocator)
in_dir = fmt.tprintf("%s/thor_tool_test/%s/in", base, name)
out_dir = fmt.tprintf("%s/thor_tool_test/%s/out", base, name)
os.make_directory_all(in_dir)
os.make_directory_all(out_dir)
return
}
// Tool_Test_Ctx carries the asset path as a context value (mustache resolves
// tag keys from the context, not from quoted literals).
Tool_Test_Ctx :: struct {
p: string,
}
@(test)
test_tool_unknown_tool :: proc(t: ^testing.T) {
reg: Tool_Registry // no commands configured
tpl, _ := parse(`{{ p | tool nope }}`)
defer delete_template(&tpl)
_, err := render(tpl, Tool_Test_Ctx{p = "/a.css"}, tools = &reg)
testing.expect(t, err != nil, "unknown tool should be a hard error")
}
@(test)
test_tool_unavailable_without_registry :: proc(t: ^testing.T) {
// No registry threaded through (tools == nil): the pipe must fail rather
// than silently do nothing.
tpl, _ := parse(`{{ p | tool anything }}`)
defer delete_template(&tpl)
_, err := render(tpl, Tool_Test_Ctx{p = "/a.css"})
testing.expect(t, err != nil, "tool pipe without a registry should error")
}
@(test)
test_tool_arg_count :: proc(t: ^testing.T) {
reg: Tool_Registry
// Zero args.
tpl0, _ := parse(`{{ p | tool }}`)
defer delete_template(&tpl0)
_, err0 := render(tpl0, Tool_Test_Ctx{p = "/a.css"}, tools = &reg)
testing.expect(t, err0 != nil, "tool with no name should error")
// Two args.
tpl2, _ := parse(`{{ p | tool one two }}`)
defer delete_template(&tpl2)
_, err2 := render(tpl2, Tool_Test_Ctx{p = "/a.css"}, tools = &reg)
testing.expect(t, err2 != nil, "tool with two args should error")
}
@(test)
test_tool_absolute_url_passthrough :: proc(t: ^testing.T) {
// A CDN URL has no local source; it should pass through untouched.
reg: Tool_Registry
url, err := run_tool(&reg, "anything", "https://cdn.example.com/x.css", 0)
testing.expect(t, err == nil, "absolute URL should not error")
testing.expect_value(t, url, "https://cdn.example.com/x.css")
}
@(test)
test_tool_source_not_found :: proc(t: ^testing.T) {
in_dir, out_dir := tool_test_dirs("missing_src")
commands := make(map[string]string, context.temp_allocator)
commands["cp"] = "cp {{in}} {{out}}"
reg := Tool_Registry {
commands = commands,
input_dir = in_dir,
output_dir = out_dir,
}
_, err := run_tool(&reg, "cp", "/nope.css", 0)
testing.expect(t, err != nil, "missing source file should be a hard error")
}
@(test)
test_tool_missing_binary :: proc(t: ^testing.T) {
in_dir, out_dir := tool_test_dirs("missing_bin")
_ = os.write_entire_file_from_string(fmt.tprintf("%s/a.css", in_dir), "body{}")
commands := make(map[string]string, context.temp_allocator)
commands["ghost"] = "thor-tool-does-not-exist-xyz {{in}} {{out}}"
reg := Tool_Registry {
commands = commands,
input_dir = in_dir,
output_dir = out_dir,
}
_, err := run_tool(&reg, "ghost", "/a.css", 0)
testing.expect(t, err != nil, "missing binary should be caught by PATH check")
}
@(test)
test_tool_runs :: proc(t: ^testing.T) {
when ODIN_OS != .Windows {
in_dir, out_dir := tool_test_dirs("runs")
os.make_directory_all(fmt.tprintf("%s/css", in_dir))
_ = os.write_entire_file_from_string(
fmt.tprintf("%s/css/main.css", in_dir),
"body { color: red }",
)
commands := make(map[string]string, context.temp_allocator)
commands["copyit"] = "cp {{in}} {{out}}"
reg := Tool_Registry {
commands = commands,
input_dir = in_dir,
output_dir = out_dir,
}
tpl, _ := parse(`{{ p | tool copyit }}`)
defer delete_template(&tpl)
result, err := render(tpl, Tool_Test_Ctx{p = "/css/main.css"}, tools = &reg)
defer delete(result)
testing.expect(t, err == nil, "cp tool should succeed")
testing.expect_value(t, result, "/css/main.css")
// The tool wrote the output file.
written := fmt.tprintf("%s/css/main.css", out_dir)
data, rerr := os.read_entire_file_from_path(written, context.temp_allocator)
testing.expect(t, rerr == nil, "output file should exist")
testing.expect_value(t, string(data), "body { color: red }")
// The result was memoized; a second call for the same asset returns the
// same URL from the registry (mutated through the pointer).
url2, err2 := run_tool(&reg, "copyit", "/css/main.css", 0)
testing.expect(t, err2 == nil, "memoized call should not error")
testing.expect_value(t, url2, "/css/main.css")
testing.expect(t, len(reg.manifest) == 1, "result should be memoized")
}
}
@(test)
test_tool_minify_section :: proc(t: ^testing.T) {
// The command template is itself mustache: {{#minify}} should expand only
// when the registry has minify on. Verify via the rendered command string.
cmd, err := render_command("prog {{#minify}}--min{{/minify}} {{in}}", "/a", "/b", true, 0)
testing.expect(t, err == nil, "render_command should succeed (minify on)")
testing.expect(t, strings.contains(cmd, "--min"), "minify section should render when on")
cmd2, err2 := render_command("prog {{#minify}}--min{{/minify}} {{in}}", "/a", "/b", false, 0)
testing.expect(t, err2 == nil, "render_command should succeed (minify off)")
testing.expect(t, !strings.contains(cmd2, "--min"), "minify section should be empty when off")
}