go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/data/lex64/lex64_test.go (about)

     1  // Copyright 2022 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package lex64
    16  
    17  import (
    18  	"fmt"
    19  	"sort"
    20  	"strings"
    21  	"testing"
    22  	"testing/quick"
    23  
    24  	"github.com/google/go-cmp/cmp"
    25  )
    26  
    27  // TestAlphabet checks that the alphabet is in lexicographic order.
    28  func TestAlphabet(t *testing.T) {
    29  	t.Parallel()
    30  
    31  	as := []string{
    32  		alphabetV1,
    33  		alphabetV2,
    34  		fmt.Sprintf("-%s", alphabetV1),
    35  		fmt.Sprintf("-%s", alphabetV2),
    36  	}
    37  
    38  	for _, alphabet := range as {
    39  		sortedAlphabet := sortString(alphabet)
    40  
    41  		if diff := cmp.Diff(sortedAlphabet, alphabet); diff != "" {
    42  			t.Errorf("unexpected diff (-want +got): %s", diff)
    43  		}
    44  	}
    45  }
    46  
    47  // TestGetEncoding tests that invalid encoding names produce an error with
    48  // the expected message.
    49  func TestGetEncoding(t *testing.T) {
    50  	t.Parallel()
    51  
    52  	_, err := GetEncoding(Scheme("17b72b62-202c-4e09-91f1-94bbd1b2ede0"))
    53  	switch err {
    54  	case nil:
    55  		t.Error("error unexpectedly nil")
    56  	default:
    57  		expected := fmt.Sprintf("invalid lex64 version %q", "17b72b62-202c-4e09-91f1-94bbd1b2ede0")
    58  		actual := err.Error()
    59  		if diff := cmp.Diff(expected, actual); diff != "" {
    60  			t.Errorf("unexpected diff (-want +got): %s", diff)
    61  		}
    62  	}
    63  }
    64  
    65  // TestEncodeAndDecode tests that encoding and decoding work and roundtrip as
    66  // expected.
    67  func TestEncodeAndDecode(t *testing.T) {
    68  	t.Parallel()
    69  
    70  	cases := []struct {
    71  		name     string
    72  		in       string
    73  		encoding Scheme
    74  		out      string
    75  	}{
    76  		{
    77  			name:     "F1F2F3 v1",
    78  			in:       "\xF1\xF2\xF3",
    79  			encoding: V1,
    80  			out:      "wUAn",
    81  		},
    82  		{
    83  			name:     "F1F2F3 v2",
    84  			in:       "\xF1\xF2\xF3",
    85  			encoding: V2,
    86  			out:      "wUAn",
    87  		},
    88  		{
    89  			name:     "empty string v1",
    90  			in:       "",
    91  			encoding: V1,
    92  			out:      "",
    93  		},
    94  		{
    95  			name:     "empty string v2",
    96  			in:       "",
    97  			encoding: V1,
    98  			out:      "",
    99  		},
   100  		{
   101  			name:     "single char v1",
   102  			in:       "\x00",
   103  			encoding: V1,
   104  			out:      "00",
   105  		},
   106  		{
   107  			name:     "single char v2",
   108  			in:       "\x00",
   109  			encoding: V2,
   110  			out:      "..",
   111  		},
   112  		{
   113  			name:     "random string v1",
   114  			in:       "\x67\x9a\x5c\x48\xbe\x97\x27\x75\xdf\x6a",
   115  			encoding: V1,
   116  			out:      "OtdRHAuM9rMUPV",
   117  		},
   118  		{
   119  			name:     "random string v2",
   120  			in:       "\x67\x9a\x5c\x48\xbe\x97\x27\x75\xdf\x6a",
   121  			encoding: V2,
   122  			out:      "OtdRHAuM8rMUPV",
   123  		},
   124  	}
   125  
   126  	for _, tt := range cases {
   127  		tt := tt
   128  		t.Run(tt.name, func(t *testing.T) {
   129  			t.Parallel()
   130  
   131  			encoding := encodings[tt.encoding]
   132  
   133  			in := []byte(tt.in)
   134  			actual, err := Encode(encoding, in)
   135  			if err != nil {
   136  				t.Errorf("unexpected error for subtest %q: %s", tt.name, err)
   137  			}
   138  			expected := tt.out
   139  
   140  			if diff := cmp.Diff(expected, actual); diff != "" {
   141  				t.Errorf("unexpected diff during encoding for subtest %q (-want +got): %s", tt.name, diff)
   142  			}
   143  		})
   144  	}
   145  }
   146  
   147  // TestRoundTripEquality tests that encoding and decoding with padding
   148  // round-trips.
   149  func TestRoundTripEquality(t *testing.T) {
   150  	t.Parallel()
   151  
   152  	for _, v := range []struct {
   153  		opts Scheme
   154  	}{
   155  		{
   156  			opts: V1,
   157  		},
   158  		{
   159  			opts: V1Padding,
   160  		},
   161  		{
   162  			opts: V2,
   163  		},
   164  		{
   165  			opts: V2Padding,
   166  		},
   167  	} {
   168  		roundTrip := func(a []byte) bool {
   169  			encoding, _ := GetEncoding(v.opts)
   170  			encoded, err := Encode(encoding, a)
   171  			if err != nil {
   172  				panic(err.Error())
   173  			}
   174  			decoded, err := Decode(encoding, encoded)
   175  			if err != nil {
   176  				panic(err.Error())
   177  			}
   178  			return cmpBytes(a, decoded) == 0
   179  		}
   180  
   181  		if err := quick.Check(roundTrip, nil); err != nil {
   182  			t.Errorf("unexpected error: %s", err)
   183  		}
   184  	}
   185  }
   186  
   187  // TestPreservationOfComparisonOrder tests that encoding with padding preserves
   188  // comparison order.
   189  func TestPreservationOfComparisonOrder(t *testing.T) {
   190  	t.Parallel()
   191  
   192  	for _, v := range []struct {
   193  		name string
   194  		opts Scheme
   195  	}{
   196  		{
   197  			name: "current",
   198  			opts: V2,
   199  		},
   200  		{
   201  			name: "current with padding",
   202  			opts: V2Padding,
   203  		},
   204  		{
   205  			name: "legacy",
   206  			opts: V1,
   207  		},
   208  		{
   209  			name: "legacy with padding",
   210  			opts: V1Padding,
   211  		},
   212  	} {
   213  		v := v
   214  		t.Run(v.name, func(t *testing.T) {
   215  			t.Parallel()
   216  			expected := func(a []byte, b []byte) int {
   217  				return cmpBytes(a, b)
   218  			}
   219  
   220  			actual := func(a []byte, b []byte) int {
   221  				encoding, _ := GetEncoding(v.opts)
   222  				a1, err := Encode(encoding, a)
   223  				if err != nil {
   224  					panic(err.Error())
   225  				}
   226  				b1, err := Encode(encoding, b)
   227  				if err != nil {
   228  					panic(err.Error())
   229  				}
   230  				return strings.Compare(a1, b1)
   231  			}
   232  			if err := quick.CheckEqual(expected, actual, &quick.Config{
   233  				MaxCount: 10000,
   234  			}); err != nil {
   235  				t.Errorf("unexpected error: %s", err)
   236  			}
   237  		})
   238  	}
   239  }
   240  
   241  // TestConcatenationFails is a demonstration that concatenation is not respected.
   242  func TestConcatenationFails(t *testing.T) {
   243  	t.Parallel()
   244  
   245  	encoding, _ := GetEncoding(V2Padding)
   246  
   247  	a := []byte("a")
   248  	b := []byte("b")
   249  
   250  	a2, err := Encode(encoding, a)
   251  	if err != nil {
   252  		t.Error(err)
   253  	}
   254  
   255  	b2, err := Encode(encoding, b)
   256  	if err != nil {
   257  		t.Error(err)
   258  	}
   259  
   260  	_, err = Decode(encoding, fmt.Sprintf("%s%s", a2, b2))
   261  	switch err {
   262  	case nil:
   263  		t.Error("error is unexpectedly nil")
   264  	default:
   265  		if !strings.Contains(err.Error(), "illegal base64") {
   266  			t.Errorf("wrong error: %s", err)
   267  		}
   268  	}
   269  }
   270  
   271  // cmpString compares two sequences of bytes and returns +1, 0, or -1.
   272  func cmpBytes(a []byte, b []byte) int {
   273  	return strings.Compare(string(a), string(b))
   274  }
   275  
   276  // sortString sorts a string characterwise.
   277  func sortString(s string) string {
   278  	chars := strings.Split(s, "")
   279  	sort.Strings(chars)
   280  	return strings.Join(chars, "")
   281  }