@@ -2,6 +2,7 @@ package status
22
33import (
44 "testing"
5+ "unicode/utf8"
56
67 "github.com/jonboulle/clockwork"
78 "github.com/openshift-pipelines/pipelines-as-code/pkg/params"
@@ -116,6 +117,141 @@ func TestCollectFailedTasksLogSnippet(t *testing.T) {
116117 }
117118}
118119
120+ func TestCollectFailedTasksLogSnippetUTF8SafeTruncation (t * testing.T ) {
121+ clock := clockwork .NewFakeClock ()
122+
123+ tests := []struct {
124+ name string
125+ podOutput string
126+ expectedTruncation bool
127+ expectedLengthRunes int // Expected rune count for non-truncated strings
128+ expectValidUTF8 bool // Should result in valid UTF-8
129+ }{
130+ {
131+ name : "short ascii text" ,
132+ podOutput : "Error: simple failure message" ,
133+ expectedTruncation : false ,
134+ expectedLengthRunes : 29 ,
135+ expectValidUTF8 : true ,
136+ },
137+ {
138+ name : "long ascii text over limit" ,
139+ podOutput : string (make ([]byte , maxErrorSnippetCharacterLimit + 100 )), // Fill with null bytes which are 1 byte each
140+ expectedTruncation : true ,
141+ expectValidUTF8 : true ,
142+ },
143+ {
144+ name : "utf8 text under limit" ,
145+ podOutput : "🚀 Error: deployment failed with émojis and spécial chars" ,
146+ expectedTruncation : false ,
147+ expectedLengthRunes : len ([]rune ("🚀 Error: deployment failed with émojis and spécial chars" )),
148+ expectValidUTF8 : true ,
149+ },
150+ {
151+ name : "utf8 text over limit" ,
152+ podOutput : "🚀 " + string (make ([]rune , maxErrorSnippetCharacterLimit )), // Create string with unicode chars (will be >65535 bytes)
153+ expectedTruncation : true ,
154+ expectValidUTF8 : true ,
155+ },
156+ {
157+ name : "mixed utf8 at boundary" ,
158+ podOutput : string (make ([]rune , maxErrorSnippetCharacterLimit + 1 )) + "🚀🔥💥" ,
159+ expectedTruncation : true ,
160+ expectValidUTF8 : true ,
161+ },
162+ }
163+
164+ for _ , tt := range tests {
165+ t .Run (tt .name , func (t * testing.T ) {
166+ pr := tektontest .MakePRCompletion (clock , "pipeline-newest" , "ns" , tektonv1 .PipelineRunReasonSuccessful .String (), nil , make (map [string ]string ), 10 )
167+ pr .Status .ChildReferences = []tektonv1.ChildStatusReference {
168+ {
169+ TypeMeta : runtime.TypeMeta {
170+ Kind : "TaskRun" ,
171+ },
172+ Name : "task1" ,
173+ PipelineTaskName : "task1" ,
174+ },
175+ }
176+
177+ taskStatus := tektonv1.TaskRunStatusFields {
178+ PodName : "task1" ,
179+ Steps : []tektonv1.StepState {
180+ {
181+ Name : "step1" ,
182+ ContainerState : corev1.ContainerState {
183+ Terminated : & corev1.ContainerStateTerminated {
184+ ExitCode : 1 ,
185+ },
186+ },
187+ },
188+ },
189+ }
190+
191+ tdata := testclient.Data {
192+ TaskRuns : []* tektonv1.TaskRun {
193+ tektontest .MakeTaskRunCompletion (clock , "task1" , "ns" , "pipeline-newest" ,
194+ map [string ]string {}, taskStatus , knativeduckv1.Conditions {
195+ {
196+ Type : knativeapi .ConditionSucceeded ,
197+ Status : corev1 .ConditionFalse ,
198+ Reason : "Failed" ,
199+ Message : "task failed" ,
200+ },
201+ },
202+ 10 ),
203+ },
204+ }
205+
206+ ctx , _ := rtesting .SetupFakeContext (t )
207+ stdata , _ := testclient .SeedTestData (t , ctx , tdata )
208+ cs := & params.Run {Clients : paramclients.Clients {
209+ Tekton : stdata .Pipeline ,
210+ }}
211+
212+ intf := & kubernetestint.KinterfaceTest {
213+ GetPodLogsOutput : map [string ]string {
214+ "task1" : tt .podOutput ,
215+ },
216+ }
217+
218+ got := CollectFailedTasksLogSnippet (ctx , cs , intf , pr , 1 )
219+ assert .Equal (t , 1 , len (got ))
220+
221+ snippet := got ["task1" ].LogSnippet
222+ byteCount := len (snippet )
223+ runeCount := len ([]rune (snippet ))
224+
225+ if tt .expectedTruncation {
226+ // Should be truncated to at most maxErrorSnippetCharacterLimit bytes
227+ if byteCount > maxErrorSnippetCharacterLimit {
228+ t .Errorf ("Expected truncated string to be at most %d bytes, got %d" ,
229+ maxErrorSnippetCharacterLimit , byteCount )
230+ }
231+
232+ // Verify the string is valid UTF-8 after truncation
233+ assert .True (t , utf8 .ValidString (snippet ), "Truncated string should be valid UTF-8" )
234+
235+ // Should be shorter than original (in bytes)
236+ assert .Less (t , byteCount , len (tt .podOutput ),
237+ "Truncated string should be shorter than original" )
238+ } else {
239+ // Should match expected length exactly (in runes for non-truncated)
240+ assert .Equal (t , tt .expectedLengthRunes , runeCount ,
241+ "Expected string length %d runes, got %d" , tt .expectedLengthRunes , runeCount )
242+
243+ // Should match original (no truncation)
244+ assert .Equal (t , tt .podOutput , snippet , "String should not be truncated" )
245+ }
246+
247+ // Always verify valid UTF-8
248+ if tt .expectValidUTF8 {
249+ assert .True (t , utf8 .ValidString (snippet ), "String should be valid UTF-8" )
250+ }
251+ })
252+ }
253+ }
254+
119255func TestGetStatusFromTaskStatusOrFromAsking (t * testing.T ) {
120256 testNS := "test"
121257 tests := []struct {
0 commit comments