99 "os"
1010 "os/exec"
1111 "path/filepath"
12+ "strings"
1213 "testing"
1314 "time"
1415
@@ -17,8 +18,6 @@ import (
1718 "github.com/stretchr/testify/require"
1819)
1920
20- // todo test get history
21-
2221func TestExecutor_WorkingDir_Current (t * testing.T ) {
2322 var b bytes.Buffer
2423 ex := makeTestExecutor (t )
@@ -28,7 +27,8 @@ func TestExecutor_WorkingDir_Current(t *testing.T) {
2827
2928 err := ex .execJob (context .TODO (), io .Writer (& b ))
3029 assert .NoError (t , err )
31- assert .Equal (t , ex .workingDir + "\r \n " , b .String ())
30+ // Normalize line endings for cross-platform compatibility.
31+ assert .Equal (t , ex .workingDir + "\n " , strings .ReplaceAll (b .String (), "\r \n " , "\n " ))
3232}
3333
3434func TestExecutor_WorkingDir_Nil (t * testing.T ) {
@@ -39,7 +39,7 @@ func TestExecutor_WorkingDir_Nil(t *testing.T) {
3939
4040 err := ex .execJob (context .TODO (), io .Writer (& b ))
4141 assert .NoError (t , err )
42- assert .Equal (t , ex .workingDir + "\r \ n " , b .String ())
42+ assert .Equal (t , ex .workingDir + "\n " , strings . ReplaceAll ( b .String (), " \r \n " , " \n " ))
4343}
4444
4545func TestExecutor_HomeDir (t * testing.T ) {
@@ -49,7 +49,7 @@ func TestExecutor_HomeDir(t *testing.T) {
4949
5050 err := ex .execJob (context .TODO (), io .Writer (& b ))
5151 assert .NoError (t , err )
52- assert .Equal (t , ex .homeDir + "\r \ n " , b .String ())
52+ assert .Equal (t , ex .homeDir + "\n " , strings . ReplaceAll ( b .String (), " \r \n " , " \n " ))
5353}
5454
5555func TestExecutor_NonZeroExit (t * testing.T ) {
@@ -61,7 +61,7 @@ func TestExecutor_NonZeroExit(t *testing.T) {
6161 assert .Error (t , err )
6262 assert .NotEmpty (t , ex .jobStateHistory )
6363 exitStatus := ex .jobStateHistory [len (ex .jobStateHistory )- 1 ].ExitStatus
64- assert .NotNil (t , exitStatus , ex . jobStateHistory )
64+ assert .NotNil (t , exitStatus )
6565 assert .Equal (t , 100 , * exitStatus )
6666}
6767
@@ -96,7 +96,7 @@ func TestExecutor_LocalRepo(t *testing.T) {
9696
9797 err = ex .execJob (context .TODO (), io .Writer (& b ))
9898 assert .NoError (t , err )
99- assert .Equal (t , "bar\r \ n " , b .String ())
99+ assert .Equal (t , "bar\n " , strings . ReplaceAll ( b .String (), " \r \n " , " \n " ))
100100}
101101
102102func TestExecutor_Recover (t * testing.T ) {
@@ -148,8 +148,8 @@ func TestExecutor_RemoteRepo(t *testing.T) {
148148
149149 err = ex .execJob (context .TODO (), io .Writer (& b ))
150150 assert .NoError (t , err )
151- expected := fmt .Sprintf ("%s\r \ n %s\r \ n %s\r \n " , ex .getRepoData ().RepoHash , ex .getRepoData ().RepoConfigName , ex .getRepoData ().RepoConfigEmail )
152- assert .Equal (t , expected , b .String ())
151+ expected := fmt .Sprintf ("%s\n %s\n %s\n " , ex .getRepoData ().RepoHash , ex .getRepoData ().RepoConfigName , ex .getRepoData ().RepoConfigEmail )
152+ assert .Equal (t , expected , strings . ReplaceAll ( b .String (), " \r \n " , " \n " ))
153153}
154154
155155/* Helpers */
@@ -236,3 +236,98 @@ func TestWriteDstackProfile(t *testing.T) {
236236 assert .Equal (t , value , string (out ))
237237 }
238238}
239+
240+ func TestExecutor_Logs (t * testing.T ) {
241+ var b bytes.Buffer
242+ ex := makeTestExecutor (t )
243+ // Use printf to generate ANSI control codes.
244+ // \033[31m = red text, \033[1;32m = bold green text, \033[0m = reset
245+ ex .jobSpec .Commands = append (ex .jobSpec .Commands , "printf '\\ 033[31mRed Hello World\\ 033[0m\\ n' && printf '\\ 033[1;32mBold Green Line 2\\ 033[0m\\ n' && printf 'Line 3\\ n'" )
246+
247+ err := ex .execJob (context .TODO (), io .Writer (& b ))
248+ assert .NoError (t , err )
249+
250+ logHistory := ex .GetHistory (0 ).JobLogs
251+ assert .NotEmpty (t , logHistory )
252+
253+ logString := combineLogMessages (logHistory )
254+ normalizedLogString := strings .ReplaceAll (logString , "\r \n " , "\n " )
255+
256+ expectedOutput := "Red Hello World\n Bold Green Line 2\n Line 3\n "
257+ assert .Equal (t , expectedOutput , normalizedLogString , "Should strip ANSI codes from regular logs" )
258+
259+ // Verify timestamps are in order
260+ assert .Greater (t , len (logHistory ), 0 )
261+ for i := 1 ; i < len (logHistory ); i ++ {
262+ assert .GreaterOrEqual (t , logHistory [i ].Timestamp , logHistory [i - 1 ].Timestamp )
263+ }
264+ }
265+
266+ func TestExecutor_LogsWithErrors (t * testing.T ) {
267+ var b bytes.Buffer
268+ ex := makeTestExecutor (t )
269+ ex .jobSpec .Commands = append (ex .jobSpec .Commands , "echo 'Success message' && echo 'Error message' >&2 && exit 1" )
270+
271+ err := ex .execJob (context .TODO (), io .Writer (& b ))
272+ assert .Error (t , err )
273+
274+ logHistory := ex .GetHistory (0 ).JobLogs
275+ assert .NotEmpty (t , logHistory )
276+
277+ logString := combineLogMessages (logHistory )
278+ normalizedLogString := strings .ReplaceAll (logString , "\r \n " , "\n " )
279+
280+ expectedOutput := "Success message\n Error message\n "
281+ assert .Equal (t , expectedOutput , normalizedLogString )
282+ }
283+
284+ func TestExecutor_LogsAnsiCodeHandling (t * testing.T ) {
285+ var b bytes.Buffer
286+ ex := makeTestExecutor (t )
287+
288+ // Test a variety of ANSI escape sequences on stdout and stderr.
289+ cmd := "printf '\\ 033[31mRed\\ 033[0m \\ 033[32mGreen\\ 033[0m\\ n' && " +
290+ "printf '\\ 033[1mBold\\ 033[0m \\ 033[4mUnderline\\ 033[0m\\ n' && " +
291+ "printf '\\ 033[s\\ 033[uPlain text\\ n' >&2"
292+
293+ ex .jobSpec .Commands = append (ex .jobSpec .Commands , cmd )
294+
295+ err := ex .execJob (context .TODO (), io .Writer (& b ))
296+ assert .NoError (t , err )
297+
298+ // 1. Check WebSocket logs, which should preserve ANSI codes.
299+ wsLogHistory := ex .GetJobWsLogsHistory ()
300+ assert .NotEmpty (t , wsLogHistory )
301+ wsLogString := combineLogMessages (wsLogHistory )
302+ normalizedWsLogString := strings .ReplaceAll (wsLogString , "\r \n " , "\n " )
303+
304+ expectedWsOutput := "\033 [31mRed\033 [0m \033 [32mGreen\033 [0m\n " +
305+ "\033 [1mBold\033 [0m \033 [4mUnderline\033 [0m\n " +
306+ "\033 [s\033 [uPlain text\n "
307+ assert .Equal (t , expectedWsOutput , normalizedWsLogString , "Websocket logs should preserve ANSI codes" )
308+
309+ // 2. Check regular job logs, which should have ANSI codes stripped.
310+ regularLogHistory := ex .GetHistory (0 ).JobLogs
311+ assert .NotEmpty (t , regularLogHistory )
312+ regularLogString := combineLogMessages (regularLogHistory )
313+ normalizedRegularLogString := strings .ReplaceAll (regularLogString , "\r \n " , "\n " )
314+
315+ expectedRegularOutput := "Red Green\n " +
316+ "Bold Underline\n " +
317+ "Plain text\n "
318+ assert .Equal (t , expectedRegularOutput , normalizedRegularLogString , "Regular logs should have ANSI codes stripped" )
319+
320+ // Verify timestamps are ordered for both log types.
321+ assert .Greater (t , len (wsLogHistory ), 0 )
322+ for i := 1 ; i < len (wsLogHistory ); i ++ {
323+ assert .GreaterOrEqual (t , wsLogHistory [i ].Timestamp , wsLogHistory [i - 1 ].Timestamp )
324+ }
325+ }
326+
327+ func combineLogMessages (logHistory []schemas.LogEvent ) string {
328+ var logOutput bytes.Buffer
329+ for _ , logEvent := range logHistory {
330+ logOutput .Write (logEvent .Message )
331+ }
332+ return logOutput .String ()
333+ }
0 commit comments