hishtory/client/lib/lib_test.go

562 lines
20 KiB
Go

package lib
import (
"os"
"os/user"
"path"
"reflect"
"strings"
"testing"
"time"
"github.com/ddworken/hishtory/client/data"
"github.com/ddworken/hishtory/client/hctx"
"github.com/ddworken/hishtory/shared"
"github.com/ddworken/hishtory/shared/testutils"
)
func TestSetup(t *testing.T) {
defer testutils.BackupAndRestore(t)()
defer testutils.RunTestServer()()
homedir, err := os.UserHomeDir()
testutils.Check(t, err)
if _, err := os.Stat(path.Join(homedir, data.GetHishtoryPath(), data.CONFIG_PATH)); err == nil {
t.Fatalf("hishtory secret file already exists!")
}
testutils.Check(t, Setup("", false))
if _, err := os.Stat(path.Join(homedir, data.GetHishtoryPath(), data.CONFIG_PATH)); err != nil {
t.Fatalf("hishtory secret file does not exist after Setup()!")
}
data, err := os.ReadFile(path.Join(homedir, data.GetHishtoryPath(), data.CONFIG_PATH))
testutils.Check(t, err)
if len(data) < 10 {
t.Fatalf("hishtory secret has unexpected length: %d", len(data))
}
config := hctx.GetConf(hctx.MakeContext())
if config.IsOffline != false {
t.Fatalf("hishtory config should have been offline")
}
}
func TestSetupOffline(t *testing.T) {
defer testutils.BackupAndRestore(t)()
defer testutils.RunTestServer()()
homedir, err := os.UserHomeDir()
testutils.Check(t, err)
if _, err := os.Stat(path.Join(homedir, data.GetHishtoryPath(), data.CONFIG_PATH)); err == nil {
t.Fatalf("hishtory secret file already exists!")
}
testutils.Check(t, Setup("", true))
if _, err := os.Stat(path.Join(homedir, data.GetHishtoryPath(), data.CONFIG_PATH)); err != nil {
t.Fatalf("hishtory secret file does not exist after Setup()!")
}
data, err := os.ReadFile(path.Join(homedir, data.GetHishtoryPath(), data.CONFIG_PATH))
testutils.Check(t, err)
if len(data) < 10 {
t.Fatalf("hishtory secret has unexpected length: %d", len(data))
}
config := hctx.GetConf(hctx.MakeContext())
if config.IsOffline != true {
t.Fatalf("hishtory config should have been offline, actual=%#v", string(data))
}
}
func TestBuildHistoryEntry(t *testing.T) {
defer testutils.BackupAndRestore(t)()
defer testutils.RunTestServer()()
testutils.Check(t, Setup("", false))
// Test building an actual entry for bash
entry, err := BuildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "bash", "120", " 123 ls /foo ", "1641774958"})
testutils.Check(t, err)
if entry.ExitCode != 120 {
t.Fatalf("history entry has unexpected exit code: %v", entry.ExitCode)
}
user, err := user.Current()
if err != nil {
t.Fatalf("failed to retrieve user: %v", err)
}
if entry.LocalUsername != user.Username {
t.Fatalf("history entry has unexpected user name: %v", entry.LocalUsername)
}
if !strings.HasPrefix(entry.CurrentWorkingDirectory, "/") && !strings.HasPrefix(entry.CurrentWorkingDirectory, "~/") {
t.Fatalf("history entry has unexpected cwd: %v", entry.CurrentWorkingDirectory)
}
if !strings.HasPrefix(entry.HomeDirectory, "/") {
t.Fatalf("history entry has unexpected home directory: %v", entry.HomeDirectory)
}
if entry.Command != "ls /foo" {
t.Fatalf("history entry has unexpected command: %v", entry.Command)
}
if !strings.HasPrefix(entry.StartTime.Format(time.RFC3339), "2022-01-09T") && !strings.HasPrefix(entry.StartTime.Format(time.RFC3339), "2022-01-10T") {
t.Fatalf("history entry has incorrect date in the start time: %v", entry.StartTime.Format(time.RFC3339))
}
if entry.StartTime.Unix() != 1641774958 {
t.Fatalf("history entry has incorrect Unix time in the start time: %v", entry.StartTime.Unix())
}
// Test building an entry for zsh
entry, err = BuildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "zsh", "120", "ls /foo\n", "1641774958"})
testutils.Check(t, err)
if entry.ExitCode != 120 {
t.Fatalf("history entry has unexpected exit code: %v", entry.ExitCode)
}
if entry.LocalUsername != user.Username {
t.Fatalf("history entry has unexpected user name: %v", entry.LocalUsername)
}
if !strings.HasPrefix(entry.CurrentWorkingDirectory, "/") && !strings.HasPrefix(entry.CurrentWorkingDirectory, "~/") {
t.Fatalf("history entry has unexpected cwd: %v", entry.CurrentWorkingDirectory)
}
if !strings.HasPrefix(entry.HomeDirectory, "/") {
t.Fatalf("history entry has unexpected home directory: %v", entry.HomeDirectory)
}
if entry.Command != "ls /foo" {
t.Fatalf("history entry has unexpected command: %v", entry.Command)
}
if !strings.HasPrefix(entry.StartTime.Format(time.RFC3339), "2022-01-09T") && !strings.HasPrefix(entry.StartTime.Format(time.RFC3339), "2022-01-10T") {
t.Fatalf("history entry has incorrect date in the start time: %v", entry.StartTime.Format(time.RFC3339))
}
if entry.StartTime.Unix() != 1641774958 {
t.Fatalf("history entry has incorrect Unix time in the start time: %v", entry.StartTime.Unix())
}
// Test building an entry for fish
entry, err = BuildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "fish", "120", "ls /foo\n", "1641774958"})
testutils.Check(t, err)
if entry.ExitCode != 120 {
t.Fatalf("history entry has unexpected exit code: %v", entry.ExitCode)
}
if entry.LocalUsername != user.Username {
t.Fatalf("history entry has unexpected user name: %v", entry.LocalUsername)
}
if !strings.HasPrefix(entry.CurrentWorkingDirectory, "/") && !strings.HasPrefix(entry.CurrentWorkingDirectory, "~/") {
t.Fatalf("history entry has unexpected cwd: %v", entry.CurrentWorkingDirectory)
}
if !strings.HasPrefix(entry.HomeDirectory, "/") {
t.Fatalf("history entry has unexpected home directory: %v", entry.HomeDirectory)
}
if entry.Command != "ls /foo" {
t.Fatalf("history entry has unexpected command: %v", entry.Command)
}
if !strings.HasPrefix(entry.StartTime.Format(time.RFC3339), "2022-01-09T") && !strings.HasPrefix(entry.StartTime.Format(time.RFC3339), "2022-01-10T") {
t.Fatalf("history entry has incorrect date in the start time: %v", entry.StartTime.Format(time.RFC3339))
}
if entry.StartTime.Unix() != 1641774958 {
t.Fatalf("history entry has incorrect Unix time in the start time: %v", entry.StartTime.Unix())
}
// Test building an entry that is empty, and thus not saved
entry, err = BuildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "zsh", "120", " \n", "1641774958"})
testutils.Check(t, err)
if entry != nil {
t.Fatalf("expected history entry to be nil")
}
}
func TestBuildHistoryEntryWithTimestampStripping(t *testing.T) {
defer testutils.BackupAndRestoreEnv("HISTTIMEFORMAT")()
defer testutils.BackupAndRestore(t)()
defer testutils.RunTestServer()()
testutils.Check(t, Setup("", false))
testcases := []struct {
input, histtimeformat, expectedCommand string
}{
{" 123 ls /foo ", "", "ls /foo"},
{" 2389 [2022-09-28 04:38:32 +0000] echo", "", "[2022-09-28 04:38:32 +0000] echo"},
{" 2389 [2022-09-28 04:38:32 +0000] echo", "[%F %T %z] ", "echo"},
}
for _, tc := range testcases {
conf := hctx.GetConf(hctx.MakeContext())
conf.LastSavedHistoryLine = ""
testutils.Check(t, hctx.SetConfig(conf))
os.Setenv("HISTTIMEFORMAT", tc.histtimeformat)
entry, err := BuildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "bash", "120", tc.input, "1641774958"})
testutils.Check(t, err)
if entry == nil {
t.Fatalf("entry is unexpectedly nil")
}
if entry.Command != tc.expectedCommand {
t.Fatalf("BuildHistoryEntry(%#v) returned %#v (expected=%#v)", tc.input, entry.Command, tc.expectedCommand)
}
}
}
func TestPersist(t *testing.T) {
defer testutils.BackupAndRestore(t)()
testutils.Check(t, hctx.InitConfig())
db := hctx.GetDb(hctx.MakeContext())
entry := testutils.MakeFakeHistoryEntry("ls ~/")
testutils.Check(t, db.Create(entry).Error)
var historyEntries []*data.HistoryEntry
result := db.Find(&historyEntries)
testutils.Check(t, result.Error)
if len(historyEntries) != 1 {
t.Fatalf("DB has %d entries, expected 1!", len(historyEntries))
}
dbEntry := historyEntries[0]
if !data.EntryEquals(entry, *dbEntry) {
t.Fatalf("DB data is different than input! \ndb =%#v \ninput=%#v", *dbEntry, entry)
}
}
func TestSearch(t *testing.T) {
defer testutils.BackupAndRestore(t)()
testutils.Check(t, hctx.InitConfig())
ctx := hctx.MakeContext()
db := hctx.GetDb(ctx)
// Insert data
entry1 := testutils.MakeFakeHistoryEntry("ls /foo")
testutils.Check(t, db.Create(entry1).Error)
entry2 := testutils.MakeFakeHistoryEntry("ls /bar")
testutils.Check(t, db.Create(entry2).Error)
// Search for data
results, err := Search(ctx, db, "ls", 5)
testutils.Check(t, err)
if len(results) != 2 {
t.Fatalf("Search() returned %d results, expected 2, results=%#v", len(results), results)
}
if !data.EntryEquals(*results[0], entry2) {
t.Fatalf("Search()[0]=%#v, expected: %#v", results[0], entry2)
}
if !data.EntryEquals(*results[1], entry1) {
t.Fatalf("Search()[0]=%#v, expected: %#v", results[1], entry1)
}
// Search but exclude bar
results, err = Search(ctx, db, "ls -bar", 5)
testutils.Check(t, err)
if len(results) != 1 {
t.Fatalf("Search() returned %d results, expected 1, results=%#v", len(results), results)
}
// Search but exclude foo
results, err = Search(ctx, db, "ls -foo", 5)
testutils.Check(t, err)
if len(results) != 1 {
t.Fatalf("Search() returned %d results, expected 1, results=%#v", len(results), results)
}
// Search but include / also
results, err = Search(ctx, db, "ls /", 5)
testutils.Check(t, err)
if len(results) != 2 {
t.Fatalf("Search() returned %d results, expected 1, results=%#v", len(results), results)
}
// Search but exclude slash
results, err = Search(ctx, db, "ls -/", 5)
testutils.Check(t, err)
if len(results) != 0 {
t.Fatalf("Search() returned %d results, expected 0, results=%#v", len(results), results)
}
// Tests for escaping
testutils.Check(t, db.Create(testutils.MakeFakeHistoryEntry("ls -baz")).Error)
results, err = Search(ctx, db, "ls", 5)
testutils.Check(t, err)
if len(results) != 3 {
t.Fatalf("Search() returned %d results, expected 3, results=%#v", len(results), results)
}
results, err = Search(ctx, db, "ls -baz", 5)
testutils.Check(t, err)
if len(results) != 2 {
t.Fatalf("Search() returned %d results, expected 2, results=%#v", len(results), results)
}
results, err = Search(ctx, db, "ls \\-baz", 5)
testutils.Check(t, err)
if len(results) != 1 {
t.Fatalf("Search() returned %d results, expected 1, results=%#v", len(results), results)
}
// A malformed search query, but we should just ignore the dash since this is a common enough thing
results, err = Search(ctx, db, "ls -", 5)
testutils.Check(t, err)
if len(results) != 3 {
t.Fatalf("Search() returned %d results, expected 3, results=%#v", len(results), results)
}
}
func TestAddToDbIfNew(t *testing.T) {
// Set up
defer testutils.BackupAndRestore(t)()
testutils.Check(t, hctx.InitConfig())
db := hctx.GetDb(hctx.MakeContext())
// Add duplicate entries
entry1 := testutils.MakeFakeHistoryEntry("ls /foo")
AddToDbIfNew(db, entry1)
AddToDbIfNew(db, entry1)
entry2 := testutils.MakeFakeHistoryEntry("ls /foo")
AddToDbIfNew(db, entry2)
AddToDbIfNew(db, entry2)
AddToDbIfNew(db, entry1)
// Check there should only be two entries
var entries []data.HistoryEntry
result := db.Find(&entries)
if result.Error != nil {
t.Fatal(result.Error)
}
if len(entries) != 2 {
t.Fatalf("entries has an incorrect length: %d, entries=%#v", len(entries), entries)
}
}
func TestParseCrossPlatformInt(t *testing.T) {
res, err := parseCrossPlatformInt("123")
testutils.Check(t, err)
if res != 123 {
t.Fatalf("failed to parse cross platform int %d", res)
}
res, err = parseCrossPlatformInt("123N")
testutils.Check(t, err)
if res != 123 {
t.Fatalf("failed to parse cross platform int %d", res)
}
}
func TestBuildRegexFromTimeFormat(t *testing.T) {
testcases := []struct {
formatString, regex string
}{
{"%Y ", "[0-9]{4} "},
{"%F %T ", "[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} "},
{"%F%T", "[0-9]{4}-[0-9]{2}-[0-9]{2}[0-9]{2}:[0-9]{2}:[0-9]{2}"},
{"%%", "%"},
{"%%%%", "%%"},
{"%%%Y", "%[0-9]{4}"},
{"%%%F%T", "%[0-9]{4}-[0-9]{2}-[0-9]{2}[0-9]{2}:[0-9]{2}:[0-9]{2}"},
}
for _, tc := range testcases {
if regex := buildRegexFromTimeFormat(tc.formatString); regex != tc.regex {
t.Fatalf("building a regex for %#v returned %#v (expected=%#v)", tc.formatString, regex, tc.regex)
}
}
}
func TestGetLastCommand(t *testing.T) {
testcases := []struct {
input, expectedOutput string
}{
{" 0 ls", "ls"},
{" 33 ls", "ls"},
{" 33 ls --aaaa foo bar ", "ls --aaaa foo bar"},
{" 2389 [2022-09-28 04:38:32 +0000] echo", "[2022-09-28 04:38:32 +0000] echo"},
}
for _, tc := range testcases {
actualOutput, err := getLastCommand(tc.input)
testutils.Check(t, err)
if actualOutput != tc.expectedOutput {
t.Fatalf("getLastCommand(%#v) returned %#v (expected=%#v)", tc.input, actualOutput, tc.expectedOutput)
}
}
}
func TestMaybeSkipBashHistTimePrefix(t *testing.T) {
defer testutils.BackupAndRestoreEnv("HISTTIMEFORMAT")()
testcases := []struct {
env, cmdLine, expected string
}{
{"%F %T ", "2019-07-12 13:02:31 sudo apt update", "sudo apt update"},
{"%F %T ", "2019-07-12 13:02:31 ls a b", "ls a b"},
{"%F %T ", "2019-07-12 13:02:31 ls a ", "ls a "},
{"%F %T ", "2019-07-12 13:02:31 ls a", "ls a"},
{"%F %T ", "2019-07-12 13:02:31 ls", "ls"},
{"%F %T ", "2019-07-12 13:02:31 ls -Slah", "ls -Slah"},
{"%F ", "2019-07-12 ls -Slah", "ls -Slah"},
{"%F ", "2019-07-12 ls -Slah", "ls -Slah"},
{"%Y", "2019ls -Slah", "ls -Slah"},
{"%Y%Y", "20192020ls -Slah", "ls -Slah"},
{"%Y%Y", "20192020ls -Slah20192020", "ls -Slah20192020"},
{"", "ls -Slah", "ls -Slah"},
{"[%F %T] ", "[2019-07-12 13:02:31] sudo apt update", "sudo apt update"},
{"[%F a %T] ", "[2019-07-12 a 13:02:31] sudo apt update", "sudo apt update"},
{"aaa ", "aaa sudo apt update", "sudo apt update"},
{"%c ", "Sun Aug 19 02:56:02 2012 sudo apt update", "sudo apt update"},
{"%c ", "Sun Aug 19 02:56:02 2012 ls", "ls"},
{"[%c] ", "[Sun Aug 19 02:56:02 2012] ls", "ls"},
{"[%c %t] ", "[Sun Aug 19 02:56:02 2012 ] ls", "ls"},
{"[%c %t]", "[Sun Aug 19 02:56:02 2012 ]ls", "ls"},
{"[%c %t]", "[Sun Aug 19 02:56:02 2012 ]foo", "foo"},
{"[%c %t", "[Sun Aug 19 02:56:02 2012 foo", "foo"},
{"[%F %T %z]", "[2022-09-28 04:17:06 +0000]foo", "foo"},
{"[%F %T %z] ", "[2022-09-28 04:17:06 +0000] foo", "foo"},
}
for _, tc := range testcases {
os.Setenv("HISTTIMEFORMAT", tc.env)
stripped, err := maybeSkipBashHistTimePrefix(tc.cmdLine)
testutils.Check(t, err)
if stripped != tc.expected {
t.Fatalf("skipping the time prefix returned %#v (expected=%#v for %#v)", stripped, tc.expected, tc.cmdLine)
}
}
}
func TestChunks(t *testing.T) {
testcases := []struct {
input []int
chunkSize int
output [][]int
}{
{[]int{1, 2, 3, 4, 5}, 2, [][]int{{1, 2}, {3, 4}, {5}}},
{[]int{1, 2, 3, 4, 5}, 3, [][]int{{1, 2, 3}, {4, 5}}},
{[]int{1, 2, 3, 4, 5}, 1, [][]int{{1}, {2}, {3}, {4}, {5}}},
{[]int{1, 2, 3, 4, 5}, 4, [][]int{{1, 2, 3, 4}, {5}}},
}
for _, tc := range testcases {
actual := shared.Chunks(tc.input, tc.chunkSize)
if !reflect.DeepEqual(actual, tc.output) {
t.Fatal("chunks failure")
}
}
}
func TestZshWeirdness(t *testing.T) {
testcases := []struct {
input string
output string
}{
{": 1666062975:0;bash", "bash"},
{": 16660:0;ls", "ls"},
{"ls", "ls"},
{"0", "0"},
{"hgffddxsdsrzsz xddfgdxfdv gdfc ghcvhgfcfg vgv", "hgffddxsdsrzsz xddfgdxfdv gdfc ghcvhgfcfg vgv"},
}
for _, tc := range testcases {
actual := stripZshWeirdness(tc.input)
if !reflect.DeepEqual(actual, tc.output) {
t.Fatalf("weirdness failure for %#v", tc.input)
}
}
}
func TestParseTimeGenerously(t *testing.T) {
ts, err := parseTimeGenerously("2006-01-02T15:04:00-08:00")
testutils.Check(t, err)
if ts.Unix() != 1136243040 {
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
}
ts, err = parseTimeGenerously("2006-01-02 T15:04:00 -08:00")
testutils.Check(t, err)
if ts.Unix() != 1136243040 {
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
}
ts, err = parseTimeGenerously("2006-01-02_T15:04:00_-08:00")
testutils.Check(t, err)
if ts.Unix() != 1136243040 {
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
}
ts, err = parseTimeGenerously("2006-01-02T15:04:00")
testutils.Check(t, err)
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 15 || ts.Minute() != 4 || ts.Second() != 0 {
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
}
ts, err = parseTimeGenerously("2006-01-02_T15:04:00")
testutils.Check(t, err)
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 15 || ts.Minute() != 4 || ts.Second() != 0 {
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
}
ts, err = parseTimeGenerously("2006-01-02_15:04:00")
testutils.Check(t, err)
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 15 || ts.Minute() != 4 || ts.Second() != 0 {
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
}
ts, err = parseTimeGenerously("2006-01-02T15:04")
testutils.Check(t, err)
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 15 || ts.Minute() != 4 || ts.Second() != 0 {
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
}
ts, err = parseTimeGenerously("2006-01-02_15:04")
testutils.Check(t, err)
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 15 || ts.Minute() != 4 || ts.Second() != 0 {
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
}
ts, err = parseTimeGenerously("2006-01-02")
testutils.Check(t, err)
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 0 || ts.Minute() != 0 || ts.Second() != 0 {
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
}
}
func TestStripBackslash(t *testing.T) {
testcases := []struct {
input string
output string
}{
{"f bar", "f bar"},
{"f \\bar", "f bar"},
{"f\\:bar", "f:bar"},
{"f\\:bar\\", "f:bar"},
{"\\f\\:bar\\", "f:bar"},
{"", ""},
{"\\\\", ""},
}
for _, tc := range testcases {
actual := stripBackslash(tc.input)
if !reflect.DeepEqual(actual, tc.output) {
t.Fatalf("unescape failure for %#v, actual=%#v", tc.input, actual)
}
}
}
func TestContainsUnescaped(t *testing.T) {
testcases := []struct {
input string
token string
expected bool
}{
{"f bar", "f", true},
{"f bar", "f bar", true},
{"f bar", "f r", false},
{"f bar", "f ", true},
{"foo:bar", ":", true},
{"foo:bar", "-", false},
{"foo\\:bar", ":", false},
{"foo\\-bar", "-", false},
{"foo\\-bar", "foo", true},
{"foo\\-bar", "bar", true},
{"foo\\-bar", "a", true},
}
for _, tc := range testcases {
actual := containsUnescaped(tc.input, tc.token)
if !reflect.DeepEqual(actual, tc.expected) {
t.Fatalf("containsUnescaped failure for containsUnescaped(%#v, %#v), actual=%#v", tc.input, tc.token, actual)
}
}
}
func TestSplitEscaped(t *testing.T) {
testcases := []struct {
input string
char rune
limit int
expected []string
}{
{"foo bar", ' ', 2, []string{"foo", "bar"}},
{"foo bar baz", ' ', 2, []string{"foo", "bar baz"}},
{"foo bar baz", ' ', 3, []string{"foo", "bar", "baz"}},
{"foo bar baz", ' ', 1, []string{"foo bar baz"}},
{"foo bar baz", ' ', -1, []string{"foo", "bar", "baz"}},
{"foo\\ bar baz", ' ', -1, []string{"foo\\ bar", "baz"}},
{"foo\\bar baz", ' ', -1, []string{"foo\\bar", "baz"}},
{"foo\\bar baz foob", ' ', 2, []string{"foo\\bar", "baz foob"}},
{"foo\\ bar\\ baz", ' ', -1, []string{"foo\\ bar\\ baz"}},
{"foo\\ bar\\ baz", ' ', -1, []string{"foo\\ bar\\ ", "baz"}},
}
for _, tc := range testcases {
actual := splitEscaped(tc.input, tc.char, tc.limit)
if !reflect.DeepEqual(actual, tc.expected) {
t.Fatalf("containsUnescaped failure for splitEscaped(%#v, %#v, %#v), actual=%#v", tc.input, string(tc.char), tc.limit, actual)
}
}
}