a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
295 lines
8.0 KiB
Go
295 lines
8.0 KiB
Go
package languages
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// quartoSample is a representative .qmd document exercising every node
|
|
// family: frontmatter keys, a parent/child heading pair, a labelled {r}
|
|
// chunk, an unlabelled {python} chunk, and a plain (non-executable)
|
|
// fenced code block that must NOT yield a chunk node.
|
|
const quartoSample = "---\n" +
|
|
"title: \"My Report\"\n" +
|
|
"author: Jane Doe\n" +
|
|
"format:\n" +
|
|
" html:\n" +
|
|
" toc: true\n" + // nested key — must NOT become a frontmatter node
|
|
"---\n" +
|
|
"\n" +
|
|
"# Introduction\n" +
|
|
"\n" +
|
|
"This is the **intro** with a [link](http://x) and `code`.\n" +
|
|
"\n" +
|
|
"## Details\n" +
|
|
"\n" +
|
|
"Some _child_ section prose here.\n" +
|
|
"\n" +
|
|
"```{r}\n" +
|
|
"#| label: fig-plot\n" +
|
|
"plot(cars)\n" +
|
|
"```\n" +
|
|
"\n" +
|
|
"```{python}\n" +
|
|
"import sys\n" +
|
|
"print(sys.version)\n" +
|
|
"```\n" +
|
|
"\n" +
|
|
"```\n" +
|
|
"echo just a plain block, not a chunk\n" +
|
|
"```\n"
|
|
|
|
func extractQuarto(t *testing.T, path, body string) *parserResultQuarto {
|
|
t.Helper()
|
|
e := NewQuartoExtractor()
|
|
res, err := e.Extract(path, []byte(body))
|
|
if err != nil {
|
|
t.Fatalf("Extract: %v", err)
|
|
}
|
|
return &parserResultQuarto{
|
|
nodesByID: indexNodes(res.Nodes),
|
|
nodes: res.Nodes,
|
|
defines: countDefines(res.Edges),
|
|
edges: res.Edges,
|
|
}
|
|
}
|
|
|
|
type parserResultQuarto struct {
|
|
nodesByID map[string]*graph.Node
|
|
nodes []*graph.Node
|
|
defines int
|
|
edges []*graph.Edge
|
|
}
|
|
|
|
func indexNodes(nodes []*graph.Node) map[string]*graph.Node {
|
|
m := make(map[string]*graph.Node, len(nodes))
|
|
for _, n := range nodes {
|
|
m[n.ID] = n
|
|
}
|
|
return m
|
|
}
|
|
|
|
func countDefines(edges []*graph.Edge) int {
|
|
n := 0
|
|
for _, e := range edges {
|
|
if e.Kind == graph.EdgeDefines {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
func TestQuartoLanguageAndExtensions(t *testing.T) {
|
|
e := NewQuartoExtractor()
|
|
if got := e.Language(); got != "quarto" {
|
|
t.Fatalf("Language() = %q, want quarto", got)
|
|
}
|
|
exts := e.Extensions()
|
|
if len(exts) != 1 || exts[0] != ".qmd" {
|
|
t.Fatalf("Extensions() = %v, want [.qmd]", exts)
|
|
}
|
|
}
|
|
|
|
func TestQuartoExtract(t *testing.T) {
|
|
const path = "report.qmd"
|
|
r := extractQuarto(t, path, quartoSample)
|
|
|
|
t.Run("file_node", func(t *testing.T) {
|
|
f := r.nodesByID[path]
|
|
if f == nil {
|
|
t.Fatalf("missing file node %q", path)
|
|
}
|
|
if f.Kind != graph.KindFile {
|
|
t.Fatalf("file node kind = %q, want %q", f.Kind, graph.KindFile)
|
|
}
|
|
if f.Language != "quarto" {
|
|
t.Fatalf("file node language = %q, want quarto", f.Language)
|
|
}
|
|
if f.StartLine != 1 {
|
|
t.Fatalf("file node StartLine = %d, want 1", f.StartLine)
|
|
}
|
|
})
|
|
|
|
t.Run("frontmatter_keys", func(t *testing.T) {
|
|
cases := []string{"title", "author", "format"}
|
|
for _, key := range cases {
|
|
id := path + "::frontmatter::" + key
|
|
n := r.nodesByID[id]
|
|
if n == nil {
|
|
t.Fatalf("missing frontmatter node %q", id)
|
|
}
|
|
if n.Kind != graph.KindConfigKey {
|
|
t.Fatalf("frontmatter %q kind = %q, want %q", key, n.Kind, graph.KindConfigKey)
|
|
}
|
|
if n.Name != key {
|
|
t.Fatalf("frontmatter node Name = %q, want %q", n.Name, key)
|
|
}
|
|
if n.Meta["source"] != "quarto_frontmatter" {
|
|
t.Fatalf("frontmatter %q meta source = %v, want quarto_frontmatter", key, n.Meta["source"])
|
|
}
|
|
}
|
|
// Nested keys (`html`, `toc`) must NOT be emitted.
|
|
for _, key := range []string{"html", "toc"} {
|
|
if n := r.nodesByID[path+"::frontmatter::"+key]; n != nil {
|
|
t.Fatalf("nested key %q wrongly emitted as a frontmatter node", key)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("prose_sections_breadcrumb_and_text", func(t *testing.T) {
|
|
var parent, child *graph.Node
|
|
for _, n := range r.nodes {
|
|
if n.Kind != graph.KindDoc {
|
|
continue
|
|
}
|
|
switch n.Name {
|
|
case "report.qmd > Introduction":
|
|
parent = n
|
|
case "report.qmd > Introduction > Details":
|
|
child = n
|
|
}
|
|
}
|
|
if parent == nil {
|
|
t.Fatalf("missing parent doc section 'report.qmd > Introduction'")
|
|
}
|
|
if child == nil {
|
|
t.Fatalf("missing child doc section 'report.qmd > Introduction > Details'")
|
|
}
|
|
// section_text has markdown inline syntax stripped.
|
|
pt, _ := parent.Meta["section_text"].(string)
|
|
if !strings.Contains(pt, "intro") || strings.Contains(pt, "**") || strings.Contains(pt, "[link]") {
|
|
t.Fatalf("parent section_text not stripped: %q", pt)
|
|
}
|
|
if strings.Contains(pt, "http://x") {
|
|
t.Fatalf("parent section_text kept link URL: %q", pt)
|
|
}
|
|
ct, _ := child.Meta["section_text"].(string)
|
|
if !strings.Contains(ct, "child section prose") {
|
|
t.Fatalf("child section_text missing prose: %q", ct)
|
|
}
|
|
if strings.Contains(ct, "_") {
|
|
t.Fatalf("child section_text kept emphasis markers: %q", ct)
|
|
}
|
|
// Stable, heading-derived IDs (not line-based).
|
|
if parent.ID != path+"::doc:report-qmd-introduction" {
|
|
t.Fatalf("parent doc ID = %q", parent.ID)
|
|
}
|
|
if child.ID != path+"::doc:report-qmd-introduction-details" {
|
|
t.Fatalf("child doc ID = %q", child.ID)
|
|
}
|
|
})
|
|
|
|
t.Run("labelled_r_chunk", func(t *testing.T) {
|
|
id := path + "::chunk:fig-plot"
|
|
n := r.nodesByID[id]
|
|
if n == nil {
|
|
t.Fatalf("missing labelled chunk node %q", id)
|
|
}
|
|
if n.Kind != graph.KindFunction {
|
|
t.Fatalf("chunk kind = %q, want %q", n.Kind, graph.KindFunction)
|
|
}
|
|
if n.Name != "fig-plot" {
|
|
t.Fatalf("chunk Name = %q, want fig-plot", n.Name)
|
|
}
|
|
if n.Meta["chunk_language"] != "r" {
|
|
t.Fatalf("chunk_language = %v, want r", n.Meta["chunk_language"])
|
|
}
|
|
})
|
|
|
|
t.Run("unlabelled_python_chunk", func(t *testing.T) {
|
|
id := path + "::chunk:chunk-python-1"
|
|
n := r.nodesByID[id]
|
|
if n == nil {
|
|
t.Fatalf("missing generated-name chunk node %q", id)
|
|
}
|
|
if n.Kind != graph.KindFunction {
|
|
t.Fatalf("chunk kind = %q, want %q", n.Kind, graph.KindFunction)
|
|
}
|
|
if n.Name != "chunk-python-1" {
|
|
t.Fatalf("chunk Name = %q, want chunk-python-1", n.Name)
|
|
}
|
|
if n.Meta["chunk_language"] != "python" {
|
|
t.Fatalf("chunk_language = %v, want python", n.Meta["chunk_language"])
|
|
}
|
|
})
|
|
|
|
t.Run("plain_block_is_not_a_chunk", func(t *testing.T) {
|
|
for _, n := range r.nodes {
|
|
if n.Kind == graph.KindFunction && strings.Contains(n.Name, "echo") {
|
|
t.Fatalf("plain fenced block wrongly produced a chunk node: %q", n.ID)
|
|
}
|
|
}
|
|
// Exactly two chunk nodes (the {r} and {python} blocks).
|
|
chunks := 0
|
|
for _, n := range r.nodes {
|
|
if n.Kind == graph.KindFunction {
|
|
chunks++
|
|
}
|
|
}
|
|
if chunks != 2 {
|
|
t.Fatalf("chunk node count = %d, want 2", chunks)
|
|
}
|
|
})
|
|
|
|
t.Run("edge_defines_count_and_kinds", func(t *testing.T) {
|
|
// One EdgeDefines per non-file node: 3 frontmatter + 2 doc + 2 chunk = 7.
|
|
nonFile := 0
|
|
for _, n := range r.nodes {
|
|
if n.Kind != graph.KindFile {
|
|
nonFile++
|
|
}
|
|
}
|
|
if nonFile != 7 {
|
|
t.Fatalf("non-file node count = %d, want 7", nonFile)
|
|
}
|
|
if r.defines != nonFile {
|
|
t.Fatalf("EdgeDefines count = %d, want %d", r.defines, nonFile)
|
|
}
|
|
// Every define edge originates at the file node.
|
|
for _, e := range r.edges {
|
|
if e.Kind == graph.EdgeDefines && e.From != path {
|
|
t.Fatalf("EdgeDefines from %q, want file node %q", e.From, path)
|
|
}
|
|
}
|
|
// All node kinds are the expected ones (no new kinds introduced).
|
|
allowed := map[graph.NodeKind]bool{
|
|
graph.KindFile: true, graph.KindConfigKey: true,
|
|
graph.KindDoc: true, graph.KindFunction: true,
|
|
}
|
|
for _, n := range r.nodes {
|
|
if !allowed[n.Kind] {
|
|
t.Fatalf("unexpected node kind %q on %q", n.Kind, n.ID)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestQuartoCRLF(t *testing.T) {
|
|
// CRLF must scan identically to LF.
|
|
crlf := strings.ReplaceAll(quartoSample, "\n", "\r\n")
|
|
r := extractQuarto(t, "report.qmd", crlf)
|
|
if r.nodesByID["report.qmd::chunk:fig-plot"] == nil {
|
|
t.Fatalf("CRLF: missing labelled chunk node")
|
|
}
|
|
if r.nodesByID["report.qmd::frontmatter::title"] == nil {
|
|
t.Fatalf("CRLF: missing frontmatter title node")
|
|
}
|
|
}
|
|
|
|
func TestQuartoTildeFenceAndBraceLabel(t *testing.T) {
|
|
// ~~~ fences and an inline brace label.
|
|
doc := "~~~{r, label=\"setup\"}\n" +
|
|
"library(ggplot2)\n" +
|
|
"~~~\n"
|
|
r := extractQuarto(t, "t.qmd", doc)
|
|
n := r.nodesByID["t.qmd::chunk:setup"]
|
|
if n == nil {
|
|
t.Fatalf("missing tilde-fence chunk with brace label")
|
|
}
|
|
if n.Meta["chunk_language"] != "r" {
|
|
t.Fatalf("chunk_language = %v, want r", n.Meta["chunk_language"])
|
|
}
|
|
}
|