go-hep.org/x/hep@v0.38.1/groot/rsrv/rsrv_test.go (about)

     1  // Copyright ©2018 The go-hep Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package rsrv
     6  
     7  import (
     8  	"bytes"
     9  	"encoding/base64"
    10  	"encoding/json"
    11  	"image/color"
    12  	"io"
    13  	"log"
    14  	"mime/multipart"
    15  	"net/http"
    16  	"net/http/httptest"
    17  	"os"
    18  	"path/filepath"
    19  	"reflect"
    20  	"runtime"
    21  	"sort"
    22  	"strings"
    23  	"testing"
    24  	"time"
    25  
    26  	uuid "github.com/hashicorp/go-uuid"
    27  	"go-hep.org/x/hep/groot/internal/rtests"
    28  	_ "go-hep.org/x/hep/groot/riofs/plugin/http"
    29  	_ "go-hep.org/x/hep/groot/riofs/plugin/xrootd"
    30  	"gonum.org/v1/plot/cmpimg"
    31  )
    32  
    33  var (
    34  	srv *Server
    35  )
    36  
    37  func TestMain(m *testing.M) {
    38  	dir, err := os.MkdirTemp("", "groot-rsrv-")
    39  	if err != nil {
    40  		log.Panicf("could not create temporary directory: %v", err)
    41  	}
    42  	defer os.RemoveAll(dir)
    43  
    44  	srv = New(dir)
    45  	setupCookie(srv)
    46  
    47  	os.Exit(m.Run())
    48  }
    49  
    50  func newTestServer() *httptest.Server {
    51  	mux := http.NewServeMux()
    52  	mux.HandleFunc("/open-file", srv.OpenFile)
    53  	mux.HandleFunc("/upload-file", srv.UploadFile)
    54  	mux.HandleFunc("/close-file", srv.CloseFile)
    55  	mux.HandleFunc("/list-files", srv.ListFiles)
    56  	mux.HandleFunc("/list-dirs", srv.Dirent)
    57  	mux.HandleFunc("/list-tree", srv.Tree)
    58  	mux.HandleFunc("/plot-h1", srv.PlotH1)
    59  	mux.HandleFunc("/plot-h2", srv.PlotH2)
    60  	mux.HandleFunc("/plot-s2", srv.PlotS2)
    61  	mux.HandleFunc("/plot-tree", srv.PlotTree)
    62  
    63  	return httptest.NewServer(mux)
    64  }
    65  
    66  func TestOpenFile(t *testing.T) {
    67  	ts := newTestServer()
    68  	defer ts.Close()
    69  
    70  	local, err := filepath.Abs("../testdata/simple.root")
    71  	if err != nil {
    72  		t.Fatalf("%+v", err)
    73  	}
    74  
    75  	for _, tc := range []struct {
    76  		uri    string
    77  		status int
    78  	}{
    79  		{"https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/simple.root", http.StatusOK},
    80  		{rtests.XrdRemote("testdata/simple.root"), http.StatusOK},
    81  		{"file://" + local, http.StatusOK},
    82  	} {
    83  		t.Run(tc.uri, func(t *testing.T) {
    84  			testOpenFile(t, ts, tc.uri, tc.status)
    85  			defer testCloseFile(t, ts, tc.uri)
    86  		})
    87  	}
    88  }
    89  
    90  func TestDoubleOpenFile(t *testing.T) {
    91  	ts := newTestServer()
    92  	defer ts.Close()
    93  
    94  	testOpenFile(t, ts, "https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/simple.root", 0)
    95  	testOpenFile(t, ts, "https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/simple.root", http.StatusConflict)
    96  }
    97  
    98  func testOpenFile(t *testing.T, ts *httptest.Server, uri string, status int) {
    99  	t.Helper()
   100  
   101  	req := OpenFileRequest{URI: uri}
   102  
   103  	body := new(bytes.Buffer)
   104  	err := json.NewEncoder(body).Encode(req)
   105  	if err != nil {
   106  		t.Fatalf("could not encode request: %v", err)
   107  	}
   108  
   109  	hreq, err := http.NewRequest(http.MethodPost, ts.URL+"/open-file", body)
   110  	if err != nil {
   111  		t.Fatalf("could not create http request: %v", err)
   112  	}
   113  	srv.addCookies(hreq)
   114  
   115  	hresp, err := ts.Client().Do(hreq)
   116  	if err != nil {
   117  		t.Fatalf("could not post http request: %v", err)
   118  	}
   119  	defer hresp.Body.Close()
   120  
   121  	if got, want := hresp.StatusCode, status; got != want && want != 0 {
   122  		t.Fatalf("invalid status code: got=%v, want=%v", got, want)
   123  	}
   124  }
   125  
   126  func TestUploadFile(t *testing.T) {
   127  	ts := newTestServer()
   128  	defer ts.Close()
   129  
   130  	local, err := filepath.Abs("../testdata/simple.root")
   131  	if err != nil {
   132  		t.Fatalf("%+v", err)
   133  	}
   134  
   135  	for _, tc := range []struct {
   136  		dst, src string
   137  		status   int
   138  	}{
   139  		{"foo.root", local, http.StatusOK},
   140  	} {
   141  		t.Run(tc.dst, func(t *testing.T) {
   142  			testUploadFile(t, ts, tc.dst, tc.src, tc.status)
   143  			defer testCloseFile(t, ts, tc.dst)
   144  		})
   145  	}
   146  }
   147  
   148  func testUploadFile(t *testing.T, ts *httptest.Server, dst, src string, status int) {
   149  	t.Helper()
   150  
   151  	body := new(bytes.Buffer)
   152  	mpart := multipart.NewWriter(body)
   153  	req, err := mpart.CreateFormField("groot-dst")
   154  	if err != nil {
   155  		t.Fatalf("could not create json-request form field: %v", err)
   156  	}
   157  	_, err = req.Write([]byte(dst))
   158  	if err != nil {
   159  		t.Fatalf("could not fill destination field: %v", err)
   160  	}
   161  
   162  	w, err := mpart.CreateFormFile("groot-file", src)
   163  	if err != nil {
   164  		t.Fatalf("could not create form-file: %v", err)
   165  	}
   166  	{
   167  		f, err := os.Open(src)
   168  		if err != nil {
   169  			t.Fatalf("%+v", err)
   170  		}
   171  		defer f.Close()
   172  
   173  		_, err = io.CopyBuffer(w, f, make([]byte, 16*1024*1024))
   174  		if err != nil {
   175  			t.Fatalf("could not copy file: %v", err)
   176  		}
   177  	}
   178  
   179  	if err := mpart.Close(); err != nil {
   180  		t.Fatalf("could not close multipart form data: %v", err)
   181  	}
   182  
   183  	hreq, err := http.NewRequest(http.MethodPost, ts.URL+"/upload-file", body)
   184  	if err != nil {
   185  		t.Fatalf("could not create http request: %v", err)
   186  	}
   187  	srv.addCookies(hreq)
   188  	hreq.Header.Set("Content-Type", mpart.FormDataContentType())
   189  
   190  	hresp, err := ts.Client().Do(hreq)
   191  	if err != nil {
   192  		t.Fatalf("could not post http request: %v", err)
   193  	}
   194  	defer hresp.Body.Close()
   195  
   196  	if got, want := hresp.StatusCode, status; got != want && want != 0 {
   197  		t.Fatalf("invalid status code: got=%v, want=%v", got, want)
   198  	}
   199  }
   200  
   201  func TestCloseFile(t *testing.T) {
   202  	ts := newTestServer()
   203  	defer ts.Close()
   204  
   205  	testOpenFile(t, ts, "https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/simple.root", 0)
   206  	testCloseFile(t, ts, "https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/simple.root")
   207  	testOpenFile(t, ts, "https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/simple.root", http.StatusOK)
   208  	testCloseFile(t, ts, "https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/simple.root")
   209  }
   210  
   211  func testCloseFile(t *testing.T, ts *httptest.Server, uri string) {
   212  	t.Helper()
   213  
   214  	req := CloseFileRequest{URI: uri}
   215  	body := new(bytes.Buffer)
   216  	err := json.NewEncoder(body).Encode(req)
   217  	if err != nil {
   218  		t.Fatalf("could not encode request: %v", err)
   219  	}
   220  
   221  	hreq, err := http.NewRequest(http.MethodPost, ts.URL+"/close-file", body)
   222  	if err != nil {
   223  		t.Fatalf("could not create http request: %v", err)
   224  	}
   225  	srv.addCookies(hreq)
   226  
   227  	hresp, err := ts.Client().Do(hreq)
   228  	if err != nil {
   229  		t.Fatalf("could not post http request: %v", err)
   230  	}
   231  	defer hresp.Body.Close()
   232  
   233  	if hresp.StatusCode != http.StatusOK {
   234  		t.Fatalf("could not close file %q: %v", uri, hresp.StatusCode)
   235  	}
   236  }
   237  
   238  func TestListFiles(t *testing.T) {
   239  	ts := newTestServer()
   240  	defer ts.Close()
   241  
   242  	testOpenFile(t, ts, "https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/simple.root", 0)
   243  	testOpenFile(t, ts, "https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/dirs-6.14.00.root", http.StatusOK)
   244  	testListFiles(t, ts, []File{
   245  		{"https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/simple.root", 60600},
   246  		{"https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/dirs-6.14.00.root", 61400},
   247  	})
   248  	testCloseFile(t, ts, "https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/simple.root")
   249  	testCloseFile(t, ts, "https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/dirs-6.14.00.root")
   250  }
   251  
   252  func testListFiles(t *testing.T, ts *httptest.Server, want []File) {
   253  	t.Helper()
   254  
   255  	hreq, err := http.NewRequest(http.MethodPost, ts.URL+"/list-files", nil)
   256  	if err != nil {
   257  		t.Fatalf("could not create http request: %v", err)
   258  	}
   259  	srv.addCookies(hreq)
   260  
   261  	hresp, err := ts.Client().Do(hreq)
   262  	if err != nil {
   263  		t.Fatalf("could not post http request: %v", err)
   264  	}
   265  	defer hresp.Body.Close()
   266  
   267  	if hresp.StatusCode != http.StatusOK {
   268  		t.Fatalf("could not list files: %v", hresp.StatusCode)
   269  	}
   270  
   271  	var resp ListResponse
   272  	err = json.NewDecoder(hresp.Body).Decode(&resp)
   273  	if err != nil {
   274  		t.Fatalf("could not decode response: %v", err)
   275  	}
   276  
   277  	got := resp.Files
   278  	sort.Slice(got, func(i, j int) bool {
   279  		return got[i].URI < got[j].URI
   280  	})
   281  	sort.Slice(want, func(i, j int) bool {
   282  		return want[i].URI < want[j].URI
   283  	})
   284  
   285  	if !reflect.DeepEqual(got, want) {
   286  		t.Fatalf("invalid ls content:\ngot= %v\nwant=%v\n", got, want)
   287  	}
   288  }
   289  
   290  func TestDirent(t *testing.T) {
   291  	ts := newTestServer()
   292  	defer ts.Close()
   293  
   294  	testOpenFile(t, ts, "https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/simple.root", http.StatusOK)
   295  	defer testCloseFile(t, ts, "https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/simple.root")
   296  
   297  	testDirent(t, ts, DirentRequest{
   298  		URI:       "https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/simple.root",
   299  		Dir:       "/",
   300  		Recursive: false,
   301  	}, []string{
   302  		"/",
   303  		"/tree",
   304  	})
   305  
   306  	testOpenFile(t, ts, "https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/dirs-6.14.00.root", http.StatusOK)
   307  	defer testCloseFile(t, ts, "https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/dirs-6.14.00.root")
   308  
   309  	testDirent(t, ts, DirentRequest{
   310  		URI:       "https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/dirs-6.14.00.root",
   311  		Dir:       "/",
   312  		Recursive: false,
   313  	}, []string{
   314  		"/",
   315  		"/dir1",
   316  		"/dir2",
   317  		"/dir3",
   318  	})
   319  	testDirent(t, ts, DirentRequest{
   320  		URI:       "https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/dirs-6.14.00.root",
   321  		Dir:       "/",
   322  		Recursive: true,
   323  	}, []string{
   324  		"/",
   325  		"/dir1",
   326  		"/dir1/dir11",
   327  		"/dir1/dir11/h1",
   328  		"/dir2",
   329  		"/dir3",
   330  	})
   331  	testDirent(t, ts, DirentRequest{
   332  		URI:       "https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/dirs-6.14.00.root",
   333  		Dir:       "/dir1",
   334  		Recursive: false,
   335  	}, []string{
   336  		"/dir1",
   337  		"/dir1/dir11",
   338  	})
   339  	testDirent(t, ts, DirentRequest{
   340  		URI:       "https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/dirs-6.14.00.root",
   341  		Dir:       "/dir1",
   342  		Recursive: true,
   343  	}, []string{
   344  		"/dir1",
   345  		"/dir1/dir11",
   346  		"/dir1/dir11/h1",
   347  	})
   348  }
   349  
   350  func testDirent(t *testing.T, ts *httptest.Server, req DirentRequest, content []string) {
   351  	t.Helper()
   352  
   353  	body := new(bytes.Buffer)
   354  	err := json.NewEncoder(body).Encode(req)
   355  	if err != nil {
   356  		t.Fatalf("could not encode request: %v", err)
   357  	}
   358  
   359  	hreq, err := http.NewRequest(http.MethodPost, ts.URL+"/list-dirs", body)
   360  	if err != nil {
   361  		t.Fatalf("could not create http request: %v", err)
   362  	}
   363  	srv.addCookies(hreq)
   364  
   365  	hresp, err := ts.Client().Do(hreq)
   366  	if err != nil {
   367  		t.Fatalf("could not post http request: %v", err)
   368  	}
   369  	defer hresp.Body.Close()
   370  
   371  	if hresp.StatusCode != http.StatusOK {
   372  		t.Fatalf("could not list dirs: %v", hresp.StatusCode)
   373  	}
   374  
   375  	var resp DirentResponse
   376  	err = json.NewDecoder(hresp.Body).Decode(&resp)
   377  	if err != nil {
   378  		t.Fatalf("could not decode response: %v", err)
   379  	}
   380  
   381  	var got []string
   382  	for _, f := range resp.Content {
   383  		got = append(got, f.Path)
   384  	}
   385  
   386  	sort.Strings(got)
   387  	sort.Strings(content)
   388  
   389  	if !reflect.DeepEqual(got, content) {
   390  		t.Fatalf("invalid dirent content: (req=%#v)\ngot= %v\nwant=%v\n", req, got, content)
   391  	}
   392  }
   393  
   394  func TestTree(t *testing.T) {
   395  	ts := newTestServer()
   396  	defer ts.Close()
   397  
   398  	const uri = "https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/small-flat-tree.root"
   399  	testOpenFile(t, ts, uri, http.StatusOK)
   400  	defer testCloseFile(t, ts, uri)
   401  
   402  	for _, tc := range []struct {
   403  		req  TreeRequest
   404  		want Tree
   405  	}{
   406  		{
   407  			req: TreeRequest{
   408  				URI: uri,
   409  				Obj: "tree",
   410  			},
   411  			want: Tree{
   412  				Type:    "TTree",
   413  				Name:    "tree",
   414  				Title:   "my tree title",
   415  				Entries: 100,
   416  				Branches: []Branch{
   417  					{Type: "TBranch", Name: "Int32", Leaves: []Leaf{{Type: "int32", Name: "Int32"}}},
   418  					{Type: "TBranch", Name: "Int64", Leaves: []Leaf{{Type: "int64", Name: "Int64"}}},
   419  					{Type: "TBranch", Name: "UInt32", Leaves: []Leaf{{Type: "uint32", Name: "UInt32"}}},
   420  					{Type: "TBranch", Name: "UInt64", Leaves: []Leaf{{Type: "uint64", Name: "UInt64"}}},
   421  					{Type: "TBranch", Name: "Float32", Leaves: []Leaf{{Type: "float32", Name: "Float32"}}},
   422  					{Type: "TBranch", Name: "Float64", Leaves: []Leaf{{Type: "float64", Name: "Float64"}}},
   423  					{Type: "TBranch", Name: "Str", Leaves: []Leaf{{Type: "string", Name: "Str"}}},
   424  					{Type: "TBranch", Name: "ArrayInt32", Leaves: []Leaf{{Type: "int32", Name: "ArrayInt32"}}},
   425  					{Type: "TBranch", Name: "ArrayInt64", Leaves: []Leaf{{Type: "int64", Name: "ArrayInt64"}}},
   426  					{Type: "TBranch", Name: "ArrayUInt32", Leaves: []Leaf{{Type: "uint32", Name: "ArrayInt32"}}}, // FIXME(sbinet): ref-file had a typo (should be ArrayUInt32)
   427  					{Type: "TBranch", Name: "ArrayUInt64", Leaves: []Leaf{{Type: "uint64", Name: "ArrayInt64"}}}, // FIXME(sbinet): ref-file had a typo (should be ArrayUInt64)
   428  					{Type: "TBranch", Name: "ArrayFloat32", Leaves: []Leaf{{Type: "float32", Name: "ArrayFloat32"}}},
   429  					{Type: "TBranch", Name: "ArrayFloat64", Leaves: []Leaf{{Type: "float64", Name: "ArrayFloat64"}}},
   430  					{Type: "TBranch", Name: "N", Leaves: []Leaf{{Type: "int32", Name: "N"}}},
   431  					{Type: "TBranch", Name: "SliceInt32", Leaves: []Leaf{{Type: "int32", Name: "SliceInt32"}}},
   432  					{Type: "TBranch", Name: "SliceInt64", Leaves: []Leaf{{Type: "int64", Name: "SliceInt64"}}},
   433  					{Type: "TBranch", Name: "SliceUInt32", Leaves: []Leaf{{Type: "uint32", Name: "SliceInt32"}}}, // FIXME(sbinet): ref-file had a typo (should be SliceUInt32)
   434  					{Type: "TBranch", Name: "SliceUInt64", Leaves: []Leaf{{Type: "uint64", Name: "SliceInt64"}}}, // FIXME(sbinet): ref-file had a typo (should be SliceUInt64)
   435  					{Type: "TBranch", Name: "SliceFloat32", Leaves: []Leaf{{Type: "float32", Name: "SliceFloat32"}}},
   436  					{Type: "TBranch", Name: "SliceFloat64", Leaves: []Leaf{{Type: "float64", Name: "SliceFloat64"}}},
   437  				},
   438  				Leaves: []Leaf{
   439  					{Type: "int32", Name: "Int32"},
   440  					{Type: "int64", Name: "Int64"},
   441  					{Type: "uint32", Name: "UInt32"},
   442  					{Type: "uint64", Name: "UInt64"},
   443  					{Type: "float32", Name: "Float32"},
   444  					{Type: "float64", Name: "Float64"},
   445  					{Type: "string", Name: "Str"},
   446  					{Type: "int32", Name: "ArrayInt32"},
   447  					{Type: "int64", Name: "ArrayInt64"},
   448  					{Type: "uint32", Name: "ArrayInt32"}, // FIXME(sbinet): ref-file had a typo (should be ArrayUInt32)
   449  					{Type: "uint64", Name: "ArrayInt64"}, // FIXME(sbinet): ref-file had a typo (should be ArrayUInt64)
   450  					{Type: "float32", Name: "ArrayFloat32"},
   451  					{Type: "float64", Name: "ArrayFloat64"},
   452  					{Type: "int32", Name: "N"},
   453  					{Type: "int32", Name: "SliceInt32"},
   454  					{Type: "int64", Name: "SliceInt64"},
   455  					{Type: "uint32", Name: "SliceInt32"}, // FIXME(sbinet): ref-file had a typo (should be SliceUInt32)
   456  					{Type: "uint64", Name: "SliceInt64"}, // FIXME(sbinet): ref-file had a typo (should be SliceUInt64)
   457  					{Type: "float32", Name: "SliceFloat32"},
   458  					{Type: "float64", Name: "SliceFloat64"},
   459  				},
   460  			},
   461  		},
   462  	} {
   463  		t.Run(tc.want.Name, func(t *testing.T) {
   464  			var resp TreeResponse
   465  			testTree(t, ts, tc.req, &resp)
   466  
   467  			if !reflect.DeepEqual(resp.Tree, tc.want) {
   468  				t.Fatalf("invalid tree:\ngot= %#v\nwant=%#v", resp.Tree, tc.want)
   469  			}
   470  		})
   471  	}
   472  }
   473  
   474  func testTree(t *testing.T, ts *httptest.Server, req TreeRequest, resp *TreeResponse) {
   475  	t.Helper()
   476  
   477  	body := new(bytes.Buffer)
   478  	err := json.NewEncoder(body).Encode(req)
   479  	if err != nil {
   480  		t.Fatalf("could not encode request: %v", err)
   481  	}
   482  
   483  	hreq, err := http.NewRequest(http.MethodPost, ts.URL+"/list-tree", body)
   484  	if err != nil {
   485  		t.Fatalf("could not create http request: %v", err)
   486  	}
   487  	srv.addCookies(hreq)
   488  
   489  	hresp, err := ts.Client().Do(hreq)
   490  	if err != nil {
   491  		t.Fatalf("could not post http request: %v", err)
   492  	}
   493  	defer hresp.Body.Close()
   494  
   495  	if hresp.StatusCode != http.StatusOK {
   496  		t.Fatalf("could not plot h1: %v", hresp.StatusCode)
   497  	}
   498  
   499  	err = json.NewDecoder(hresp.Body).Decode(resp)
   500  	if err != nil {
   501  		t.Fatalf("could not decode response: %v", err)
   502  	}
   503  }
   504  
   505  func TestPlotH1(t *testing.T) {
   506  	ts := newTestServer()
   507  	defer ts.Close()
   508  
   509  	testOpenFile(t, ts, "https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/dirs-6.14.00.root", http.StatusOK)
   510  	defer testCloseFile(t, ts, "https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/dirs-6.14.00.root")
   511  
   512  	const uri = "https://codeberg.org/go-hep/hep/raw/branch/main/hbook/rootcnv/testdata/gauss-h1.root"
   513  	testOpenFile(t, ts, uri, http.StatusOK)
   514  	defer testCloseFile(t, ts, uri)
   515  
   516  	for _, tc := range []struct {
   517  		req  PlotH1Request
   518  		want string
   519  	}{
   520  		{
   521  			req: PlotH1Request{
   522  				URI: uri,
   523  				Obj: "h1f",
   524  			},
   525  			want: "testdata/h1f_golden.png",
   526  		},
   527  		{
   528  			req: PlotH1Request{
   529  				URI: "https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/dirs-6.14.00.root",
   530  				Dir: "/dir1/dir11",
   531  				Obj: "h1",
   532  			},
   533  			want: "testdata/h1_golden.png",
   534  		},
   535  		{
   536  			req: PlotH1Request{
   537  				URI: uri,
   538  				Obj: "h1f",
   539  				Options: PlotOptions{
   540  					Type:      "png",
   541  					Title:     "My Title",
   542  					X:         "X axis [GeV]",
   543  					Y:         "Y axis [A.U]",
   544  					FillColor: color.RGBA{0, 0, 255, 255},
   545  					Line: LineStyle{
   546  						Color: color.Black,
   547  					},
   548  				},
   549  			},
   550  			want: "testdata/h1f_options_golden.png",
   551  		},
   552  		{
   553  			req: PlotH1Request{
   554  				URI: "https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/dirs-6.14.00.root",
   555  				Dir: "/dir1/dir11",
   556  				Obj: "h1",
   557  				Options: PlotOptions{
   558  					Type: "pdf",
   559  				},
   560  			},
   561  			want: "testdata/h1_golden.pdf",
   562  		},
   563  		{
   564  			req: PlotH1Request{
   565  				URI: "https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/dirs-6.14.00.root",
   566  				Dir: "/dir1/dir11",
   567  				Obj: "h1",
   568  				Options: PlotOptions{
   569  					Type: "svg",
   570  				},
   571  			},
   572  			want: "testdata/h1_golden.svg",
   573  		},
   574  	} {
   575  		t.Run(tc.want, func(t *testing.T) {
   576  			var resp PlotResponse
   577  			testPlotH1(t, ts, tc.req, &resp)
   578  
   579  			raw, err := base64.StdEncoding.DecodeString(resp.Data)
   580  			if err != nil {
   581  				t.Fatal(err)
   582  			}
   583  
   584  			if *cmpimg.GenerateTestData {
   585  				_ = os.WriteFile(tc.want, raw, 0644)
   586  			}
   587  
   588  			want, err := os.ReadFile(tc.want)
   589  			if err != nil {
   590  				t.Fatal(err)
   591  			}
   592  
   593  			typ := tc.req.Options.Type
   594  			if typ == "" {
   595  				typ = "png"
   596  			}
   597  			if ok, err := cmpimg.EqualApprox(typ, raw, want, 0.1); !ok || err != nil {
   598  				_ = os.WriteFile(strings.Replace(tc.want, "_golden", "", -1), raw, 0644)
   599  				fatalf := t.Fatalf
   600  				if runtime.GOOS == "darwin" {
   601  					// ignore errors for darwin and mac-silicon
   602  					fatalf = t.Logf
   603  				}
   604  				fatalf("reference files differ: err=%v ok=%v", err, ok)
   605  			}
   606  		})
   607  	}
   608  }
   609  
   610  func testPlotH1(t *testing.T, ts *httptest.Server, req PlotH1Request, resp *PlotResponse) {
   611  	t.Helper()
   612  
   613  	body := new(bytes.Buffer)
   614  	err := json.NewEncoder(body).Encode(req)
   615  	if err != nil {
   616  		t.Fatalf("could not encode request: %v", err)
   617  	}
   618  
   619  	hreq, err := http.NewRequest(http.MethodPost, ts.URL+"/plot-h1", body)
   620  	if err != nil {
   621  		t.Fatalf("could not create http request: %v", err)
   622  	}
   623  	srv.addCookies(hreq)
   624  
   625  	hresp, err := ts.Client().Do(hreq)
   626  	if err != nil {
   627  		t.Fatalf("could not post http request: %v", err)
   628  	}
   629  	defer hresp.Body.Close()
   630  
   631  	if hresp.StatusCode != http.StatusOK {
   632  		t.Fatalf("could not plot h1: %v", hresp.StatusCode)
   633  	}
   634  
   635  	err = json.NewDecoder(hresp.Body).Decode(resp)
   636  	if err != nil {
   637  		t.Fatalf("could not decode response: %v", err)
   638  	}
   639  }
   640  
   641  func TestPlotH2(t *testing.T) {
   642  	ts := newTestServer()
   643  	defer ts.Close()
   644  
   645  	const uri = "https://codeberg.org/go-hep/hep/raw/branch/main/hbook/rootcnv/testdata/gauss-h2.root"
   646  	testOpenFile(t, ts, uri, http.StatusOK)
   647  	defer testCloseFile(t, ts, uri)
   648  
   649  	for _, tc := range []struct {
   650  		req  PlotH2Request
   651  		want string
   652  	}{
   653  		{
   654  			req: PlotH2Request{
   655  				URI: uri,
   656  				Obj: "h2f",
   657  			},
   658  			want: "testdata/h2f_golden.png",
   659  		},
   660  		{
   661  			req: PlotH2Request{
   662  				URI: uri,
   663  				Dir: "/",
   664  				Obj: "h2d",
   665  				Options: PlotOptions{
   666  					Type: "png",
   667  				},
   668  			},
   669  			want: "testdata/h2d_golden.png",
   670  		},
   671  		{
   672  			req: PlotH2Request{
   673  				URI: uri,
   674  				Dir: "/",
   675  				Obj: "h2d",
   676  				Options: PlotOptions{
   677  					Type:  "png",
   678  					Title: "My Title",
   679  					X:     "X-axis [GeV]",
   680  					Y:     "Y-axis [GeV]",
   681  				},
   682  			},
   683  			want: "testdata/h2d_options_golden.png",
   684  		},
   685  		{
   686  			req: PlotH2Request{
   687  				URI: uri,
   688  				Dir: "/",
   689  				Obj: "h2d",
   690  				Options: PlotOptions{
   691  					Type: "pdf",
   692  				},
   693  			},
   694  			want: "testdata/h2d_golden.pdf",
   695  		},
   696  		{
   697  			req: PlotH2Request{
   698  				URI: uri,
   699  				Dir: "/",
   700  				Obj: "h2d",
   701  				Options: PlotOptions{
   702  					Type: "svg",
   703  				},
   704  			},
   705  			want: "testdata/h2d_golden.svg",
   706  		},
   707  	} {
   708  		t.Run(tc.want, func(t *testing.T) {
   709  			var resp PlotResponse
   710  			testPlotH2(t, ts, tc.req, &resp)
   711  
   712  			raw, err := base64.StdEncoding.DecodeString(resp.Data)
   713  			if err != nil {
   714  				t.Fatal(err)
   715  			}
   716  
   717  			if *cmpimg.GenerateTestData {
   718  				_ = os.WriteFile(tc.want, raw, 0644)
   719  			}
   720  
   721  			want, err := os.ReadFile(tc.want)
   722  			if err != nil {
   723  				t.Fatal(err)
   724  			}
   725  
   726  			typ := tc.req.Options.Type
   727  			if typ == "" {
   728  				typ = "png"
   729  			}
   730  			if ok, err := cmpimg.EqualApprox(typ, raw, want, 0.1); !ok || err != nil {
   731  				_ = os.WriteFile(strings.Replace(tc.want, "_golden", "", -1), raw, 0644)
   732  				fatalf := t.Fatalf
   733  				if runtime.GOOS == "darwin" {
   734  					// ignore errors for darwin and mac-silicon
   735  					fatalf = t.Logf
   736  				}
   737  				fatalf("reference files differ: err=%v ok=%v", err, ok)
   738  			}
   739  		})
   740  	}
   741  }
   742  
   743  func testPlotH2(t *testing.T, ts *httptest.Server, req PlotH2Request, resp *PlotResponse) {
   744  	t.Helper()
   745  
   746  	body := new(bytes.Buffer)
   747  	err := json.NewEncoder(body).Encode(req)
   748  	if err != nil {
   749  		t.Fatalf("could not encode request: %v", err)
   750  	}
   751  
   752  	hreq, err := http.NewRequest(http.MethodPost, ts.URL+"/plot-h2", body)
   753  	if err != nil {
   754  		t.Fatalf("could not create http request: %v", err)
   755  	}
   756  	srv.addCookies(hreq)
   757  
   758  	hresp, err := ts.Client().Do(hreq)
   759  	if err != nil {
   760  		t.Fatalf("could not post http request: %v", err)
   761  	}
   762  	defer hresp.Body.Close()
   763  
   764  	if hresp.StatusCode != http.StatusOK {
   765  		t.Fatalf("could not plot h1: %v", hresp.StatusCode)
   766  	}
   767  
   768  	err = json.NewDecoder(hresp.Body).Decode(resp)
   769  	if err != nil {
   770  		t.Fatalf("could not decode response: %v", err)
   771  	}
   772  }
   773  
   774  func TestPlotS2(t *testing.T) {
   775  	ts := newTestServer()
   776  	defer ts.Close()
   777  
   778  	const uri = "https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/graphs.root"
   779  	testOpenFile(t, ts, uri, http.StatusOK)
   780  	defer testCloseFile(t, ts, uri)
   781  
   782  	for _, tc := range []struct {
   783  		req  PlotS2Request
   784  		want string
   785  	}{
   786  		{
   787  			req: PlotS2Request{
   788  				URI: uri,
   789  				Obj: "tg",
   790  			},
   791  			want: "testdata/tg_golden.png",
   792  		},
   793  		{
   794  			req: PlotS2Request{
   795  				URI: uri,
   796  				Dir: "/",
   797  				Obj: "tge",
   798  				Options: PlotOptions{
   799  					Type: "png",
   800  				},
   801  			},
   802  			want: "testdata/tge_golden.png",
   803  		},
   804  		{
   805  			req: PlotS2Request{
   806  				URI: uri,
   807  				Dir: "/",
   808  				Obj: "tgae",
   809  				Options: PlotOptions{
   810  					Type:  "png",
   811  					Title: "My Title",
   812  					X:     "X-axis [GeV]",
   813  					Y:     "Y-axis [GeV]",
   814  					Line: LineStyle{
   815  						Color: color.RGBA{B: 255, A: 255},
   816  					},
   817  				},
   818  			},
   819  			want: "testdata/tgae_options_golden.png",
   820  		},
   821  		{
   822  			req: PlotS2Request{
   823  				URI: uri,
   824  				Dir: "/",
   825  				Obj: "tgae",
   826  				Options: PlotOptions{
   827  					Type: "pdf",
   828  				},
   829  			},
   830  			want: "testdata/tgae_golden.pdf",
   831  		},
   832  		{
   833  			req: PlotS2Request{
   834  				URI: uri,
   835  				Dir: "/",
   836  				Obj: "tgae",
   837  				Options: PlotOptions{
   838  					Type: "svg",
   839  				},
   840  			},
   841  			want: "testdata/tgae_golden.svg",
   842  		},
   843  	} {
   844  		t.Run(tc.want, func(t *testing.T) {
   845  			var resp PlotResponse
   846  			testPlotS2(t, ts, tc.req, &resp)
   847  
   848  			raw, err := base64.StdEncoding.DecodeString(resp.Data)
   849  			if err != nil {
   850  				t.Fatal(err)
   851  			}
   852  
   853  			if *cmpimg.GenerateTestData {
   854  				_ = os.WriteFile(tc.want, raw, 0644)
   855  			}
   856  
   857  			want, err := os.ReadFile(tc.want)
   858  			if err != nil {
   859  				t.Fatal(err)
   860  			}
   861  
   862  			typ := tc.req.Options.Type
   863  			if typ == "" {
   864  				typ = "png"
   865  			}
   866  			if ok, err := cmpimg.EqualApprox(typ, raw, want, 0.1); !ok || err != nil {
   867  				_ = os.WriteFile(strings.Replace(tc.want, "_golden", "", -1), raw, 0644)
   868  				fatalf := t.Fatalf
   869  				if runtime.GOOS == "darwin" {
   870  					// ignore errors for darwin and mac-silicon
   871  					fatalf = t.Logf
   872  				}
   873  				fatalf("reference files differ: err=%v ok=%v", err, ok)
   874  			}
   875  		})
   876  	}
   877  }
   878  
   879  func testPlotS2(t *testing.T, ts *httptest.Server, req PlotS2Request, resp *PlotResponse) {
   880  	t.Helper()
   881  
   882  	body := new(bytes.Buffer)
   883  	err := json.NewEncoder(body).Encode(req)
   884  	if err != nil {
   885  		t.Fatalf("could not encode request: %v", err)
   886  	}
   887  
   888  	hreq, err := http.NewRequest(http.MethodPost, ts.URL+"/plot-s2", body)
   889  	if err != nil {
   890  		t.Fatalf("could not create http request: %v", err)
   891  	}
   892  	srv.addCookies(hreq)
   893  
   894  	hresp, err := ts.Client().Do(hreq)
   895  	if err != nil {
   896  		t.Fatalf("could not post http request: %v", err)
   897  	}
   898  	defer hresp.Body.Close()
   899  
   900  	if hresp.StatusCode != http.StatusOK {
   901  		t.Fatalf("could not plot h1: %v", hresp.StatusCode)
   902  	}
   903  
   904  	err = json.NewDecoder(hresp.Body).Decode(resp)
   905  	if err != nil {
   906  		t.Fatalf("could not decode response: %v", err)
   907  	}
   908  }
   909  
   910  func TestPlotTree(t *testing.T) {
   911  	ts := newTestServer()
   912  	defer ts.Close()
   913  
   914  	const uri = "https://codeberg.org/go-hep/hep/raw/branch/main/groot/testdata/small-flat-tree.root"
   915  	testOpenFile(t, ts, uri, http.StatusOK)
   916  	defer testCloseFile(t, ts, uri)
   917  
   918  	for _, tc := range []struct {
   919  		req  PlotTreeRequest
   920  		want string
   921  	}{
   922  		{
   923  			req: PlotTreeRequest{
   924  				URI:  uri,
   925  				Obj:  "tree",
   926  				Vars: []string{"Int32"},
   927  			},
   928  			want: "testdata/tree_i32_golden.png",
   929  		},
   930  		{
   931  			req: PlotTreeRequest{
   932  				URI:  uri,
   933  				Dir:  "/",
   934  				Obj:  "tree",
   935  				Vars: []string{"Float64"},
   936  			},
   937  			want: "testdata/tree_f64_golden.png",
   938  		},
   939  		{
   940  			req: PlotTreeRequest{
   941  				URI:  uri,
   942  				Dir:  "/",
   943  				Obj:  "tree",
   944  				Vars: []string{"ArrayFloat64"},
   945  			},
   946  			want: "testdata/tree_array_f64_golden.png",
   947  		},
   948  		{
   949  			req: PlotTreeRequest{
   950  				URI:  uri,
   951  				Dir:  "/",
   952  				Obj:  "tree",
   953  				Vars: []string{"SliceFloat64"},
   954  			},
   955  			want: "testdata/tree_slice_f64_golden.png",
   956  		},
   957  	} {
   958  		t.Run(tc.want, func(t *testing.T) {
   959  			var resp PlotResponse
   960  			testPlotTree(t, ts, tc.req, &resp)
   961  
   962  			raw, err := base64.StdEncoding.DecodeString(resp.Data)
   963  			if err != nil {
   964  				t.Fatal(err)
   965  			}
   966  
   967  			if *cmpimg.GenerateTestData {
   968  				_ = os.WriteFile(tc.want, raw, 0644)
   969  			}
   970  
   971  			want, err := os.ReadFile(tc.want)
   972  			if err != nil {
   973  				t.Fatal(err)
   974  			}
   975  
   976  			typ := tc.req.Options.Type
   977  			if typ == "" {
   978  				typ = "png"
   979  			}
   980  			if ok, err := cmpimg.EqualApprox(typ, raw, want, 0.1); !ok || err != nil {
   981  				_ = os.WriteFile(strings.Replace(tc.want, "_golden", "", -1), raw, 0644)
   982  				fatalf := t.Fatalf
   983  				if runtime.GOOS == "darwin" {
   984  					// ignore errors for darwin and mac-silicon
   985  					fatalf = t.Logf
   986  				}
   987  				fatalf("reference files differ: err=%v ok=%v", err, ok)
   988  			}
   989  		})
   990  	}
   991  }
   992  
   993  func testPlotTree(t *testing.T, ts *httptest.Server, req PlotTreeRequest, resp *PlotResponse) {
   994  	t.Helper()
   995  
   996  	body := new(bytes.Buffer)
   997  	err := json.NewEncoder(body).Encode(req)
   998  	if err != nil {
   999  		t.Fatalf("could not encode request: %v", err)
  1000  	}
  1001  
  1002  	hreq, err := http.NewRequest(http.MethodPost, ts.URL+"/plot-tree", body)
  1003  	if err != nil {
  1004  		t.Fatalf("could not create http request: %v", err)
  1005  	}
  1006  	srv.addCookies(hreq)
  1007  
  1008  	hresp, err := ts.Client().Do(hreq)
  1009  	if err != nil {
  1010  		t.Fatalf("could not post http request: %v", err)
  1011  	}
  1012  	defer hresp.Body.Close()
  1013  
  1014  	if hresp.StatusCode != http.StatusOK {
  1015  		t.Fatalf("could not plot h1: %v", hresp.StatusCode)
  1016  	}
  1017  
  1018  	err = json.NewDecoder(hresp.Body).Decode(resp)
  1019  	if err != nil {
  1020  		t.Fatalf("could not decode response: %v", err)
  1021  	}
  1022  }
  1023  
  1024  func (srv *Server) addCookies(req *http.Request) {
  1025  	for _, cookie := range srv.cookies {
  1026  		req.AddCookie(cookie)
  1027  	}
  1028  }
  1029  
  1030  func setupCookie(srv *Server) {
  1031  	v, err := uuid.GenerateUUID()
  1032  	if err != nil {
  1033  		panic(err)
  1034  	}
  1035  	cookie := &http.Cookie{
  1036  		Name:    cookieName,
  1037  		Value:   v,
  1038  		Expires: time.Now().Add(24 * time.Hour),
  1039  	}
  1040  	srv.mu.Lock()
  1041  	defer srv.mu.Unlock()
  1042  	srv.sessions[cookie.Value] = NewDB(filepath.Join(srv.dir, cookie.Value))
  1043  	srv.cookies[cookie.Value] = cookie
  1044  }