136 lines
3.9 KiB
Go
136 lines
3.9 KiB
Go
package docs_test
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
var (
|
|
markdownLinkPattern = regexp.MustCompile(`\[[^\]]+\]\(([^)]+)\)`)
|
|
goCommandPattern = regexp.MustCompile(`\bgo\s+(?:run|build)\s+(\./[^\s` + "`" + `]+)`)
|
|
)
|
|
|
|
func TestMarkdownRelativeLinksResolve(t *testing.T) {
|
|
repositoryRoot := repositoryRoot(t)
|
|
for _, document := range markdownDocuments(t, repositoryRoot) {
|
|
document := document
|
|
t.Run(relativeTestName(repositoryRoot, document), func(t *testing.T) {
|
|
content := readDocument(t, document)
|
|
for _, match := range markdownLinkPattern.FindAllStringSubmatch(content, -1) {
|
|
target := strings.TrimSpace(match[1])
|
|
if target == "" || strings.HasPrefix(target, "#") || hasExternalScheme(target) {
|
|
continue
|
|
}
|
|
if title := strings.Index(target, ` "`); title >= 0 {
|
|
target = target[:title]
|
|
}
|
|
target = strings.Trim(target, "<>")
|
|
if fragment := strings.IndexByte(target, '#'); fragment >= 0 {
|
|
target = target[:fragment]
|
|
}
|
|
if target == "" {
|
|
continue
|
|
}
|
|
unescaped, err := url.PathUnescape(target)
|
|
if err != nil {
|
|
t.Errorf("link %q is not path-encoded correctly: %v", match[1], err)
|
|
continue
|
|
}
|
|
resolved := filepath.Clean(filepath.Join(filepath.Dir(document), filepath.FromSlash(unescaped)))
|
|
if _, err := os.Stat(resolved); err != nil {
|
|
t.Errorf("link %q resolves to missing path %s: %v", match[1], resolved, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDocumentedGoCommandsReferenceExistingTargets(t *testing.T) {
|
|
repositoryRoot := repositoryRoot(t)
|
|
documents := []string{
|
|
"README.md",
|
|
"deploy/README.md",
|
|
"docs/configuration/reference.md",
|
|
"docs/development/guide.md",
|
|
"docs/testing/strategy.md",
|
|
"docs/testing/test-strategy.md",
|
|
}
|
|
for _, relative := range documents {
|
|
document := filepath.Join(repositoryRoot, filepath.FromSlash(relative))
|
|
content := readDocument(t, document)
|
|
for _, match := range goCommandPattern.FindAllStringSubmatch(content, -1) {
|
|
target := strings.TrimRight(match[1], ",.;:")
|
|
if strings.Contains(target, "...") {
|
|
continue
|
|
}
|
|
resolved := filepath.Clean(filepath.Join(repositoryRoot, filepath.FromSlash(strings.TrimPrefix(target, "./"))))
|
|
if _, err := os.Stat(resolved); err != nil {
|
|
t.Errorf("%s documents missing Go target %q: %v", relative, target, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func markdownDocuments(t *testing.T, repositoryRoot string) []string {
|
|
t.Helper()
|
|
documents := []string{filepath.Join(repositoryRoot, "README.md")}
|
|
for _, directory := range []string{"docs", "deploy", "diagrams"} {
|
|
root := filepath.Join(repositoryRoot, directory)
|
|
err := filepath.WalkDir(root, func(path string, entry os.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !entry.IsDir() && strings.EqualFold(filepath.Ext(entry.Name()), ".md") {
|
|
documents = append(documents, path)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("walk %s: %v", directory, err)
|
|
}
|
|
}
|
|
slices.Sort(documents)
|
|
return documents
|
|
}
|
|
|
|
func repositoryRoot(t *testing.T) string {
|
|
t.Helper()
|
|
workingDirectory, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("get working directory: %v", err)
|
|
}
|
|
root := filepath.Clean(filepath.Join(workingDirectory, ".."))
|
|
if _, err := os.Stat(filepath.Join(root, "go.mod")); err != nil {
|
|
t.Fatalf("locate repository root from %s: %v", workingDirectory, err)
|
|
}
|
|
return root
|
|
}
|
|
|
|
func readDocument(t *testing.T, path string) string {
|
|
t.Helper()
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", path, err)
|
|
}
|
|
return string(content)
|
|
}
|
|
|
|
func hasExternalScheme(target string) bool {
|
|
parsed, err := url.Parse(target)
|
|
return err == nil && parsed.Scheme != ""
|
|
}
|
|
|
|
func relativeTestName(repositoryRoot, document string) string {
|
|
relative, err := filepath.Rel(repositoryRoot, document)
|
|
if err != nil {
|
|
return fmt.Sprintf("document-%x", document)
|
|
}
|
|
return filepath.ToSlash(relative)
|
|
}
|