go-hep.org/x/hep@v0.38.1/cmd/root2fits/main_test.go (about)

     1  // Copyright ©2020 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 main
     6  
     7  import (
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"path/filepath"
    12  	"reflect"
    13  	"strings"
    14  	"testing"
    15  
    16  	"codeberg.org/astrogo/fitsio"
    17  	"go-hep.org/x/hep/groot"
    18  	"go-hep.org/x/hep/groot/rtree"
    19  )
    20  
    21  func TestConvert(t *testing.T) {
    22  	tmp, err := os.MkdirTemp("", "root2fits-")
    23  	if err != nil {
    24  		t.Fatalf("%+v", err)
    25  	}
    26  	defer os.RemoveAll(tmp)
    27  
    28  	for _, tc := range []struct {
    29  		name  string
    30  		nevts int
    31  		data  func(i int) any
    32  		want  string
    33  	}{
    34  		{
    35  			name:  "builtins",
    36  			nevts: 5,
    37  			data: func(i int) any {
    38  				type D struct {
    39  					B   bool
    40  					I8  int8
    41  					I16 int16
    42  					I32 int32
    43  					I64 int64
    44  					U8  uint8
    45  					U16 uint16
    46  					U32 uint32
    47  					U64 uint64
    48  					F32 float32
    49  					F64 float64
    50  					Str string
    51  				}
    52  				return &D{
    53  					B:   i%2 == 0,
    54  					I8:  int8(-i),
    55  					I16: int16(-i),
    56  					I32: int32(-i),
    57  					I64: int64(-i),
    58  					U8:  uint8(i),
    59  					U16: uint16(i),
    60  					U32: uint32(i),
    61  					U64: uint64(i),
    62  					F32: float32(i),
    63  					F64: float64(i),
    64  					Str: fmt.Sprintf("%05d", i),
    65  				}
    66  			},
    67  			want: `== 00001/00005 =================================================================
    68  B          | true
    69  I8         | 0
    70  I16        | 0
    71  I32        | 0
    72  I64        | 0
    73  U8         | 0
    74  U16        | 0
    75  U32        | 0
    76  U64        | 0
    77  F32        | 0
    78  F64        | 0
    79  Str        | 00000
    80  == 00002/00005 =================================================================
    81  B          | false
    82  I8         | 255
    83  I16        | -1
    84  I32        | -1
    85  I64        | -1
    86  U8         | 1
    87  U16        | 1
    88  U32        | 1
    89  U64        | 1
    90  F32        | 1
    91  F64        | 1
    92  Str        | 00001
    93  == 00003/00005 =================================================================
    94  B          | true
    95  I8         | 254
    96  I16        | -2
    97  I32        | -2
    98  I64        | -2
    99  U8         | 2
   100  U16        | 2
   101  U32        | 2
   102  U64        | 2
   103  F32        | 2
   104  F64        | 2
   105  Str        | 00002
   106  == 00004/00005 =================================================================
   107  B          | false
   108  I8         | 253
   109  I16        | -3
   110  I32        | -3
   111  I64        | -3
   112  U8         | 3
   113  U16        | 3
   114  U32        | 3
   115  U64        | 3
   116  F32        | 3
   117  F64        | 3
   118  Str        | 00003
   119  == 00005/00005 =================================================================
   120  B          | true
   121  I8         | 252
   122  I16        | -4
   123  I32        | -4
   124  I64        | -4
   125  U8         | 4
   126  U16        | 4
   127  U32        | 4
   128  U64        | 4
   129  F32        | 4
   130  F64        | 4
   131  Str        | 00004
   132  `,
   133  		},
   134  		{
   135  			name:  "arrays",
   136  			nevts: 5,
   137  			data: func(i int) any {
   138  				type D struct {
   139  					B   [3]bool
   140  					I8  [3]int8
   141  					I16 [3]int16
   142  					I32 [3]int32
   143  					I64 [3]int64
   144  					U8  [3]uint8
   145  					U16 [3]uint16
   146  					U32 [3]uint32
   147  					U64 [3]uint64
   148  					F32 [3]float32
   149  					F64 [3]float64
   150  				}
   151  				return &D{
   152  					B:   [3]bool{i%2 == 0, (i+1)%2 == 0, (i+2)%2 == 0},
   153  					I8:  [3]int8{int8(-i), int8(-i - 1), int8(-i - 2)},
   154  					I16: [3]int16{int16(-i), int16(-i - 1), int16(-i - 2)},
   155  					I32: [3]int32{int32(-i), int32(-i - 1), int32(-i - 2)},
   156  					I64: [3]int64{int64(-i), int64(-i - 1), int64(-i - 2)},
   157  					U8:  [3]uint8{uint8(i), uint8(i + 1), uint8(i + 2)},
   158  					U16: [3]uint16{uint16(i), uint16(i + 1), uint16(i + 2)},
   159  					U32: [3]uint32{uint32(i), uint32(i + 1), uint32(i + 2)},
   160  					U64: [3]uint64{uint64(i), uint64(i + 1), uint64(i + 2)},
   161  					F32: [3]float32{float32(10 + i), float32(20 + i), float32(30 + i)},
   162  					F64: [3]float64{float64(10 + i), float64(20 + i), float64(30 + i)},
   163  				}
   164  			},
   165  			want: `== 00001/00005 =================================================================
   166  B          | [true false true]
   167  I8         | [0 255 254]
   168  I16        | [0 -1 -2]
   169  I32        | [0 -1 -2]
   170  I64        | [0 -1 -2]
   171  U8         | [0 1 2]
   172  U16        | [0 1 2]
   173  U32        | [0 1 2]
   174  U64        | [0 1 2]
   175  F32        | [10 20 30]
   176  F64        | [10 20 30]
   177  == 00002/00005 =================================================================
   178  B          | [false true false]
   179  I8         | [255 254 253]
   180  I16        | [-1 -2 -3]
   181  I32        | [-1 -2 -3]
   182  I64        | [-1 -2 -3]
   183  U8         | [1 2 3]
   184  U16        | [1 2 3]
   185  U32        | [1 2 3]
   186  U64        | [1 2 3]
   187  F32        | [11 21 31]
   188  F64        | [11 21 31]
   189  == 00003/00005 =================================================================
   190  B          | [true false true]
   191  I8         | [254 253 252]
   192  I16        | [-2 -3 -4]
   193  I32        | [-2 -3 -4]
   194  I64        | [-2 -3 -4]
   195  U8         | [2 3 4]
   196  U16        | [2 3 4]
   197  U32        | [2 3 4]
   198  U64        | [2 3 4]
   199  F32        | [12 22 32]
   200  F64        | [12 22 32]
   201  == 00004/00005 =================================================================
   202  B          | [false true false]
   203  I8         | [253 252 251]
   204  I16        | [-3 -4 -5]
   205  I32        | [-3 -4 -5]
   206  I64        | [-3 -4 -5]
   207  U8         | [3 4 5]
   208  U16        | [3 4 5]
   209  U32        | [3 4 5]
   210  U64        | [3 4 5]
   211  F32        | [13 23 33]
   212  F64        | [13 23 33]
   213  == 00005/00005 =================================================================
   214  B          | [true false true]
   215  I8         | [252 251 250]
   216  I16        | [-4 -5 -6]
   217  I32        | [-4 -5 -6]
   218  I64        | [-4 -5 -6]
   219  U8         | [4 5 6]
   220  U16        | [4 5 6]
   221  U32        | [4 5 6]
   222  U64        | [4 5 6]
   223  F32        | [14 24 34]
   224  F64        | [14 24 34]
   225  `,
   226  		},
   227  	} {
   228  		t.Run(tc.name, func(t *testing.T) {
   229  			var (
   230  				fname = filepath.Join(tmp, tc.name+".root")
   231  				tname = "tree"
   232  				oname = filepath.Join(tmp, tc.name+".fits")
   233  			)
   234  			// create
   235  			func() {
   236  				f, err := groot.Create(fname)
   237  				if err != nil {
   238  					t.Fatalf("could not create write ROOT file %q: %v", fname, err)
   239  				}
   240  				defer f.Close()
   241  
   242  				ptr := tc.data(0)
   243  				wvars := rtree.WriteVarsFromStruct(ptr)
   244  				tw, err := rtree.NewWriter(f, tname, wvars)
   245  				if err != nil {
   246  					t.Fatalf("could not create tree writer: %v", err)
   247  				}
   248  
   249  				for i := range int(tc.nevts) {
   250  					want := reflect.ValueOf(tc.data(i)).Elem().Interface()
   251  					for j, wvar := range wvars {
   252  						v := reflect.ValueOf(wvar.Value).Elem()
   253  						want := reflect.ValueOf(want).Field(j)
   254  						v.Set(want)
   255  					}
   256  					_, err = tw.Write()
   257  					if err != nil {
   258  						t.Fatalf("could not write event %d: %v", i, err)
   259  					}
   260  				}
   261  
   262  				err = tw.Close()
   263  				if err != nil {
   264  					t.Fatalf("could not close tree writer: %v", err)
   265  				}
   266  
   267  				err = f.Close()
   268  				if err != nil {
   269  					t.Fatalf("could not close write ROOT file %q: %v", fname, err)
   270  				}
   271  			}()
   272  
   273  			err := process(oname, tname, fname)
   274  			if err != nil {
   275  				t.Fatalf("could not convert ROOT tree to FITS table: %+v", err)
   276  			}
   277  
   278  			got := new(strings.Builder)
   279  			err = display(got, tname, oname)
   280  			if err != nil {
   281  				t.Fatalf("could not display FITS table content: %+v", err)
   282  			}
   283  
   284  			if got, want := got.String(), tc.want; got != want {
   285  				t.Fatalf("invalid FITS table content.\ngot:\n%s\nwant:\n%s\n", got, want)
   286  			}
   287  		})
   288  	}
   289  }
   290  
   291  func display(o io.Writer, hname, fname string) error {
   292  	r, err := os.Open(fname)
   293  	if err != nil {
   294  		return fmt.Errorf("could not open file %q: %w", fname, err)
   295  	}
   296  	defer r.Close()
   297  
   298  	f, err := fitsio.Open(r)
   299  	if err != nil {
   300  		return fmt.Errorf("could not open FITS file %q: %w", fname, err)
   301  	}
   302  	defer f.Close()
   303  
   304  	hdu := f.Get(hname)
   305  	if hdu.Type() == fitsio.IMAGE_HDU {
   306  		return fmt.Errorf("HDU %q not a table", hname)
   307  	}
   308  
   309  	table := hdu.(*fitsio.Table)
   310  	ncols := len(table.Cols())
   311  	nrows := table.NumRows()
   312  	rows, err := table.Read(0, nrows)
   313  	if err != nil {
   314  		return fmt.Errorf("could not read FITS table range: %w", err)
   315  	}
   316  	hdrline := strings.Repeat("=", 80-15)
   317  	maxname := 10
   318  	for _, col := range table.Cols() {
   319  		if len(col.Name) > maxname {
   320  			maxname = len(col.Name)
   321  		}
   322  	}
   323  
   324  	data := make([]any, ncols)
   325  	names := make([]string, ncols)
   326  	for i, col := range table.Cols() {
   327  		names[i] = col.Name
   328  		data[i] = reflect.New(col.Type()).Interface()
   329  	}
   330  
   331  	rowfmt := fmt.Sprintf("%%-%ds | %%v\n", maxname)
   332  	for irow := 0; rows.Next(); irow++ {
   333  		err = rows.Scan(data...)
   334  		if err != nil {
   335  			return fmt.Errorf("could not read row %d: %w", irow, err)
   336  		}
   337  		fmt.Fprintf(o, "== %05d/%05d %s\n", irow+1, nrows, hdrline)
   338  		for i := range ncols {
   339  			rv := reflect.Indirect(reflect.ValueOf(data[i]))
   340  			fmt.Fprintf(o, rowfmt, names[i], rv.Interface())
   341  		}
   342  	}
   343  
   344  	err = rows.Err()
   345  	if err != nil {
   346  		return fmt.Errorf("could not scan table: %w", err)
   347  	}
   348  
   349  	return nil
   350  }