diff --git a/sdk/trace/span_limits_test.go b/sdk/trace/span_limits_test.go index bd57fa662b9..74f8997c8b0 100644 --- a/sdk/trace/span_limits_test.go +++ b/sdk/trace/span_limits_test.go @@ -183,7 +183,7 @@ func TestSpanLimits(t *testing.T) { // Ensure string and string slice attributes are truncated. assert.Contains(t, attrs, attribute.String("string", "ab")) assert.Contains(t, attrs, attribute.StringSlice("stringSlice", []string{"ab", "de"})) - assert.Contains(t, attrs, attribute.String("euro", "")) + assert.Contains(t, attrs, attribute.String("euro", "€")) limits.AttributeValueLengthLimit = 0 attrs = testSpanLimits(t, limits).Attributes() diff --git a/sdk/trace/span_test.go b/sdk/trace/span_test.go index 38bf2b2e8a9..b645613743c 100644 --- a/sdk/trace/span_test.go +++ b/sdk/trace/span_test.go @@ -201,37 +201,135 @@ func TestTruncateAttr(t *testing.T) { attr: strSliceAttr, want: strSliceAttr, }, + } + + for _, test := range tests { + name := fmt.Sprintf("%s->%s(limit:%d)", test.attr.Key, test.attr.Value.Emit(), test.limit) + t.Run(name, func(t *testing.T) { + assert.Equal(t, test.want, truncateAttr(test.limit, test.attr)) + }) + } +} + +func TestTruncate(t *testing.T) { + type group struct { + limit int + input string + expected string + } + + tests := []struct { + name string + groups []group + }{ + // Edge case: limit is negative, no truncation should occur { - // This tests the ordinary safeTruncate(). - limit: 10, - attr: attribute.String(key, "€€€€"), // 3 bytes each - want: attribute.String(key, "€€€"), + name: "NoTruncation", + groups: []group{ + {-1, "No truncation!", "No truncation!"}, + }, }, + + // Edge case: string is already shorter than the limit, no truncation + // should occur { - // This tests truncation with an invalid UTF-8 input. - // - // Note that after removing the invalid rune, - // the string is over length and still has to - // be truncated on a code point boundary. - limit: 10, - attr: attribute.String(key, "€"[0:2]+"hello€€"), // corrupted first rune, then over limit - want: attribute.String(key, "hello€"), + name: "ShortText", + groups: []group{ + {10, "Short text", "Short text"}, + {15, "Short text", "Short text"}, + {100, "Short text", "Short text"}, + }, }, + + // Edge case: truncation happens with ASCII characters only { - // This tests the fallback to invalidTruncate() - // where after validation the string does not require - // truncation. - limit: 6, - attr: attribute.String(key, "€"[0:2]+"hello"), // corrupted first rune, then not over limit - want: attribute.String(key, "hello"), + name: "ASCIIOnly", + groups: []group{ + {1, "Hello World!", "H"}, + {5, "Hello World!", "Hello"}, + {12, "Hello World!", "Hello World!"}, + }, + }, + + // Truncation including multi-byte characters (UTF-8) + { + name: "ValidUTF-8", + groups: []group{ + {7, "Hello, 世界", "Hello, "}, + {8, "Hello, 世界", "Hello, 世"}, + {2, "こんにちは", "こん"}, + {3, "こんにちは", "こんに"}, + {5, "こんにちは", "こんにちは"}, + {12, "こんにちは", "こんにちは"}, + }, + }, + + // Truncation with invalid UTF-8 characters + { + name: "InvalidUTF-8", + groups: []group{ + {11, "Invalid\x80text", "Invalidtext"}, + // Do not modify invalid text if equal to limit. + {11, "Valid text\x80", "Valid text\x80"}, + // Do not modify invalid text if under limit. + {15, "Valid text\x80", "Valid text\x80"}, + {5, "Hello\x80World", "Hello"}, + {11, "Hello\x80World\x80!", "HelloWorld!"}, + {15, "Hello\x80World\x80Test", "HelloWorldTest"}, + {15, "Hello\x80\x80\x80World\x80Test", "HelloWorldTest"}, + {15, "\x80\x80\x80Hello\x80\x80\x80World\x80Test\x80\x80", "HelloWorldTest"}, + }, + }, + + // Truncation with mixed validn and invalid UTF-8 characters + { + name: "MixedUTF-8", + groups: []group{ + {6, "€"[0:2] + "hello€€", "hello€"}, + {6, "€" + "€"[0:2] + "hello", "€hello"}, + {11, "Valid text\x80📜", "Valid text📜"}, + {11, "Valid text📜\x80", "Valid text📜"}, + {14, "😊 Hello\x80World🌍🚀", "😊 HelloWorld🌍🚀"}, + {14, "😊\x80 Hello\x80World🌍🚀", "😊 HelloWorld🌍🚀"}, + {14, "😊\x80 Hello\x80World🌍\x80🚀", "😊 HelloWorld🌍🚀"}, + {14, "😊\x80 Hello\x80World🌍\x80🚀\x80", "😊 HelloWorld🌍🚀"}, + {14, "\x80😊\x80 Hello\x80World🌍\x80🚀\x80", "😊 HelloWorld🌍🚀"}, + }, + }, + + // Edge case: empty string, should return empty string + { + name: "Empty", + groups: []group{ + {5, "", ""}, + }, + }, + + // Edge case: limit is 0, should return an empty string + { + name: "Zero", + groups: []group{ + {0, "Some text", ""}, + {0, "", ""}, + }, }, } - for _, test := range tests { - name := fmt.Sprintf("%s->%s(limit:%d)", test.attr.Key, test.attr.Value.Emit(), test.limit) - t.Run(name, func(t *testing.T) { - assert.Equal(t, test.want, truncateAttr(test.limit, test.attr)) - }) + for _, tt := range tests { + for _, g := range tt.groups { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got := safeTruncate(g.input, g.limit) + assert.Equalf( + t, g.expected, got, + "input: %q([]rune%v))\ngot: %q([]rune%v)\nwant %q([]rune%v)", + g.input, []rune(g.input), + got, []rune(got), + g.expected, []rune(g.expected), + ) + }) + } } }