github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fs/operations/check_test.go (about)

     1  package operations_test
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"log"
    10  	"os"
    11  	"sort"
    12  	"strings"
    13  	"testing"
    14  
    15  	"github.com/rclone/rclone/fs"
    16  	"github.com/rclone/rclone/fs/accounting"
    17  	"github.com/rclone/rclone/fs/hash"
    18  	"github.com/rclone/rclone/fs/operations"
    19  	"github.com/rclone/rclone/fstest"
    20  	"github.com/rclone/rclone/lib/readers"
    21  	"github.com/stretchr/testify/assert"
    22  	"github.com/stretchr/testify/require"
    23  	"golang.org/x/text/unicode/norm"
    24  )
    25  
    26  func testCheck(t *testing.T, checkFunction func(ctx context.Context, opt *operations.CheckOpt) error) {
    27  	r := fstest.NewRun(t)
    28  	ctx := context.Background()
    29  	ci := fs.GetConfig(ctx)
    30  
    31  	addBuffers := func(opt *operations.CheckOpt) {
    32  		opt.Combined = new(bytes.Buffer)
    33  		opt.MissingOnSrc = new(bytes.Buffer)
    34  		opt.MissingOnDst = new(bytes.Buffer)
    35  		opt.Match = new(bytes.Buffer)
    36  		opt.Differ = new(bytes.Buffer)
    37  		opt.Error = new(bytes.Buffer)
    38  	}
    39  
    40  	sortLines := func(in string) []string {
    41  		if in == "" {
    42  			return []string{}
    43  		}
    44  		lines := strings.Split(in, "\n")
    45  		sort.Strings(lines)
    46  		return lines
    47  	}
    48  
    49  	checkBuffer := func(name string, want map[string]string, out io.Writer) {
    50  		expected := want[name]
    51  		buf, ok := out.(*bytes.Buffer)
    52  		require.True(t, ok)
    53  		assert.Equal(t, sortLines(expected), sortLines(buf.String()), name)
    54  	}
    55  
    56  	checkBuffers := func(opt *operations.CheckOpt, want map[string]string) {
    57  		checkBuffer("combined", want, opt.Combined)
    58  		checkBuffer("missingonsrc", want, opt.MissingOnSrc)
    59  		checkBuffer("missingondst", want, opt.MissingOnDst)
    60  		checkBuffer("match", want, opt.Match)
    61  		checkBuffer("differ", want, opt.Differ)
    62  		checkBuffer("error", want, opt.Error)
    63  	}
    64  
    65  	check := func(i int, wantErrors int64, wantChecks int64, oneway bool, wantOutput map[string]string) {
    66  		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
    67  			accounting.GlobalStats().ResetCounters()
    68  			var buf bytes.Buffer
    69  			log.SetOutput(&buf)
    70  			defer func() {
    71  				log.SetOutput(os.Stderr)
    72  			}()
    73  			opt := operations.CheckOpt{
    74  				Fdst:   r.Fremote,
    75  				Fsrc:   r.Flocal,
    76  				OneWay: oneway,
    77  			}
    78  			addBuffers(&opt)
    79  			err := checkFunction(ctx, &opt)
    80  			gotErrors := accounting.GlobalStats().GetErrors()
    81  			gotChecks := accounting.GlobalStats().GetChecks()
    82  			if wantErrors == 0 && err != nil {
    83  				t.Errorf("%d: Got error when not expecting one: %v", i, err)
    84  			}
    85  			if wantErrors != 0 && err == nil {
    86  				t.Errorf("%d: No error when expecting one", i)
    87  			}
    88  			if wantErrors != gotErrors {
    89  				t.Errorf("%d: Expecting %d errors but got %d", i, wantErrors, gotErrors)
    90  			}
    91  			if gotChecks > 0 && !strings.Contains(buf.String(), "matching files") {
    92  				t.Errorf("%d: Total files matching line missing", i)
    93  			}
    94  			if wantChecks != gotChecks {
    95  				t.Errorf("%d: Expecting %d total matching files but got %d", i, wantChecks, gotChecks)
    96  			}
    97  			checkBuffers(&opt, wantOutput)
    98  		})
    99  	}
   100  
   101  	file1 := r.WriteBoth(ctx, "rutabaga", "is tasty", t3)
   102  	r.CheckRemoteItems(t, file1)
   103  	r.CheckLocalItems(t, file1)
   104  	check(1, 0, 1, false, map[string]string{
   105  		"combined":     "= rutabaga\n",
   106  		"missingonsrc": "",
   107  		"missingondst": "",
   108  		"match":        "rutabaga\n",
   109  		"differ":       "",
   110  		"error":        "",
   111  	})
   112  
   113  	file2 := r.WriteFile("potato2", "------------------------------------------------------------", t1)
   114  	r.CheckLocalItems(t, file1, file2)
   115  	check(2, 1, 1, false, map[string]string{
   116  		"combined":     "+ potato2\n= rutabaga\n",
   117  		"missingonsrc": "",
   118  		"missingondst": "potato2\n",
   119  		"match":        "rutabaga\n",
   120  		"differ":       "",
   121  		"error":        "",
   122  	})
   123  
   124  	file3 := r.WriteObject(ctx, "empty space", "-", t2)
   125  	r.CheckRemoteItems(t, file1, file3)
   126  	check(3, 2, 1, false, map[string]string{
   127  		"combined":     "- empty space\n+ potato2\n= rutabaga\n",
   128  		"missingonsrc": "empty space\n",
   129  		"missingondst": "potato2\n",
   130  		"match":        "rutabaga\n",
   131  		"differ":       "",
   132  		"error":        "",
   133  	})
   134  
   135  	file2r := file2
   136  	if ci.SizeOnly {
   137  		file2r = r.WriteObject(ctx, "potato2", "--Some-Differences-But-Size-Only-Is-Enabled-----------------", t1)
   138  	} else {
   139  		r.WriteObject(ctx, "potato2", "------------------------------------------------------------", t1)
   140  	}
   141  	r.CheckRemoteItems(t, file1, file2r, file3)
   142  	check(4, 1, 2, false, map[string]string{
   143  		"combined":     "- empty space\n= potato2\n= rutabaga\n",
   144  		"missingonsrc": "empty space\n",
   145  		"missingondst": "",
   146  		"match":        "rutabaga\npotato2\n",
   147  		"differ":       "",
   148  		"error":        "",
   149  	})
   150  
   151  	file3r := file3
   152  	file3l := r.WriteFile("empty space", "DIFFER", t2)
   153  	r.CheckLocalItems(t, file1, file2, file3l)
   154  	check(5, 1, 3, false, map[string]string{
   155  		"combined":     "* empty space\n= potato2\n= rutabaga\n",
   156  		"missingonsrc": "",
   157  		"missingondst": "",
   158  		"match":        "potato2\nrutabaga\n",
   159  		"differ":       "empty space\n",
   160  		"error":        "",
   161  	})
   162  
   163  	file4 := r.WriteObject(ctx, "remotepotato", "------------------------------------------------------------", t1)
   164  	r.CheckRemoteItems(t, file1, file2r, file3r, file4)
   165  	check(6, 2, 3, false, map[string]string{
   166  		"combined":     "* empty space\n= potato2\n= rutabaga\n- remotepotato\n",
   167  		"missingonsrc": "remotepotato\n",
   168  		"missingondst": "",
   169  		"match":        "potato2\nrutabaga\n",
   170  		"differ":       "empty space\n",
   171  		"error":        "",
   172  	})
   173  	check(7, 1, 3, true, map[string]string{
   174  		"combined":     "* empty space\n= potato2\n= rutabaga\n",
   175  		"missingonsrc": "",
   176  		"missingondst": "",
   177  		"match":        "potato2\nrutabaga\n",
   178  		"differ":       "empty space\n",
   179  		"error":        "",
   180  	})
   181  }
   182  
   183  func TestCheck(t *testing.T) {
   184  	testCheck(t, operations.Check)
   185  }
   186  
   187  func TestCheckFsError(t *testing.T) {
   188  	ctx := context.Background()
   189  	dstFs, err := fs.NewFs(ctx, "nonexistent")
   190  	if err != nil {
   191  		t.Fatal(err)
   192  	}
   193  	srcFs, err := fs.NewFs(ctx, "nonexistent")
   194  	if err != nil {
   195  		t.Fatal(err)
   196  	}
   197  	opt := operations.CheckOpt{
   198  		Fdst:   dstFs,
   199  		Fsrc:   srcFs,
   200  		OneWay: false,
   201  	}
   202  	err = operations.Check(ctx, &opt)
   203  	require.Error(t, err)
   204  }
   205  
   206  func TestCheckDownload(t *testing.T) {
   207  	testCheck(t, operations.CheckDownload)
   208  }
   209  
   210  func TestCheckSizeOnly(t *testing.T) {
   211  	ctx := context.Background()
   212  	ci := fs.GetConfig(ctx)
   213  	ci.SizeOnly = true
   214  	defer func() { ci.SizeOnly = false }()
   215  	TestCheck(t)
   216  }
   217  
   218  func TestCheckEqualReaders(t *testing.T) {
   219  	b65a := make([]byte, 65*1024)
   220  	b65b := make([]byte, 65*1024)
   221  	b65b[len(b65b)-1] = 1
   222  	b66 := make([]byte, 66*1024)
   223  
   224  	differ, err := operations.CheckEqualReaders(bytes.NewBuffer(b65a), bytes.NewBuffer(b65a))
   225  	assert.NoError(t, err)
   226  	assert.Equal(t, differ, false)
   227  
   228  	differ, err = operations.CheckEqualReaders(bytes.NewBuffer(b65a), bytes.NewBuffer(b65b))
   229  	assert.NoError(t, err)
   230  	assert.Equal(t, differ, true)
   231  
   232  	differ, err = operations.CheckEqualReaders(bytes.NewBuffer(b65a), bytes.NewBuffer(b66))
   233  	assert.NoError(t, err)
   234  	assert.Equal(t, differ, true)
   235  
   236  	differ, err = operations.CheckEqualReaders(bytes.NewBuffer(b66), bytes.NewBuffer(b65a))
   237  	assert.NoError(t, err)
   238  	assert.Equal(t, differ, true)
   239  
   240  	myErr := errors.New("sentinel")
   241  	wrap := func(b []byte) io.Reader {
   242  		r := bytes.NewBuffer(b)
   243  		e := readers.ErrorReader{Err: myErr}
   244  		return io.MultiReader(r, e)
   245  	}
   246  
   247  	differ, err = operations.CheckEqualReaders(wrap(b65a), bytes.NewBuffer(b65a))
   248  	assert.Equal(t, myErr, err)
   249  	assert.Equal(t, differ, true)
   250  
   251  	differ, err = operations.CheckEqualReaders(wrap(b65a), bytes.NewBuffer(b65b))
   252  	assert.Equal(t, myErr, err)
   253  	assert.Equal(t, differ, true)
   254  
   255  	differ, err = operations.CheckEqualReaders(wrap(b65a), bytes.NewBuffer(b66))
   256  	assert.Equal(t, myErr, err)
   257  	assert.Equal(t, differ, true)
   258  
   259  	differ, err = operations.CheckEqualReaders(wrap(b66), bytes.NewBuffer(b65a))
   260  	assert.Equal(t, myErr, err)
   261  	assert.Equal(t, differ, true)
   262  
   263  	differ, err = operations.CheckEqualReaders(bytes.NewBuffer(b65a), wrap(b65a))
   264  	assert.Equal(t, myErr, err)
   265  	assert.Equal(t, differ, true)
   266  
   267  	differ, err = operations.CheckEqualReaders(bytes.NewBuffer(b65a), wrap(b65b))
   268  	assert.Equal(t, myErr, err)
   269  	assert.Equal(t, differ, true)
   270  
   271  	differ, err = operations.CheckEqualReaders(bytes.NewBuffer(b65a), wrap(b66))
   272  	assert.Equal(t, myErr, err)
   273  	assert.Equal(t, differ, true)
   274  
   275  	differ, err = operations.CheckEqualReaders(bytes.NewBuffer(b66), wrap(b65a))
   276  	assert.Equal(t, myErr, err)
   277  	assert.Equal(t, differ, true)
   278  }
   279  
   280  func TestParseSumFile(t *testing.T) {
   281  	r := fstest.NewRun(t)
   282  	ctx := context.Background()
   283  
   284  	const sumFile = "test.sum"
   285  
   286  	samples := []struct {
   287  		hash, sep, name string
   288  		ok              bool
   289  	}{
   290  		{"1", "  ", "file1", true},
   291  		{"2", " *", "file2", true},
   292  		{"3", "  ", " file3 ", true},
   293  		{"4", "  ", "\tfile3\t", true},
   294  		{"5", " ", "file5", false},
   295  		{"6", "\t", "file6", false},
   296  		{"7", " \t", " file7 ", false},
   297  		{"", "  ", "file8", false},
   298  		{"", "", "file9", false},
   299  	}
   300  
   301  	for _, eol := range []string{"\n", "\r\n"} {
   302  		data := &bytes.Buffer{}
   303  		wantNum := 0
   304  		for _, s := range samples {
   305  			_, _ = data.WriteString(s.hash + s.sep + s.name + eol)
   306  			if s.ok {
   307  				wantNum++
   308  			}
   309  		}
   310  
   311  		_ = r.WriteObject(ctx, sumFile, data.String(), t1)
   312  		file, err := r.Fremote.NewObject(ctx, sumFile)
   313  		assert.NoError(t, err)
   314  		sums, err := operations.ParseSumFile(ctx, file)
   315  		assert.NoError(t, err)
   316  
   317  		assert.Equal(t, wantNum, len(sums))
   318  		for _, s := range samples {
   319  			if s.ok {
   320  				assert.Equal(t, s.hash, sums[s.name])
   321  			}
   322  		}
   323  	}
   324  }
   325  
   326  func testCheckSum(t *testing.T, download bool) {
   327  	const dataDir = "data"
   328  	const sumFile = "test.sum"
   329  
   330  	hashType := hash.MD5
   331  	const (
   332  		testString1      = "Hello, World!"
   333  		testDigest1      = "65a8e27d8879283831b664bd8b7f0ad4"
   334  		testDigest1Upper = "65A8E27D8879283831B664BD8B7F0AD4"
   335  		testString2      = "I am the walrus"
   336  		testDigest2      = "87396e030ef3f5b35bbf85c0a09a4fb3"
   337  		testDigest2Mixed = "87396e030EF3f5b35BBf85c0a09a4FB3"
   338  	)
   339  
   340  	type wantType map[string]string
   341  
   342  	ctx := context.Background()
   343  	r := fstest.NewRun(t)
   344  
   345  	subRemote := r.FremoteName
   346  	if !strings.HasSuffix(subRemote, ":") {
   347  		subRemote += "/"
   348  	}
   349  	subRemote += dataDir
   350  	dataFs, err := fs.NewFs(ctx, subRemote)
   351  	require.NoError(t, err)
   352  
   353  	if !download && !dataFs.Hashes().Contains(hashType) {
   354  		t.Skipf("%s lacks %s, skipping", dataFs, hashType)
   355  	}
   356  
   357  	makeFile := func(name, content string) fstest.Item {
   358  		remote := dataDir + "/" + name
   359  		return r.WriteObject(ctx, remote, content, t1)
   360  	}
   361  
   362  	makeSums := func(sums operations.HashSums) fstest.Item {
   363  		files := make([]string, 0, len(sums))
   364  		for name := range sums {
   365  			files = append(files, name)
   366  		}
   367  		sort.Strings(files)
   368  		buf := &bytes.Buffer{}
   369  		for _, name := range files {
   370  			_, _ = fmt.Fprintf(buf, "%s  %s\n", sums[name], name)
   371  		}
   372  		return r.WriteObject(ctx, sumFile, buf.String(), t1)
   373  	}
   374  
   375  	sortLines := func(in string) []string {
   376  		if in == "" {
   377  			return []string{}
   378  		}
   379  		lines := strings.Split(in, "\n")
   380  		sort.Strings(lines)
   381  		return lines
   382  	}
   383  
   384  	checkResult := func(runNo int, want wantType, name string, out io.Writer) {
   385  		expected := want[name]
   386  		buf, ok := out.(*bytes.Buffer)
   387  		require.True(t, ok)
   388  		assert.Equal(t, sortLines(expected), sortLines(buf.String()), "wrong %s result in run %d", name, runNo)
   389  	}
   390  
   391  	checkRun := func(runNo, wantChecks, wantErrors int, want wantType) {
   392  		accounting.GlobalStats().ResetCounters()
   393  		buf := new(bytes.Buffer)
   394  		log.SetOutput(buf)
   395  		defer log.SetOutput(os.Stderr)
   396  
   397  		opt := operations.CheckOpt{
   398  			Combined:     new(bytes.Buffer),
   399  			Match:        new(bytes.Buffer),
   400  			Differ:       new(bytes.Buffer),
   401  			Error:        new(bytes.Buffer),
   402  			MissingOnSrc: new(bytes.Buffer),
   403  			MissingOnDst: new(bytes.Buffer),
   404  		}
   405  		err := operations.CheckSum(ctx, dataFs, r.Fremote, sumFile, hashType, &opt, download)
   406  
   407  		gotErrors := int(accounting.GlobalStats().GetErrors())
   408  		if wantErrors == 0 {
   409  			assert.NoError(t, err, "unexpected error in run %d", runNo)
   410  		}
   411  		if wantErrors > 0 {
   412  			assert.Error(t, err, "no expected error in run %d", runNo)
   413  		}
   414  		assert.Equal(t, wantErrors, gotErrors, "wrong error count in run %d", runNo)
   415  
   416  		gotChecks := int(accounting.GlobalStats().GetChecks())
   417  		if wantChecks > 0 || gotChecks > 0 {
   418  			assert.Contains(t, buf.String(), "matching files", "missing matching files in run %d", runNo)
   419  		}
   420  		assert.Equal(t, wantChecks, gotChecks, "wrong number of checks in run %d", runNo)
   421  
   422  		checkResult(runNo, want, "combined", opt.Combined)
   423  		checkResult(runNo, want, "missingonsrc", opt.MissingOnSrc)
   424  		checkResult(runNo, want, "missingondst", opt.MissingOnDst)
   425  		checkResult(runNo, want, "match", opt.Match)
   426  		checkResult(runNo, want, "differ", opt.Differ)
   427  		checkResult(runNo, want, "error", opt.Error)
   428  	}
   429  
   430  	check := func(runNo, wantChecks, wantErrors int, wantResults wantType) {
   431  		runName := fmt.Sprintf("subtest%d", runNo)
   432  		t.Run(runName, func(t *testing.T) {
   433  			checkRun(runNo, wantChecks, wantErrors, wantResults)
   434  		})
   435  	}
   436  
   437  	file1 := makeFile("banana", testString1)
   438  	fcsums := makeSums(operations.HashSums{
   439  		"banana": testDigest1,
   440  	})
   441  	r.CheckRemoteItems(t, fcsums, file1)
   442  	check(1, 1, 0, wantType{
   443  		"combined":     "= banana\n",
   444  		"missingonsrc": "",
   445  		"missingondst": "",
   446  		"match":        "banana\n",
   447  		"differ":       "",
   448  		"error":        "",
   449  	})
   450  
   451  	file2 := makeFile("potato", testString2)
   452  	fcsums = makeSums(operations.HashSums{
   453  		"banana": testDigest1,
   454  	})
   455  	r.CheckRemoteItems(t, fcsums, file1, file2)
   456  	check(2, 2, 1, wantType{
   457  		"combined":     "- potato\n= banana\n",
   458  		"missingonsrc": "potato\n",
   459  		"missingondst": "",
   460  		"match":        "banana\n",
   461  		"differ":       "",
   462  		"error":        "",
   463  	})
   464  
   465  	fcsums = makeSums(operations.HashSums{
   466  		"banana": testDigest1,
   467  		"potato": testDigest2,
   468  	})
   469  	r.CheckRemoteItems(t, fcsums, file1, file2)
   470  	check(3, 2, 0, wantType{
   471  		"combined":     "= potato\n= banana\n",
   472  		"missingonsrc": "",
   473  		"missingondst": "",
   474  		"match":        "banana\npotato\n",
   475  		"differ":       "",
   476  		"error":        "",
   477  	})
   478  
   479  	fcsums = makeSums(operations.HashSums{
   480  		"banana": testDigest2,
   481  		"potato": testDigest2,
   482  	})
   483  	r.CheckRemoteItems(t, fcsums, file1, file2)
   484  	check(4, 2, 1, wantType{
   485  		"combined":     "* banana\n= potato\n",
   486  		"missingonsrc": "",
   487  		"missingondst": "",
   488  		"match":        "potato\n",
   489  		"differ":       "banana\n",
   490  		"error":        "",
   491  	})
   492  
   493  	fcsums = makeSums(operations.HashSums{
   494  		"banana": testDigest1,
   495  		"potato": testDigest2,
   496  		"orange": testDigest2,
   497  	})
   498  	r.CheckRemoteItems(t, fcsums, file1, file2)
   499  	check(5, 2, 1, wantType{
   500  		"combined":     "+ orange\n= potato\n= banana\n",
   501  		"missingonsrc": "",
   502  		"missingondst": "orange\n",
   503  		"match":        "banana\npotato\n",
   504  		"differ":       "",
   505  		"error":        "",
   506  	})
   507  
   508  	fcsums = makeSums(operations.HashSums{
   509  		"banana": testDigest1,
   510  		"potato": testDigest1,
   511  		"orange": testDigest2,
   512  	})
   513  	r.CheckRemoteItems(t, fcsums, file1, file2)
   514  	check(6, 2, 2, wantType{
   515  		"combined":     "+ orange\n* potato\n= banana\n",
   516  		"missingonsrc": "",
   517  		"missingondst": "orange\n",
   518  		"match":        "banana\n",
   519  		"differ":       "potato\n",
   520  		"error":        "",
   521  	})
   522  
   523  	// test mixed-case checksums
   524  	file1 = makeFile("banana", testString1)
   525  	file2 = makeFile("potato", testString2)
   526  	fcsums = makeSums(operations.HashSums{
   527  		"banana": testDigest1Upper,
   528  		"potato": testDigest2Mixed,
   529  	})
   530  	r.CheckRemoteItems(t, fcsums, file1, file2)
   531  	check(7, 2, 0, wantType{
   532  		"combined":     "= banana\n= potato\n",
   533  		"missingonsrc": "",
   534  		"missingondst": "",
   535  		"match":        "banana\npotato\n",
   536  		"differ":       "",
   537  		"error":        "",
   538  	})
   539  }
   540  
   541  func TestCheckSum(t *testing.T) {
   542  	testCheckSum(t, false)
   543  }
   544  
   545  func TestCheckSumDownload(t *testing.T) {
   546  	testCheckSum(t, true)
   547  }
   548  
   549  func TestApplyTransforms(t *testing.T) {
   550  	var (
   551  		hashType        = hash.MD5
   552  		content         = "Hello, World!"
   553  		hash            = "65a8e27d8879283831b664bd8b7f0ad4"
   554  		nfc             = norm.NFC.String(norm.NFD.String("測試_Русский___ě_áñ"))
   555  		nfd             = norm.NFD.String(nfc)
   556  		nfcx2           = nfc + nfc
   557  		nfdx2           = nfd + nfd
   558  		both            = nfc + nfd
   559  		upper           = "HELLO, WORLD!"
   560  		lower           = "hello, world!"
   561  		upperlowermixed = "HeLlO, wOrLd!"
   562  	)
   563  
   564  	testScenario := func(checkfileName, remotefileName, scenario string) {
   565  		r := fstest.NewRunIndividual(t)
   566  		ctx := context.Background()
   567  		ci := fs.GetConfig(ctx)
   568  		opt := operations.CheckOpt{}
   569  
   570  		remotefile := r.WriteObject(ctx, remotefileName, content, t2)
   571  		// test whether remote is capable of running test
   572  		entries, err := r.Fremote.List(ctx, "")
   573  		assert.NoError(t, err)
   574  		if entries.Len() == 1 && entries[0].Remote() != remotefileName {
   575  			t.Skipf("Fs is incapable of running test, skipping: %s (expected: %s (%s) actual: %s (%s))", scenario, remotefileName, detectEncoding(remotefileName), entries[0].Remote(), detectEncoding(entries[0].Remote()))
   576  		}
   577  
   578  		checkfile := r.WriteFile("test.sum", hash+"  "+checkfileName, t2)
   579  		r.CheckLocalItems(t, checkfile)
   580  		assert.False(t, checkfileName == remotefile.Path, "Values match but should not: %s %s", checkfileName, remotefile.Path)
   581  
   582  		testname := scenario + " (without normalization)"
   583  		println(testname)
   584  		ci.NoUnicodeNormalization = true
   585  		ci.IgnoreCaseSync = false
   586  		accounting.GlobalStats().ResetCounters()
   587  		err = operations.CheckSum(ctx, r.Fremote, r.Flocal, "test.sum", hashType, &opt, true)
   588  		assert.Error(t, err, "no expected error for %s %v %v", testname, checkfileName, remotefileName)
   589  
   590  		testname = scenario + " (with normalization)"
   591  		println(testname)
   592  		ci.NoUnicodeNormalization = false
   593  		ci.IgnoreCaseSync = true
   594  		accounting.GlobalStats().ResetCounters()
   595  		err = operations.CheckSum(ctx, r.Fremote, r.Flocal, "test.sum", hashType, &opt, true)
   596  		assert.NoError(t, err, "unexpected error for %s %v %v", testname, checkfileName, remotefileName)
   597  	}
   598  
   599  	testScenario(upper, lower, "upper checkfile vs. lower remote")
   600  	testScenario(lower, upper, "lower checkfile vs. upper remote")
   601  	testScenario(lower, upperlowermixed, "lower checkfile vs. upperlowermixed remote")
   602  	testScenario(upperlowermixed, upper, "upperlowermixed checkfile vs. upper remote")
   603  	testScenario(nfd, nfc, "NFD checkfile vs. NFC remote")
   604  	testScenario(nfc, nfd, "NFC checkfile vs. NFD remote")
   605  	testScenario(nfdx2, both, "NFDx2 checkfile vs. both remote")
   606  	testScenario(nfcx2, both, "NFCx2 checkfile vs. both remote")
   607  	testScenario(both, nfdx2, "both checkfile vs. NFDx2 remote")
   608  	testScenario(both, nfcx2, "both checkfile vs. NFCx2 remote")
   609  }
   610  
   611  func detectEncoding(s string) string {
   612  	if norm.NFC.IsNormalString(s) && norm.NFD.IsNormalString(s) {
   613  		return "BOTH"
   614  	}
   615  	if !norm.NFC.IsNormalString(s) && norm.NFD.IsNormalString(s) {
   616  		return "NFD"
   617  	}
   618  	if norm.NFC.IsNormalString(s) && !norm.NFD.IsNormalString(s) {
   619  		return "NFC"
   620  	}
   621  	return "OTHER"
   622  }