lab.nexedi.com/kirr/go123@v0.0.0-20240207185015-8299741fa871/xfmt/fmt_test.go (about)

     1  // Copyright (C) 2017  Nexedi SA and Contributors.
     2  //                     Kirill Smelkov <kirr@nexedi.com>
     3  //
     4  // This program is free software: you can Use, Study, Modify and Redistribute
     5  // it under the terms of the GNU General Public License version 3, or (at your
     6  // option) any later version, as published by the Free Software Foundation.
     7  //
     8  // You can also Link and Combine this program with other software covered by
     9  // the terms of any of the Free Software licenses or any of the Open Source
    10  // Initiative approved licenses and Convey the resulting work. Corresponding
    11  // source of such a combination shall include the source code for all other
    12  // software used.
    13  //
    14  // This program is distributed WITHOUT ANY WARRANTY; without even the implied
    15  // warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
    16  //
    17  // See COPYING file for full licensing terms.
    18  // See https://www.nexedi.com/licensing for rationale and options.
    19  
    20  package xfmt
    21  
    22  import (
    23  	"fmt"
    24  	"reflect"
    25  	"testing"
    26  )
    27  
    28  var testv = []struct {format, xformatMeth string; value interface{}} {
    29  	{"%c",		"Cb",		byte('A')},
    30  	{"%c",		"C",		rune(-1)},
    31  	{"%c",		"C",		'B'},		// 1-byte encoded
    32  	{"%c",		"C",		'и'},		// 2-bytes encoded
    33  	{"%c",		"C",		'\u20ac'},	// 3-bytes encoded
    34  	{"%c",		"C",		'\U00010001'},	// 4-bytes encoded
    35  
    36  	{"%s",		"S",		"hello"},
    37  	{"%s",		"Sb",		[]byte("world")},
    38  	{"%q",		"Q",		"alpha"},
    39  	{"%q",		"Qb",		[]byte("beta")},
    40  	{"%q",		"Qcb",		byte('D')},
    41  	{"%q",		"Qc",		'B'},		// 1-byte encoded
    42  	{"%q",		"Qc",		'и'},		// 2-bytes encoded
    43  	{"%q",		"Qc",		'\u20ac'},	// 3-bytes encoded
    44  	{"%q",		"Qc",		'\U00010001'},	// 4-bytes encoded
    45  
    46  	{"%x",		"Xb",		[]byte("hexstring")},
    47  	{"%x",		"Xs",		"stringhex"},
    48  	{"%d",		"D",		12765},
    49  	{"%d",		"D64",		int64(12764)},
    50  	{"%x",		"X",		12789},
    51  	{"%016x",	"X016",		uint64(124)},
    52  
    53  	{"%v",		"V",		&stringerTest{}},
    54  }
    55  
    56  
    57  type stringerTest struct {
    58  }
    59  
    60  func (s *stringerTest) String() string {
    61  	return string(s.XFmtString(nil))
    62  }
    63  
    64  func (s *stringerTest) XFmtString(b []byte) []byte {
    65  	return append(b, `stringer test`...)
    66  }
    67  
    68  // verify formatting result is the same in between std fmt and xfmt
    69  func TestXFmt(t *testing.T) {
    70  	buf := &Buffer{}
    71  	xbuf := reflect.ValueOf(buf)
    72  
    73  	for _, tt := range testv {
    74  		// result via fmt
    75  		resFmt := fmt.Sprintf(tt.format, tt.value)
    76  
    77  		// result via xfmt (via reflect.Call)
    78  		buf.Reset()
    79  
    80  		xmeth := xbuf.MethodByName(tt.xformatMeth)
    81  		if !xmeth.IsValid() {
    82  			t.Errorf(".%v: no such method", tt.xformatMeth)
    83  			continue
    84  		}
    85  
    86  		xargv := []reflect.Value{reflect.ValueOf(tt.value)}
    87  		xretv := []reflect.Value{}
    88  		callOk := false
    89  		func () {
    90  			defer func() {
    91  				if r := recover(); r != nil {
    92  					t.Errorf("%v: panic: %v", tt, r)
    93  				}
    94  			}()
    95  			xretv = xmeth.Call(xargv)
    96  			callOk = true
    97  		}()
    98  		if !callOk {
    99  			continue
   100  		}
   101  
   102  		// check all formatters return pointer to the same buf
   103  		// (this way it is handy to do .S("hello ") .X016(123) .V(zzz) ...
   104  		if !(len(xretv) == 1 && xretv[0].Interface() == buf) {
   105  			t.Errorf(".%v: returned %#v  ; want %#v", tt.xformatMeth, xretv[0].Interface(), buf)
   106  			continue
   107  		}
   108  
   109  		resXFmt := string(*buf)
   110  
   111  		// results must be the same
   112  		if resFmt != resXFmt {
   113  			t.Errorf(".%v(%v) -> %q   !=   printf(%q) -> %q",
   114  				tt.xformatMeth, tt.value, resXFmt, tt.format, resFmt)
   115  		}
   116  	}
   117  }
   118  
   119  func BenchmarkXFmt(b *testing.B) {
   120  	buf := &Buffer{}
   121  
   122  	for _, tt := range testv {
   123  		b.Run(fmt.Sprintf("%s(%#v)", tt.format, tt.value), func (b *testing.B) {
   124  			for i := 0; i < b.N; i++ {
   125  				fmt.Sprintf(tt.format, tt.value)
   126  			}
   127  		})
   128  
   129  		// construct methProxy for natively calling (not via reflect) method associated with tt.xformatMeth
   130  		// (calling via reflect allocates a lot and is slow)
   131  		// NOTE because of proxies the call is a bit slower than e.g. directly calling buf.S("...")
   132  		var methProxy func(buf *Buffer, v interface{})
   133  
   134  		xmeth, ok := reflect.TypeOf(buf).MethodByName(tt.xformatMeth)
   135  		if !ok {
   136  			b.Errorf(".%v: no such method", tt.xformatMeth)
   137  			continue
   138  		}
   139  
   140  		// XXX a bit ugly -> use code generation instead?
   141  		meth := xmeth.Func.Interface()
   142  		switch tt.value.(type) {
   143  		case byte:	methProxy = func(buf *Buffer, v interface{}) { meth.(func (*Buffer, byte) *Buffer)(buf, v.(byte)) }
   144  		case rune:	methProxy = func(buf *Buffer, v interface{}) { meth.(func (*Buffer, rune) *Buffer)(buf, v.(rune)) }
   145  		case string:	methProxy = func(buf *Buffer, v interface{}) { meth.(func (*Buffer, string) *Buffer)(buf, v.(string)) }
   146  		case []byte:	methProxy = func(buf *Buffer, v interface{}) { meth.(func (*Buffer, []byte) *Buffer)(buf, v.([]byte)) }
   147  		case int:	methProxy = func(buf *Buffer, v interface{}) { meth.(func (*Buffer, int) *Buffer)(buf, v.(int)) }
   148  		case int64:	methProxy = func(buf *Buffer, v interface{}) { meth.(func (*Buffer, int64) *Buffer)(buf, v.(int64)) }
   149  		case uint64:	methProxy = func(buf *Buffer, v interface{}) { meth.(func (*Buffer, uint64) *Buffer)(buf, v.(uint64)) }
   150  
   151  		case *stringerTest:	methProxy = func(buf *Buffer, v interface{}) { meth.(func (*Buffer, Stringer) *Buffer)(buf, v.(Stringer)) }
   152  
   153  		default:
   154  			b.Fatalf("TODO add support for %T", tt.value)
   155  		}
   156  
   157  		b.Run(fmt.Sprintf(".%s(%#v)", tt.xformatMeth, tt.value), func (b *testing.B) {
   158  			for i := 0; i < b.N; i++ {
   159  				buf.Reset()
   160  				methProxy(buf, tt.value)
   161  			}
   162  		})
   163  	}
   164  }