github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/views/hook_ui_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package views
     5  
     6  import (
     7  	"fmt"
     8  	"regexp"
     9  	"testing"
    10  	"time"
    11  
    12  	"strings"
    13  
    14  	"github.com/zclconf/go-cty/cty"
    15  
    16  	"github.com/terramate-io/tf/addrs"
    17  	"github.com/terramate-io/tf/command/arguments"
    18  	"github.com/terramate-io/tf/plans"
    19  	"github.com/terramate-io/tf/providers"
    20  	"github.com/terramate-io/tf/states"
    21  	"github.com/terramate-io/tf/terminal"
    22  	"github.com/terramate-io/tf/terraform"
    23  )
    24  
    25  // Test the PreApply hook for creating a new resource
    26  func TestUiHookPreApply_create(t *testing.T) {
    27  	streams, done := terminal.StreamsForTesting(t)
    28  	view := NewView(streams)
    29  	h := NewUiHook(view)
    30  	h.resources = map[string]uiResourceState{
    31  		"test_instance.foo": {
    32  			Op:    uiResourceCreate,
    33  			Start: time.Now(),
    34  		},
    35  	}
    36  
    37  	addr := addrs.Resource{
    38  		Mode: addrs.ManagedResourceMode,
    39  		Type: "test_instance",
    40  		Name: "foo",
    41  	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
    42  
    43  	priorState := cty.NullVal(cty.Object(map[string]cty.Type{
    44  		"id":  cty.String,
    45  		"bar": cty.List(cty.String),
    46  	}))
    47  	plannedNewState := cty.ObjectVal(map[string]cty.Value{
    48  		"id": cty.StringVal("test"),
    49  		"bar": cty.ListVal([]cty.Value{
    50  			cty.StringVal("baz"),
    51  		}),
    52  	})
    53  
    54  	action, err := h.PreApply(addr, states.CurrentGen, plans.Create, priorState, plannedNewState)
    55  	if err != nil {
    56  		t.Fatal(err)
    57  	}
    58  	if action != terraform.HookActionContinue {
    59  		t.Fatalf("Expected hook to continue, given: %#v", action)
    60  	}
    61  
    62  	// stop the background writer
    63  	uiState := h.resources[addr.String()]
    64  	close(uiState.DoneCh)
    65  	<-uiState.done
    66  
    67  	expectedOutput := "test_instance.foo: Creating...\n"
    68  	result := done(t)
    69  	output := result.Stdout()
    70  	if output != expectedOutput {
    71  		t.Fatalf("Output didn't match.\nExpected: %q\nGiven: %q", expectedOutput, output)
    72  	}
    73  
    74  	expectedErrOutput := ""
    75  	errOutput := result.Stderr()
    76  	if errOutput != expectedErrOutput {
    77  		t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput)
    78  	}
    79  }
    80  
    81  // Test the PreApply hook's use of a periodic timer to display "still working"
    82  // log lines
    83  func TestUiHookPreApply_periodicTimer(t *testing.T) {
    84  	streams, done := terminal.StreamsForTesting(t)
    85  	view := NewView(streams)
    86  	h := NewUiHook(view)
    87  	h.periodicUiTimer = 1 * time.Second
    88  	h.resources = map[string]uiResourceState{
    89  		"test_instance.foo": {
    90  			Op:    uiResourceModify,
    91  			Start: time.Now(),
    92  		},
    93  	}
    94  
    95  	addr := addrs.Resource{
    96  		Mode: addrs.ManagedResourceMode,
    97  		Type: "test_instance",
    98  		Name: "foo",
    99  	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
   100  
   101  	priorState := cty.ObjectVal(map[string]cty.Value{
   102  		"id":  cty.StringVal("test"),
   103  		"bar": cty.ListValEmpty(cty.String),
   104  	})
   105  	plannedNewState := cty.ObjectVal(map[string]cty.Value{
   106  		"id": cty.StringVal("test"),
   107  		"bar": cty.ListVal([]cty.Value{
   108  			cty.StringVal("baz"),
   109  		}),
   110  	})
   111  
   112  	action, err := h.PreApply(addr, states.CurrentGen, plans.Update, priorState, plannedNewState)
   113  	if err != nil {
   114  		t.Fatal(err)
   115  	}
   116  	if action != terraform.HookActionContinue {
   117  		t.Fatalf("Expected hook to continue, given: %#v", action)
   118  	}
   119  
   120  	time.Sleep(3100 * time.Millisecond)
   121  
   122  	// stop the background writer
   123  	uiState := h.resources[addr.String()]
   124  	close(uiState.DoneCh)
   125  	<-uiState.done
   126  
   127  	expectedOutput := `test_instance.foo: Modifying... [id=test]
   128  test_instance.foo: Still modifying... [id=test, 1s elapsed]
   129  test_instance.foo: Still modifying... [id=test, 2s elapsed]
   130  test_instance.foo: Still modifying... [id=test, 3s elapsed]
   131  `
   132  	result := done(t)
   133  	output := result.Stdout()
   134  	if output != expectedOutput {
   135  		t.Fatalf("Output didn't match.\nExpected: %q\nGiven: %q", expectedOutput, output)
   136  	}
   137  
   138  	expectedErrOutput := ""
   139  	errOutput := result.Stderr()
   140  	if errOutput != expectedErrOutput {
   141  		t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput)
   142  	}
   143  }
   144  
   145  // Test the PreApply hook's destroy path, including passing a deposed key as
   146  // the gen argument.
   147  func TestUiHookPreApply_destroy(t *testing.T) {
   148  	streams, done := terminal.StreamsForTesting(t)
   149  	view := NewView(streams)
   150  	h := NewUiHook(view)
   151  	h.resources = map[string]uiResourceState{
   152  		"test_instance.foo": {
   153  			Op:    uiResourceDestroy,
   154  			Start: time.Now(),
   155  		},
   156  	}
   157  
   158  	addr := addrs.Resource{
   159  		Mode: addrs.ManagedResourceMode,
   160  		Type: "test_instance",
   161  		Name: "foo",
   162  	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
   163  
   164  	priorState := cty.ObjectVal(map[string]cty.Value{
   165  		"id": cty.StringVal("abc123"),
   166  		"verbs": cty.ListVal([]cty.Value{
   167  			cty.StringVal("boop"),
   168  		}),
   169  	})
   170  	plannedNewState := cty.NullVal(cty.Object(map[string]cty.Type{
   171  		"id":    cty.String,
   172  		"verbs": cty.List(cty.String),
   173  	}))
   174  
   175  	key := states.NewDeposedKey()
   176  	action, err := h.PreApply(addr, key, plans.Delete, priorState, plannedNewState)
   177  	if err != nil {
   178  		t.Fatal(err)
   179  	}
   180  	if action != terraform.HookActionContinue {
   181  		t.Fatalf("Expected hook to continue, given: %#v", action)
   182  	}
   183  
   184  	// stop the background writer
   185  	uiState := h.resources[addr.String()]
   186  	close(uiState.DoneCh)
   187  	<-uiState.done
   188  
   189  	result := done(t)
   190  	expectedOutput := fmt.Sprintf("test_instance.foo (deposed object %s): Destroying... [id=abc123]\n", key)
   191  	output := result.Stdout()
   192  	if output != expectedOutput {
   193  		t.Fatalf("Output didn't match.\nExpected: %q\nGiven: %q", expectedOutput, output)
   194  	}
   195  
   196  	expectedErrOutput := ""
   197  	errOutput := result.Stderr()
   198  	if errOutput != expectedErrOutput {
   199  		t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput)
   200  	}
   201  }
   202  
   203  // Verify that colorize is called on format strings, not user input, by adding
   204  // valid color codes as resource names and IDs.
   205  func TestUiHookPostApply_colorInterpolation(t *testing.T) {
   206  	streams, done := terminal.StreamsForTesting(t)
   207  	view := NewView(streams)
   208  	view.Configure(&arguments.View{NoColor: false})
   209  	h := NewUiHook(view)
   210  	h.resources = map[string]uiResourceState{
   211  		"test_instance.foo[\"[red]\"]": {
   212  			Op:    uiResourceCreate,
   213  			Start: time.Now(),
   214  		},
   215  	}
   216  
   217  	addr := addrs.Resource{
   218  		Mode: addrs.ManagedResourceMode,
   219  		Type: "test_instance",
   220  		Name: "foo",
   221  	}.Instance(addrs.StringKey("[red]")).Absolute(addrs.RootModuleInstance)
   222  
   223  	newState := cty.ObjectVal(map[string]cty.Value{
   224  		"id": cty.StringVal("[blue]"),
   225  	})
   226  
   227  	action, err := h.PostApply(addr, states.CurrentGen, newState, nil)
   228  	if err != nil {
   229  		t.Fatal(err)
   230  	}
   231  	if action != terraform.HookActionContinue {
   232  		t.Fatalf("Expected hook to continue, given: %#v", action)
   233  	}
   234  	result := done(t)
   235  
   236  	reset := "\x1b[0m"
   237  	bold := "\x1b[1m"
   238  	wantPrefix := reset + bold + `test_instance.foo["[red]"]: Creation complete after`
   239  	wantSuffix := "[id=[blue]]" + reset + "\n"
   240  	output := result.Stdout()
   241  
   242  	if !strings.HasPrefix(output, wantPrefix) {
   243  		t.Fatalf("wrong output prefix\n got: %#v\nwant: %#v", output, wantPrefix)
   244  	}
   245  
   246  	if !strings.HasSuffix(output, wantSuffix) {
   247  		t.Fatalf("wrong output suffix\n got: %#v\nwant: %#v", output, wantSuffix)
   248  	}
   249  
   250  	expectedErrOutput := ""
   251  	errOutput := result.Stderr()
   252  	if errOutput != expectedErrOutput {
   253  		t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput)
   254  	}
   255  }
   256  
   257  // Test that the PostApply hook renders a total time.
   258  func TestUiHookPostApply_emptyState(t *testing.T) {
   259  	streams, done := terminal.StreamsForTesting(t)
   260  	view := NewView(streams)
   261  	h := NewUiHook(view)
   262  	h.resources = map[string]uiResourceState{
   263  		"data.google_compute_zones.available": {
   264  			Op:    uiResourceDestroy,
   265  			Start: time.Now(),
   266  		},
   267  	}
   268  
   269  	addr := addrs.Resource{
   270  		Mode: addrs.DataResourceMode,
   271  		Type: "google_compute_zones",
   272  		Name: "available",
   273  	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
   274  
   275  	newState := cty.NullVal(cty.Object(map[string]cty.Type{
   276  		"id":    cty.String,
   277  		"names": cty.List(cty.String),
   278  	}))
   279  
   280  	action, err := h.PostApply(addr, states.CurrentGen, newState, nil)
   281  	if err != nil {
   282  		t.Fatal(err)
   283  	}
   284  	if action != terraform.HookActionContinue {
   285  		t.Fatalf("Expected hook to continue, given: %#v", action)
   286  	}
   287  	result := done(t)
   288  
   289  	expectedRegexp := "^data.google_compute_zones.available: Destruction complete after -?[a-z0-9µ.]+\n$"
   290  	output := result.Stdout()
   291  	if matched, _ := regexp.MatchString(expectedRegexp, output); !matched {
   292  		t.Fatalf("Output didn't match regexp.\nExpected: %q\nGiven: %q", expectedRegexp, output)
   293  	}
   294  
   295  	expectedErrOutput := ""
   296  	errOutput := result.Stderr()
   297  	if errOutput != expectedErrOutput {
   298  		t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput)
   299  	}
   300  }
   301  
   302  func TestPreProvisionInstanceStep(t *testing.T) {
   303  	streams, done := terminal.StreamsForTesting(t)
   304  	view := NewView(streams)
   305  	h := NewUiHook(view)
   306  
   307  	addr := addrs.Resource{
   308  		Mode: addrs.ManagedResourceMode,
   309  		Type: "test_instance",
   310  		Name: "foo",
   311  	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
   312  
   313  	action, err := h.PreProvisionInstanceStep(addr, "local-exec")
   314  	if err != nil {
   315  		t.Fatal(err)
   316  	}
   317  	if action != terraform.HookActionContinue {
   318  		t.Fatalf("Expected hook to continue, given: %#v", action)
   319  	}
   320  	result := done(t)
   321  
   322  	if got, want := result.Stdout(), "test_instance.foo: Provisioning with 'local-exec'...\n"; got != want {
   323  		t.Fatalf("unexpected output\n got: %q\nwant: %q", got, want)
   324  	}
   325  }
   326  
   327  // Test ProvisionOutput, including lots of edge cases for the output
   328  // whitespace/line ending logic.
   329  func TestProvisionOutput(t *testing.T) {
   330  	addr := addrs.Resource{
   331  		Mode: addrs.ManagedResourceMode,
   332  		Type: "test_instance",
   333  		Name: "foo",
   334  	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
   335  
   336  	testCases := map[string]struct {
   337  		provisioner string
   338  		input       string
   339  		wantOutput  string
   340  	}{
   341  		"single line": {
   342  			"local-exec",
   343  			"foo\n",
   344  			"test_instance.foo (local-exec): foo\n",
   345  		},
   346  		"multiple lines": {
   347  			"x",
   348  			`foo
   349  bar
   350  baz
   351  `,
   352  			`test_instance.foo (x): foo
   353  test_instance.foo (x): bar
   354  test_instance.foo (x): baz
   355  `,
   356  		},
   357  		"trailing whitespace": {
   358  			"x",
   359  			"foo                  \nbar\n",
   360  			"test_instance.foo (x): foo\ntest_instance.foo (x): bar\n",
   361  		},
   362  		"blank lines": {
   363  			"x",
   364  			"foo\n\nbar\n\n\nbaz\n",
   365  			`test_instance.foo (x): foo
   366  test_instance.foo (x): bar
   367  test_instance.foo (x): baz
   368  `,
   369  		},
   370  		"no final newline": {
   371  			"x",
   372  			`foo
   373  bar`,
   374  			`test_instance.foo (x): foo
   375  test_instance.foo (x): bar
   376  `,
   377  		},
   378  		"CR, no LF": {
   379  			"MacOS 9?",
   380  			"foo\rbar\r",
   381  			`test_instance.foo (MacOS 9?): foo
   382  test_instance.foo (MacOS 9?): bar
   383  `,
   384  		},
   385  		"CRLF": {
   386  			"winrm",
   387  			"foo\r\nbar\r\n",
   388  			`test_instance.foo (winrm): foo
   389  test_instance.foo (winrm): bar
   390  `,
   391  		},
   392  	}
   393  
   394  	for name, tc := range testCases {
   395  		t.Run(name, func(t *testing.T) {
   396  			streams, done := terminal.StreamsForTesting(t)
   397  			view := NewView(streams)
   398  			h := NewUiHook(view)
   399  
   400  			h.ProvisionOutput(addr, tc.provisioner, tc.input)
   401  			result := done(t)
   402  
   403  			if got := result.Stdout(); got != tc.wantOutput {
   404  				t.Fatalf("unexpected output\n got: %q\nwant: %q", got, tc.wantOutput)
   405  			}
   406  		})
   407  	}
   408  }
   409  
   410  // Test the PreRefresh hook in the normal path where the resource exists with
   411  // an ID key and value in the state.
   412  func TestPreRefresh(t *testing.T) {
   413  	streams, done := terminal.StreamsForTesting(t)
   414  	view := NewView(streams)
   415  	h := NewUiHook(view)
   416  
   417  	addr := addrs.Resource{
   418  		Mode: addrs.ManagedResourceMode,
   419  		Type: "test_instance",
   420  		Name: "foo",
   421  	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
   422  
   423  	priorState := cty.ObjectVal(map[string]cty.Value{
   424  		"id":  cty.StringVal("test"),
   425  		"bar": cty.ListValEmpty(cty.String),
   426  	})
   427  
   428  	action, err := h.PreRefresh(addr, states.CurrentGen, priorState)
   429  
   430  	if err != nil {
   431  		t.Fatal(err)
   432  	}
   433  	if action != terraform.HookActionContinue {
   434  		t.Fatalf("Expected hook to continue, given: %#v", action)
   435  	}
   436  	result := done(t)
   437  
   438  	if got, want := result.Stdout(), "test_instance.foo: Refreshing state... [id=test]\n"; got != want {
   439  		t.Fatalf("unexpected output\n got: %q\nwant: %q", got, want)
   440  	}
   441  }
   442  
   443  // Test that PreRefresh still works if no ID key and value can be determined
   444  // from state.
   445  func TestPreRefresh_noID(t *testing.T) {
   446  	streams, done := terminal.StreamsForTesting(t)
   447  	view := NewView(streams)
   448  	h := NewUiHook(view)
   449  
   450  	addr := addrs.Resource{
   451  		Mode: addrs.ManagedResourceMode,
   452  		Type: "test_instance",
   453  		Name: "foo",
   454  	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
   455  
   456  	priorState := cty.ObjectVal(map[string]cty.Value{
   457  		"bar": cty.ListValEmpty(cty.String),
   458  	})
   459  
   460  	action, err := h.PreRefresh(addr, states.CurrentGen, priorState)
   461  
   462  	if err != nil {
   463  		t.Fatal(err)
   464  	}
   465  	if action != terraform.HookActionContinue {
   466  		t.Fatalf("Expected hook to continue, given: %#v", action)
   467  	}
   468  	result := done(t)
   469  
   470  	if got, want := result.Stdout(), "test_instance.foo: Refreshing state...\n"; got != want {
   471  		t.Fatalf("unexpected output\n got: %q\nwant: %q", got, want)
   472  	}
   473  }
   474  
   475  // Test the very simple PreImportState hook.
   476  func TestPreImportState(t *testing.T) {
   477  	streams, done := terminal.StreamsForTesting(t)
   478  	view := NewView(streams)
   479  	h := NewUiHook(view)
   480  
   481  	addr := addrs.Resource{
   482  		Mode: addrs.ManagedResourceMode,
   483  		Type: "test_instance",
   484  		Name: "foo",
   485  	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
   486  
   487  	action, err := h.PreImportState(addr, "test")
   488  
   489  	if err != nil {
   490  		t.Fatal(err)
   491  	}
   492  	if action != terraform.HookActionContinue {
   493  		t.Fatalf("Expected hook to continue, given: %#v", action)
   494  	}
   495  	result := done(t)
   496  
   497  	if got, want := result.Stdout(), "test_instance.foo: Importing from ID \"test\"...\n"; got != want {
   498  		t.Fatalf("unexpected output\n got: %q\nwant: %q", got, want)
   499  	}
   500  }
   501  
   502  // Test the PostImportState UI hook. Again, this hook behaviour seems odd to
   503  // me (see below), so please don't consider these tests as justification for
   504  // keeping this behaviour.
   505  func TestPostImportState(t *testing.T) {
   506  	streams, done := terminal.StreamsForTesting(t)
   507  	view := NewView(streams)
   508  	h := NewUiHook(view)
   509  
   510  	addr := addrs.Resource{
   511  		Mode: addrs.ManagedResourceMode,
   512  		Type: "test_instance",
   513  		Name: "foo",
   514  	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
   515  
   516  	// The "Prepared [...] for import" lines display the type name of each of
   517  	// the imported resources passed to the hook. I'm not sure how it's
   518  	// possible for an import to result in a different resource type name than
   519  	// the target address, but the hook works like this so we're covering it.
   520  	imported := []providers.ImportedResource{
   521  		{
   522  			TypeName: "test_some_instance",
   523  			State: cty.ObjectVal(map[string]cty.Value{
   524  				"id": cty.StringVal("test"),
   525  			}),
   526  		},
   527  		{
   528  			TypeName: "test_other_instance",
   529  			State: cty.ObjectVal(map[string]cty.Value{
   530  				"id": cty.StringVal("test"),
   531  			}),
   532  		},
   533  	}
   534  
   535  	action, err := h.PostImportState(addr, imported)
   536  
   537  	if err != nil {
   538  		t.Fatal(err)
   539  	}
   540  	if action != terraform.HookActionContinue {
   541  		t.Fatalf("Expected hook to continue, given: %#v", action)
   542  	}
   543  	result := done(t)
   544  
   545  	want := `test_instance.foo: Import prepared!
   546    Prepared test_some_instance for import
   547    Prepared test_other_instance for import
   548  `
   549  	if got := result.Stdout(); got != want {
   550  		t.Fatalf("unexpected output\n got: %q\nwant: %q", got, want)
   551  	}
   552  }
   553  
   554  func TestTruncateId(t *testing.T) {
   555  	testCases := []struct {
   556  		Input    string
   557  		Expected string
   558  		MaxLen   int
   559  	}{
   560  		{
   561  			Input:    "Hello world",
   562  			Expected: "H...d",
   563  			MaxLen:   3,
   564  		},
   565  		{
   566  			Input:    "Hello world",
   567  			Expected: "H...d",
   568  			MaxLen:   5,
   569  		},
   570  		{
   571  			Input:    "Hello world",
   572  			Expected: "He...d",
   573  			MaxLen:   6,
   574  		},
   575  		{
   576  			Input:    "Hello world",
   577  			Expected: "He...ld",
   578  			MaxLen:   7,
   579  		},
   580  		{
   581  			Input:    "Hello world",
   582  			Expected: "Hel...ld",
   583  			MaxLen:   8,
   584  		},
   585  		{
   586  			Input:    "Hello world",
   587  			Expected: "Hel...rld",
   588  			MaxLen:   9,
   589  		},
   590  		{
   591  			Input:    "Hello world",
   592  			Expected: "Hell...rld",
   593  			MaxLen:   10,
   594  		},
   595  		{
   596  			Input:    "Hello world",
   597  			Expected: "Hello world",
   598  			MaxLen:   11,
   599  		},
   600  		{
   601  			Input:    "Hello world",
   602  			Expected: "Hello world",
   603  			MaxLen:   12,
   604  		},
   605  		{
   606  			Input:    "あいうえおかきくけこさ",
   607  			Expected: "あ...さ",
   608  			MaxLen:   3,
   609  		},
   610  		{
   611  			Input:    "あいうえおかきくけこさ",
   612  			Expected: "あ...さ",
   613  			MaxLen:   5,
   614  		},
   615  		{
   616  			Input:    "あいうえおかきくけこさ",
   617  			Expected: "あい...さ",
   618  			MaxLen:   6,
   619  		},
   620  		{
   621  			Input:    "あいうえおかきくけこさ",
   622  			Expected: "あい...こさ",
   623  			MaxLen:   7,
   624  		},
   625  		{
   626  			Input:    "あいうえおかきくけこさ",
   627  			Expected: "あいう...こさ",
   628  			MaxLen:   8,
   629  		},
   630  		{
   631  			Input:    "あいうえおかきくけこさ",
   632  			Expected: "あいう...けこさ",
   633  			MaxLen:   9,
   634  		},
   635  		{
   636  			Input:    "あいうえおかきくけこさ",
   637  			Expected: "あいうえ...けこさ",
   638  			MaxLen:   10,
   639  		},
   640  		{
   641  			Input:    "あいうえおかきくけこさ",
   642  			Expected: "あいうえおかきくけこさ",
   643  			MaxLen:   11,
   644  		},
   645  		{
   646  			Input:    "あいうえおかきくけこさ",
   647  			Expected: "あいうえおかきくけこさ",
   648  			MaxLen:   12,
   649  		},
   650  	}
   651  	for i, tc := range testCases {
   652  		testName := fmt.Sprintf("%d", i)
   653  		t.Run(testName, func(t *testing.T) {
   654  			out := truncateId(tc.Input, tc.MaxLen)
   655  			if out != tc.Expected {
   656  				t.Fatalf("Expected %q to be shortened to %d as %q (given: %q)",
   657  					tc.Input, tc.MaxLen, tc.Expected, out)
   658  			}
   659  		})
   660  	}
   661  }