Developer Guide

This document covers how to call the Gonemaster engine directly from Go. It focuses on the core engine package and common workflows.

Go package reference: pkg.go.dev/codeberg.org/pawal/gonemaster/engine

Overview

  • Primary entry point: engine.Run(req) returns a slice of engine.LogEntry.
  • Optional: req.LogCallback streams *logger.Entry as tests run.
  • Profiles can be inspected with engine.EffectiveProfile(req).
  • Planned testcases can be listed with engine.PlannedTestcases(req).
  • For IDN domains, normalize to A-labels with engine/normalization.

Example: run a full test and print JSON

package main

import (
	"encoding/json"
	"fmt"
	"log"

	"codeberg.org/pawal/gonemaster/engine"
	"codeberg.org/pawal/gonemaster/engine/normalization"
)

func main() {
	_, domain := normalization.NormalizeName("example.com")
	req := engine.RunRequest{
		Domain:   domain,
		MinLevel: "NOTICE",
	}
	entries, err := engine.Run(req)
	if err != nil {
		log.Fatal(err)
	}
	payload, err := json.MarshalIndent(entries, "", "  ")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(payload))
}

Example: run a single module and testcase

package main

import (
	"fmt"
	"log"

	"codeberg.org/pawal/gonemaster/engine"
	"codeberg.org/pawal/gonemaster/engine/normalization"
)

func main() {
	_, domain := normalization.NormalizeName("example.com")
	req := engine.RunRequest{
		Domain:   domain,
		Module:   "basic",
		Testcase: "basic01",
		MinLevel: "INFO",
	}
	entries, err := engine.Run(req)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("entries=%d\n", len(entries))
}

Example: stream results as they happen

package main

import (
	"fmt"
	"log"

	"codeberg.org/pawal/gonemaster/engine"
	"codeberg.org/pawal/gonemaster/engine/i18n"
	"codeberg.org/pawal/gonemaster/engine/logger"
	"codeberg.org/pawal/gonemaster/engine/normalization"
)

func main() {
	_, domain := normalization.NormalizeName("example.com")
	req := engine.RunRequest{
		Domain:   domain,
		MinLevel: "INFO",
		LogCallback: func(entry *logger.Entry) error {
			message, found := i18n.TranslateWithStatus("en", entry.Module, entry.Tag, entry.Args)
			if !found {
				message = entry.String()
			}
			fmt.Printf("%6.2f %-8s %s\n", entry.Timestamp, entry.Level(), message)
			return nil
		},
	}
	if _, err := engine.Run(req); err != nil {
		log.Fatal(err)
	}
}

Example: inspect or customize the effective profile

package main

import (
	"fmt"
	"log"

	"codeberg.org/pawal/gonemaster/engine"
)

func main() {
	req := engine.RunRequest{
		Domain:  "example.com",
		Profile: "./profile.yaml",
	}
	profile, err := engine.EffectiveProfile(req)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("unordered=%v\n", profile.Resolver.Defaults.Unordered)
}

Example: plan the testcases before running

package main

import (
	"fmt"
	"log"

	"codeberg.org/pawal/gonemaster/engine"
)

func main() {
	req := engine.RunRequest{Domain: "example.com"}
	testcases, err := engine.PlannedTestcases(req)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("planned=%d\n", len(testcases))
}

Example: run simultaneous tests safely

The engine is re-entrant and safe to run in parallel. The simplest option is to call engine.Run in separate goroutines; each call builds its own per-run state internally. If you need to reuse or customize per-run state explicitly, use engine.NewRunner and engine.RunWithRunner.

package main

import (
	"context"
	"log"
	"sync"

	"codeberg.org/pawal/gonemaster/engine"
)

func main() {
	domains := []string{"example.com", "example.net"}
	var wg sync.WaitGroup

	for _, domain := range domains {
		wg.Add(1)
		go func(domain string) {
			defer wg.Done()
			req := engine.RunRequest{
				Domain:  domain,
				Context: context.Background(),
			}
			if _, err := engine.Run(req); err != nil {
				log.Printf("run %s: %v", domain, err)
			}
		}(domain)
	}

	wg.Wait()
}

Notes

  • engine.Run does not normalize IDNs; normalize with engine/normalization.NormalizeName before running.
  • MinLevel filters log entries at the engine output boundary.
  • LogCallback receives entries before min-level filtering, which is useful for live progress or streaming output.

Caching and tuning knobs

  • Per-run nameserver caches (query cache + error cache) isolate concurrent runs.
  • Hard network errors (host/network unreachable) are remembered per run, on the cache store, to avoid repeated failing dials within that run. Two errors on one address inside the TTL window suppress further queries to it; a single transient error does not. The state is not shared between runs, so one run’s unreachable address cannot change another run’s findings.
  • Error cache TTL is controlled by resolver.defaults.error_cache_ttl (or the --error-cache-ttl CLI flag). The effective TTL is capped by the per-query timeout/retry budget.
  • Positive/negative response TTLs are configured via resolver.defaults.positive_cache_ttl and resolver.defaults.negative_cache_ttl (CLI flags --positive-cache-ttl and --negative-cache-ttl). These settings are intended for global response caching and reuse across runs.
  • Query timing and fallback behavior can be tuned with:
    • resolver.defaults.timeout (--timeout, seconds per attempt)
    • resolver.defaults.retry (--retry, retry count)
    • resolver.defaults.retrans (--retrans, seconds between retries)
    • resolver.defaults.fallback (--fallback / --no-fallback, TCP fallback)