testmark-go

README.md

go-testmark v2๐Ÿ”—

A clean, modern API for working with testmark documents

This is version 2 of go-testmark, designed to address the pain points and improve the user experience of the original library.

Package Organization๐Ÿ”—

  • github.com/warpfork/go-testmark/v2 - Core testmark functionality
  • github.com/warpfork/go-testmark/v2/testexec - Command execution and Go program testing
  • github.com/warpfork/go-testmark/v2/doctest - Experimental documentation testing

Key Improvements in v2๐Ÿ”—

๐Ÿš€ No More Boilerplate๐Ÿ”—

  • โœ… Directory indexing happens automatically (no more doc.BuildDirIndex())
  • โœ… Simplified patch accumulation with built-in support
  • โœ… All functionality in one package (no more multiple imports)

๐Ÿ”’ Clean API Design๐Ÿ”—

  • โœ… All struct fields are private (no accidental mutations)
  • โœ… Clean accessor methods and constructors
  • โœ… Immutable operations (patching returns new documents)

โšก Enhanced Features๐Ÿ”—

  • โœ… Safe iterators for documents and directories
  • โœ… Built-in io/fs support for filesystem-like access
  • โœ… Enhanced testexec with Go module support
  • โœ… Contemporary flag handling
  • โœ… Method chaining for fluent APIs

Quick Start๐Ÿ”—

package main

import (
    "fmt"
    testmark "github.com/warpfork/go-testmark/v2"
)

func main() {
    // Parse a document - directory indexing happens automatically!
    doc, err := testmark.ReadFile("example.md")
    if err != nil {
        panic(err)
    }

    // Access hunks easily
    hunk := doc.HunkByName("my-data")
    fmt.Printf("Content: %s\n", hunk.String())

    // Use clean iterators (Go-idiomatic)
iter := doc.Hunks()
for hunk, ok := iter.Next(); ok; hunk, ok = iter.Next() {
    fmt.Printf("%s: %d bytes\n", hunk.Name(), len(hunk.Body()))
}

    // Navigate like a filesystem
    config := doc.Root().Child("config").Child("database")
    if config != nil {
        fmt.Printf("DB config: %s\n", config.Hunk().String())
    }
}

Core Concepts๐Ÿ”—

Documents with Private Fields๐Ÿ”—

All document internals are now private. Use accessor methods:

doc := testmark.MustParseString(data)

// โœ… Clean accessors
fmt.Println(doc.Count())                    // number of hunks
fmt.Println(doc.HunkNames())                // all hunk names
hunk := doc.HunkByName("my-hunk")           // get specific hunk
root := doc.Root()                          // get directory tree

// โŒ No more direct field access
// doc.DataHunks - this doesn't exist anymore!
// doc.BuildDirIndex() - happens automatically!

Safe Iterators๐Ÿ”—

No more exposing internal slices and maps:

// Iterate over hunks
for iter := doc.Hunks(); iter.Next(); {
    hunk := iter.Hunk()
    // work with hunk
}

// Iterate over directory children
for iter := entry.Children(); iter.Next(); {
    child := iter.Entry()
    // work with child
}

// Functional style
doc.Hunks().ForEach(func(hunk *testmark.Hunk) error {
    fmt.Println(hunk.Name())
    return nil
})

Simplified Patching๐Ÿ”—

Patching is now clean and supports accumulation:

// Simple patching
newHunk := testmark.NewHunkString("data", "", "new content")
patched := doc.Patch(newHunk)

// Accumulated patching
doc = doc.EnablePatching()  // method chaining!
patches := doc.Patches()
patches.Add(testmark.NewHunkString("hunk1", "", "content1"))
patches.Add(testmark.NewHunkString("hunk2", "", "content2"))
result := patches.ApplyTo(doc)

// Auto-write with patches
doc.AutoWriteFile("output.md") // applies patches if enabled

Built-in Filesystem Support๐Ÿ”—

No more separate fs package:

// Use document as filesystem
fs := doc.FS()
content, err := doc.ReadFile("path/to/hunk")
entries, err := doc.ReadDir("directory")
matches, err := doc.Glob("config/*")

// Walk the entire tree
doc.Root().Walk(func(entry *testmark.DirEntry) error {
    fmt.Printf("Visiting: %s\n", entry.Path())
    return nil
})

Enhanced Test Execution๐Ÿ”—

Simple Test Execution๐Ÿ”—

func TestMyCommands(t *testing.T) {
    doc := testmark.MustReadFile("tests.md")
    
    // Test execution with sensible defaults
    testmark.TestExec(t, doc.Root().Child("my-test"), nil)
}

Go Module Support๐Ÿ”—

Built-in support for running Go programs:

config := testmark.DefaultExecConfig()
config.ExecFn = testmark.GoExecFunc("github.com/myorg/mymodule")

// Now "go run main.go" and "./main.go" both work automatically
testmark.TestExec(t, entry, config)

Test Suites๐Ÿ”—

Simplified test suite management:

func TestSuite(t *testing.T) {
    // Use fsx.DirFS for write support if needed
    fs := fsx.DirFS(".")
    
    suite := testmark.NewSuite(fs)
    suite.MustAddExec("*.md", "test-*")                    // basic execution
    suite.MustAddGoExec("go-*.md", "*", "my/module")      // Go support
    
    suite.Run(t)
}

Contemporary Flag Handling๐Ÿ”—

// Modern flag handling
var regenFlag = flag.Bool("testmark.regen", false, "regenerate fixtures")

func TestMain(m *testing.M) {
    flag.Parse()
    
    // Set global regen behavior
    if *regenFlag {
        testmark.SetAutoRegen(true)
    }
    
    os.Exit(m.Run())
}

// Or check directly
if testmark.ShouldRegen() {
    // regenerate fixtures
}

Migration from v1๐Ÿ”—

Key Changes๐Ÿ”—

v1 v2
doc.BuildDirIndex() Automatic
doc.DataHunks doc.Hunks() iterator
doc.HunksByName[name] doc.HunkByName(name)
doc.DirEnt doc.Root()
&testmark.PatchAccumulator{} doc.EnablePatching().Patches()
Multiple imports Single v2 import
Public fields Private fields + accessors

Migration Example๐Ÿ”—

// v1 code
func oldCode() {
    doc, _ := testmark.ReadFile("test.md")
    doc.BuildDirIndex() // manual step
    
    patches := &testmark.PatchAccumulator{}
    
    for _, hunk := range doc.DataHunks {
        // work with hunk
    }
    
    newDoc := testmark.Patch(doc, patches.Patches...)
}

// v2 equivalent
func newCode() {
    doc := testmark.MustReadFile("test.md") // auto-indexed
    doc = doc.EnablePatching()              // fluent
    
    for iter := doc.Hunks(); iter.Next(); {
        hunk := iter.Hunk()
        // work with hunk
        if shouldUpdate {
            doc.Patches().Add(testmark.NewHunkString(hunk.Name(), "", "new content"))
        }
    }
    
    result := doc.Patches().ApplyTo(doc) // or doc.AutoWrite()
}

Compatibility๐Ÿ”—

v2 is designed as a separate module to avoid breaking existing v1 users:

// v1 users continue unchanged
import "github.com/warpfork/go-testmark"

// v2 users get the new API
import testmark "github.com/warpfork/go-testmark/v2"

Both versions will be maintained, but new features will focus on v2.

Examples๐Ÿ”—

See example_test.go for comprehensive examples of v2 usage patterns.

v2/README.md ยท 6.7 KiB ยท 61d171ddc5