mirror of
https://github.com/omniscale/magnacarto.git
synced 2025-02-23 07:54:10 +01:00
350 lines
8.5 KiB
Go
350 lines
8.5 KiB
Go
//go:generate bash mk_test_funcs.sh
|
|
|
|
package regression
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"math"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"github.com/omniscale/magnacarto/builder"
|
|
"github.com/omniscale/magnacarto/builder/mapnik"
|
|
"github.com/omniscale/magnacarto/builder/mapserver"
|
|
"github.com/omniscale/magnacarto/config"
|
|
"github.com/omniscale/magnacarto/render"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
|
|
"testing"
|
|
)
|
|
|
|
type testCase struct {
|
|
Name string
|
|
Zoom int
|
|
Long float64
|
|
Lat float64
|
|
Width int
|
|
Height int
|
|
CartoCompare bool
|
|
CartoFuzz float64
|
|
CartoPxDiff int64
|
|
MapServerCompare bool
|
|
MapServerFuzz float64
|
|
MapServerPxDiff int64
|
|
}
|
|
|
|
var mapnikRenderer *render.Mapnik
|
|
var mapserverRenderer *render.MapServer
|
|
|
|
func init() {
|
|
mapnikRenderer, _ = render.NewMapnik()
|
|
if mapnikRenderer != nil {
|
|
wd, _ := os.Getwd()
|
|
mapnikRenderer.RegisterFonts(wd)
|
|
}
|
|
mapserverRenderer, _ = render.NewMapServer()
|
|
}
|
|
|
|
var cmdsChecked bool
|
|
|
|
func checkTestCmds(t *testing.T) {
|
|
if cmdsChecked {
|
|
return
|
|
}
|
|
cmdsFound := true
|
|
_, err := exec.LookPath("carto")
|
|
if err != nil {
|
|
t.Error("carto command not found; make sure carto is installed (with npm) and in your PATH")
|
|
cmdsFound = false
|
|
}
|
|
|
|
_, err = exec.LookPath("compare")
|
|
if err != nil {
|
|
t.Error("compare command not found; make sure Image-Magick is installed and in your PATH")
|
|
cmdsFound = false
|
|
}
|
|
|
|
_, err = exec.LookPath("mapserv")
|
|
if err != nil {
|
|
t.Error("mapserv command not found; make sure mapserv is installed and in your PATH")
|
|
cmdsFound = false
|
|
} else {
|
|
out, err := exec.Command("mapserv", "-v").Output()
|
|
if err != nil {
|
|
t.Error("unable to get mapserv version:", err)
|
|
}
|
|
versionMatch := regexp.MustCompile(`version (\d)`).FindStringSubmatch(string(out))
|
|
if len(versionMatch) < 2 {
|
|
t.Error("unable to parse version:", string(out))
|
|
} else {
|
|
majVersion, _ := strconv.ParseInt(versionMatch[1], 10, 32)
|
|
if majVersion < 7 {
|
|
t.Error("mapserver >= 7 required", string(out))
|
|
}
|
|
}
|
|
}
|
|
|
|
if !cmdsFound {
|
|
t.Fatal("not all commands found for regression tests.")
|
|
}
|
|
|
|
cmdsChecked = true
|
|
}
|
|
|
|
func (t *testCase) load() error {
|
|
if _, err := toml.DecodeFile("config.tml", &t); err != nil {
|
|
return err
|
|
}
|
|
if _, err := toml.DecodeFile(filepath.Join("cases", t.Name, "config.tml"), &t); err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func testIt(t *testing.T, c testCase) {
|
|
if testing.Short() {
|
|
t.Skip("skipping regression test in short mode")
|
|
}
|
|
checkTestCmds(t)
|
|
t.Parallel()
|
|
|
|
if err := c.load(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
prepare(t, c)
|
|
buildCarto(t, c)
|
|
buildMagnacarto(t, c, false)
|
|
buildMagnacarto(t, c, true)
|
|
renderMapnik(t, c, "magnacarto")
|
|
renderMapserver(t, c)
|
|
renderMapnik(t, c, "carto")
|
|
compare(t, c)
|
|
}
|
|
|
|
// prepare copies all files to the build directory
|
|
func prepare(t *testing.T, c testCase) {
|
|
caseBuildDir := filepath.Join("build", c.Name)
|
|
if err := os.MkdirAll(caseBuildDir, 0755); err != nil && !os.IsExist(err) {
|
|
t.Fatal(err)
|
|
}
|
|
srcFiles, err := filepath.Glob(filepath.Join("cases", c.Name, "*"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, src := range srcFiles {
|
|
if err := cpFile(filepath.Join(caseBuildDir, filepath.Base(src)), src); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// buildCarto builds the mapnik XML with carto
|
|
func buildCarto(t *testing.T, c testCase) {
|
|
caseBuildDir := filepath.Join("build", c.Name)
|
|
|
|
cmd := exec.Command("carto", "test.mml")
|
|
cmd.Dir = caseBuildDir
|
|
|
|
dst, err := os.Create(filepath.Join(caseBuildDir, "carto.xml"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer dst.Close()
|
|
stdout, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer stdout.Close()
|
|
stderr, err := cmd.StderrPipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer stderr.Close()
|
|
if err := cmd.Start(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := io.Copy(dst, stdout); err != nil {
|
|
t.Fatal("err copying stdout:", err)
|
|
}
|
|
|
|
errContent, _ := ioutil.ReadAll(stderr)
|
|
|
|
if err := cmd.Wait(); err != nil {
|
|
// carto outputs errors to stdout, read content of file to display err
|
|
dst.Close()
|
|
if dst, err := os.Open(filepath.Join(caseBuildDir, "carto.xml")); err == nil {
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
content, _ := ioutil.ReadAll(dst)
|
|
dst.Close()
|
|
t.Log(string(content))
|
|
t.Log(string(errContent))
|
|
}
|
|
t.Fatalf("error calling carto for %s: %s", c.Name, err)
|
|
}
|
|
}
|
|
|
|
func wgsToMerc(long, lat float64) (x, y float64) {
|
|
x = long * 6378137 * math.Pi / 180.0
|
|
y = math.Log(math.Tan((90.0+lat)*math.Pi/360.0)) / math.Pi * 6378137 * math.Pi
|
|
return x, y
|
|
}
|
|
|
|
func extent(c testCase, fixOffset bool) [4]float64 {
|
|
x, y := wgsToMerc(c.Long, c.Lat)
|
|
equatorWidth := 6378137 * math.Pi * 2
|
|
res := (equatorWidth / 256.0) / math.Pow(2, float64(c.Zoom))
|
|
width, height := c.Width, c.Height
|
|
if fixOffset {
|
|
// fix for center/outer pixel differences
|
|
return [4]float64{x - float64(width)/2*res + 0.5*res, y - float64(height)/2*res - 0.5*res, x + float64(width)/2*res + 0.5*res, y + float64(height)/2*res - 0.5*res}
|
|
}
|
|
return [4]float64{x - float64(width)/2*res, y - float64(height)/2*res, x + float64(width)/2*res, y + float64(height)/2*res}
|
|
}
|
|
|
|
func renderMapnik(t *testing.T, c testCase, name string) {
|
|
caseBuildDir := filepath.Join("build", c.Name)
|
|
|
|
mapReq := render.Request{}
|
|
mapReq.EPSGCode = 3857
|
|
mapReq.BBOX = extent(c, true)
|
|
mapReq.Width = c.Width
|
|
mapReq.Height = c.Height
|
|
mapReq.Format = "png24"
|
|
|
|
if mapnikRenderer == nil {
|
|
t.Skip("mapnik not initialized")
|
|
}
|
|
|
|
f, err := os.Create(filepath.Join(caseBuildDir, "render-"+name+".png"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer f.Close()
|
|
err = mapnikRenderer.Render(filepath.Join(caseBuildDir, name+".xml"), f, mapReq)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func renderMapserver(t *testing.T, c testCase) {
|
|
caseBuildDir := filepath.Join("build", c.Name)
|
|
|
|
mapReq := render.Request{}
|
|
mapReq.EPSGCode = 3857
|
|
mapReq.BBOX = extent(c, false)
|
|
mapReq.Width = c.Width
|
|
mapReq.Height = c.Height
|
|
mapReq.Format = "image/png"
|
|
|
|
if mapserverRenderer == nil {
|
|
t.Skip("mapserver not initialized")
|
|
}
|
|
|
|
f, err := os.Create(filepath.Join(caseBuildDir, "render-magnacarto-ms.png"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer f.Close()
|
|
_, err = mapserverRenderer.Render(filepath.Join(caseBuildDir, "magnacarto.map"), f, mapReq)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// buildMagnacarto builds the mapnik XML or mapserver MAP (when mapfile is true) with magnacarto
|
|
func buildMagnacarto(t *testing.T, c testCase, mapfile bool) {
|
|
caseBuildDir := filepath.Join("build", c.Name)
|
|
|
|
suffix := ".xml"
|
|
if mapfile {
|
|
suffix = ".map"
|
|
}
|
|
|
|
conf := config.Magnacarto{BaseDir: caseBuildDir}
|
|
here, _ := os.Getwd()
|
|
conf.Mapnik.FontDirs = []string{here}
|
|
locator := conf.Locator()
|
|
|
|
var m builder.MapWriter
|
|
if mapfile {
|
|
m = mapserver.New(locator)
|
|
} else {
|
|
m = mapnik.New(locator)
|
|
}
|
|
|
|
b := builder.New(m)
|
|
b.SetMML(filepath.Join(caseBuildDir, "test.mml"))
|
|
|
|
if err := b.Build(); err != nil {
|
|
log.Fatal("error building map: ", err)
|
|
}
|
|
|
|
if err := m.WriteFiles(filepath.Join(caseBuildDir, "magnacarto"+suffix)); err != nil {
|
|
log.Fatal("error writing map: ", err)
|
|
}
|
|
}
|
|
|
|
func compare(t *testing.T, c testCase) {
|
|
dir := filepath.Join("build", c.Name)
|
|
|
|
if c.CartoCompare {
|
|
compareImg(t, dir, "render-carto.png", "render-magnacarto.png", c.CartoFuzz, c.CartoPxDiff)
|
|
}
|
|
if c.MapServerCompare {
|
|
compareImg(t, dir, "render-magnacarto.png", "render-magnacarto-ms.png", c.MapServerFuzz, c.MapServerPxDiff)
|
|
}
|
|
}
|
|
|
|
func compareImg(t *testing.T, dir, fileA, fileB string, fuzz float64, expected int64) {
|
|
fileDiff := "diff-" + fileA + "-" + fileB + ".png"
|
|
cmd := exec.Command(
|
|
"compare", "-metric", "AE", "-fuzz", fmt.Sprintf("%.2f%%", fuzz), fileA, fileB, fileDiff,
|
|
)
|
|
cmd.Dir = dir
|
|
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
// ignore exit status 1 if images differ
|
|
if exit, ok := err.(*exec.ExitError); ok && exit.Sys().(syscall.WaitStatus).ExitStatus() == 2 {
|
|
t.Logf("%#v", err)
|
|
t.Log(string(out))
|
|
t.Fatalf("error calling compare for %s (%s/%s): %s", dir, fileA, fileB, err)
|
|
}
|
|
}
|
|
|
|
diff, err := strconv.ParseInt(strings.TrimSpace(string(out)), 10, 32)
|
|
if err != nil {
|
|
t.Fatal("found no diff output: ", string(out))
|
|
}
|
|
if diff > expected {
|
|
t.Errorf("diff for %s and %s is too large (%d>%d), see %s", filepath.Join(dir, fileA), filepath.Join(dir, fileB), diff, expected, filepath.Join(dir, fileDiff))
|
|
}
|
|
}
|
|
|
|
func cpFile(dst, src string) error {
|
|
s, err := os.Open(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer s.Close()
|
|
d, err := os.Create(dst)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.Copy(d, s); err != nil {
|
|
d.Close()
|
|
return err
|
|
}
|
|
return d.Close()
|
|
}
|