From b84ef600111d1d5567f70b381be55bc266ded109 Mon Sep 17 00:00:00 2001 From: Udeshya Dhungana <075bct095.udeshya@pcampus.edu.np> Date: Thu, 26 Feb 2026 14:32:47 +0545 Subject: [PATCH 1/9] Add tests for stages 7 and 8 with error case fixtures --- Makefile | 4 +- go.mod | 4 +- internal/stage_bg1.go | 2 +- internal/stage_bg3.go | 2 +- internal/stage_bg4.go | 4 +- internal/stage_bg5.go | 4 +- internal/stage_bg6.go | 6 +- internal/stage_bg7.go | 100 ++++++++++ internal/stage_bg8.go | 174 ++++++++++++++++++ internal/stages_test.go | 7 + ...and_response_with_reaped_jobs_test_case.go | 80 ++++++++ .../jobs_builtin_response_test_case.go | 4 +- internal/test_helpers/course_definition.yml | 14 ++ .../background_jobs_job_number_not_recycled | 23 +++ .../codecrafters.yml | 1 + .../main.py | 167 +++++++++++++++++ .../your_program.sh | 3 + internal/tester_definition.go | 10 + 18 files changed, 595 insertions(+), 14 deletions(-) create mode 100644 internal/stage_bg7.go create mode 100644 internal/stage_bg8.go create mode 100644 internal/test_cases/command_response_with_reaped_jobs_test_case.go create mode 100644 internal/test_helpers/fixtures/background_jobs_job_number_not_recycled create mode 100644 internal/test_helpers/scenarios/background_jobs_job_number_not_recycled/codecrafters.yml create mode 100644 internal/test_helpers/scenarios/background_jobs_job_number_not_recycled/main.py create mode 100755 internal/test_helpers/scenarios/background_jobs_job_number_not_recycled/your_program.sh diff --git a/Makefile b/Makefile index 54f00044..42a1cee9 100644 --- a/Makefile +++ b/Makefile @@ -178,7 +178,9 @@ define _BACKGROUND_JOBS_STAGES {"slug":"jd6","tester_log_prefix":"tester::#jd6","title":"Stage#3: List a single job"}, \ {"slug":"dk5","tester_log_prefix":"tester::#dk5","title":"Stage#4: List multiple jobs"}, \ {"slug":"ma9","tester_log_prefix":"tester::#ma9","title":"Stage#5: Reaping one job using jobs"}, \ - {"slug":"rq2","tester_log_prefix":"tester::#rq2","title":"Stage#6: Reaping multiple jobs using jobs"} \ + {"slug":"rq2","tester_log_prefix":"tester::#rq2","title":"Stage#6: Reaping multiple jobs using jobs"}, \ + {"slug":"bv8","tester_log_prefix":"tester::#bv8","title":"Stage#6: Reap before the next prompt"}, \ + {"slug":"fy4","tester_log_prefix":"tester::#fy4","title":"Stage#6: Job number reset"} \ ] endef diff --git a/go.mod b/go.mod index bc865bf3..1dcca96d 100644 --- a/go.mod +++ b/go.mod @@ -8,8 +8,10 @@ require ( github.com/charmbracelet/x/vt v0.0.0-20250122132629-a969ddeb820d github.com/codecrafters-io/tester-utils v0.4.15 github.com/creack/pty v1.1.24 + github.com/dustin/go-humanize v1.0.1 github.com/fatih/color v1.18.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 + github.com/stretchr/testify v1.10.0 go.chromium.org/luci v0.0.0-20250611085002-a5741c865576 ) @@ -20,7 +22,6 @@ require ( github.com/charmbracelet/x/ansi v0.7.0 // indirect github.com/charmbracelet/x/wcwidth v0.0.0-20241011142426-46044092ad91 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect @@ -30,7 +31,6 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/stretchr/testify v1.10.0 // indirect golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/term v0.32.0 // indirect diff --git a/internal/stage_bg1.go b/internal/stage_bg1.go index a488121a..f9928233 100644 --- a/internal/stage_bg1.go +++ b/internal/stage_bg1.go @@ -27,7 +27,7 @@ func testBG1(stageHarness *test_case_harness.TestCaseHarness) error { jobsTestCase := test_cases.JobsBuiltinResponseTestCase{ SuccessMessage: "✓ Received empty response", // Expect no output - ExpectedOutputEntries: []test_cases.JobsBuiltinOutputEntry{}, + ExpectedOutputEntries: []test_cases.BackgroundJobStatusEntry{}, } if err := jobsTestCase.Run(asserter, shell, logger); err != nil { diff --git a/internal/stage_bg3.go b/internal/stage_bg3.go index dc8c5e51..66c4b12c 100644 --- a/internal/stage_bg3.go +++ b/internal/stage_bg3.go @@ -30,7 +30,7 @@ func testBG3(stageHarness *test_case_harness.TestCaseHarness) error { // Assert the job output jobsTestCase := test_cases.JobsBuiltinResponseTestCase{ - ExpectedOutputEntries: []test_cases.JobsBuiltinOutputEntry{{ + ExpectedOutputEntries: []test_cases.BackgroundJobStatusEntry{{ JobNumber: 1, Status: "Running", LaunchCommand: backgroundLaunchCommand, diff --git a/internal/stage_bg4.go b/internal/stage_bg4.go index eac6e28a..08fef70b 100644 --- a/internal/stage_bg4.go +++ b/internal/stage_bg4.go @@ -52,7 +52,7 @@ func launchBgCommandAndAssertJobs(asserter *logged_shell_asserter.LoggedShellAss jobs = append(jobs, jobInfo{JobNumber: i + 1, Command: bgCommand}) - jobsOutputEntries := make([]test_cases.JobsBuiltinOutputEntry, 0, len(jobs)) + jobsOutputEntries := make([]test_cases.BackgroundJobStatusEntry, 0, len(jobs)) for i, job := range jobs { // Default marker is unmarked @@ -66,7 +66,7 @@ func launchBgCommandAndAssertJobs(asserter *logged_shell_asserter.LoggedShellAss marker = test_cases.PreviousJob } - jobsOutputEntries = append(jobsOutputEntries, test_cases.JobsBuiltinOutputEntry{ + jobsOutputEntries = append(jobsOutputEntries, test_cases.BackgroundJobStatusEntry{ JobNumber: job.JobNumber, Status: "Running", LaunchCommand: job.Command, diff --git a/internal/stage_bg5.go b/internal/stage_bg5.go index b781e177..7867de2e 100644 --- a/internal/stage_bg5.go +++ b/internal/stage_bg5.go @@ -40,7 +40,7 @@ func testBG5(stageHarness *test_case_harness.TestCaseHarness) error { // Call jobs jobsBuiltinTestCase1 := test_cases.JobsBuiltinResponseTestCase{ - ExpectedOutputEntries: []test_cases.JobsBuiltinOutputEntry{{ + ExpectedOutputEntries: []test_cases.BackgroundJobStatusEntry{{ JobNumber: 1, Status: "Running", LaunchCommand: bgGrepCommand, @@ -63,7 +63,7 @@ func testBG5(stageHarness *test_case_harness.TestCaseHarness) error { // Call jobs again jobsBuiltinTestCase2 := test_cases.JobsBuiltinResponseTestCase{ - ExpectedOutputEntries: []test_cases.JobsBuiltinOutputEntry{{ + ExpectedOutputEntries: []test_cases.BackgroundJobStatusEntry{{ JobNumber: 1, Status: "Done", LaunchCommand: bgGrepCommand, diff --git a/internal/stage_bg6.go b/internal/stage_bg6.go index 3ae9d8fd..b37e6739 100644 --- a/internal/stage_bg6.go +++ b/internal/stage_bg6.go @@ -73,7 +73,7 @@ func testBG6(stageHarness *test_case_harness.TestCaseHarness) error { // Call jobs for the first time jobsBuiltinTestCase1 := test_cases.JobsBuiltinResponseTestCase{ - ExpectedOutputEntries: []test_cases.JobsBuiltinOutputEntry{ + ExpectedOutputEntries: []test_cases.BackgroundJobStatusEntry{ {JobNumber: 1, Status: "Running", LaunchCommand: sleepCommand, Marker: test_cases.UnmarkedJob}, {JobNumber: 2, Status: "Done", LaunchCommand: bgGrepCommand1, Marker: test_cases.PreviousJob}, {JobNumber: 3, Status: "Running", LaunchCommand: bgGrepCommand2, Marker: test_cases.CurrentJob}, @@ -92,7 +92,7 @@ func testBG6(stageHarness *test_case_harness.TestCaseHarness) error { // Call jobs for the second time jobsBuiltinTestCase2 := test_cases.JobsBuiltinResponseTestCase{ - ExpectedOutputEntries: []test_cases.JobsBuiltinOutputEntry{ + ExpectedOutputEntries: []test_cases.BackgroundJobStatusEntry{ {JobNumber: 1, Status: "Running", LaunchCommand: sleepCommand, Marker: test_cases.PreviousJob}, {JobNumber: 3, Status: "Done", LaunchCommand: bgGrepCommand2, Marker: test_cases.CurrentJob}, }, @@ -104,7 +104,7 @@ func testBG6(stageHarness *test_case_harness.TestCaseHarness) error { // Call jobs again jobsBuiltinTestCase3 := test_cases.JobsBuiltinResponseTestCase{ - ExpectedOutputEntries: []test_cases.JobsBuiltinOutputEntry{ + ExpectedOutputEntries: []test_cases.BackgroundJobStatusEntry{ {JobNumber: 1, Status: "Running", LaunchCommand: sleepCommand, Marker: test_cases.CurrentJob}, }, SuccessMessage: "Expected 1 entries found in the output", diff --git a/internal/stage_bg7.go b/internal/stage_bg7.go new file mode 100644 index 00000000..b2c9c853 --- /dev/null +++ b/internal/stage_bg7.go @@ -0,0 +1,100 @@ +package internal + +import ( + "fmt" + + "github.com/codecrafters-io/shell-tester/internal/logged_shell_asserter" + "github.com/codecrafters-io/shell-tester/internal/shell_executable" + "github.com/codecrafters-io/shell-tester/internal/test_cases" + "github.com/codecrafters-io/tester-utils/random" + "github.com/codecrafters-io/tester-utils/test_case_harness" +) + +func testBG7(stageHarness *test_case_harness.TestCaseHarness) error { + logger := stageHarness.Logger + shell := shell_executable.NewShellExecutable(stageHarness) + asserter := logged_shell_asserter.NewLoggedShellAsserter(shell) + + fifoPath := fmt.Sprintf("/tmp/%s-%d", random.RandomWord(), random.RandomInt(1, 100)) + if err := CreateRandomFIFOWithTeardown(stageHarness, fifoPath, 0644); err != nil { + return err + } + + if err := asserter.StartShellAndAssertPrompt(true); err != nil { + return err + } + + // Spawn background process: sleep 500 + sleepCommand := "sleep 500" + bgSleepTestCase := test_cases.BackgroundCommandResponseTestCase{ + Command: sleepCommand, + ExpectedJobNumber: 1, + SuccessMessage: "✓ Received entry for the launched job", + } + if err := bgSleepTestCase.Run(asserter, shell, logger); err != nil { + return err + } + + // Grep read pattern + grepPattern := random.RandomWord() + bgGrepCommand := fmt.Sprintf("grep -q %s %s", grepPattern, fifoPath) + bgGrepTestCase := test_cases.BackgroundCommandResponseTestCase{ + Command: bgGrepCommand, + ExpectedJobNumber: 2, + SuccessMessage: "✓ Received entry for the launched job", + } + if err := bgGrepTestCase.Run(asserter, shell, logger); err != nil { + return err + } + + // Run jobs + jobsBuiltinTestCase := test_cases.JobsBuiltinResponseTestCase{ + ExpectedOutputEntries: []test_cases.BackgroundJobStatusEntry{ + {JobNumber: 1, Status: "Running", LaunchCommand: sleepCommand, Marker: test_cases.PreviousJob}, + {JobNumber: 2, Status: "Running", LaunchCommand: bgGrepCommand, Marker: test_cases.CurrentJob}, + }, + SuccessMessage: "✓ Received 2 entries for the running jobs", + } + if err := jobsBuiltinTestCase.Run(asserter, shell, logger); err != nil { + return err + } + + // Write to fifo + if err := WriteToFile(stageHarness, fifoPath, grepPattern); err != nil { + return err + } + + // Issue an echo command and expect the reaped job entry will follow the echoed text + echoArgument := random.RandomWord() + echoTestCase := test_cases.CommandResponseWithReapedJobsTestCase{ + Command: fmt.Sprintf("echo %s", echoArgument), + ExpectedCommandOutput: echoArgument, + ExpectedReapedJobEntries: []*test_cases.BackgroundJobStatusEntry{{ + JobNumber: 2, + Status: "Done", + LaunchCommand: bgGrepCommand, + Marker: test_cases.CurrentJob, + }}, + SuccessMessage: "✓ Received command output followed by an entry for the reaped job", + } + + if err := echoTestCase.Run(asserter, shell, logger); err != nil { + return err + } + + // Call jobs — only sleep (job 1) remains + jobsBuiltinTestCase2 := test_cases.JobsBuiltinResponseTestCase{ + ExpectedOutputEntries: []test_cases.BackgroundJobStatusEntry{{ + JobNumber: 1, + Status: "Running", + LaunchCommand: sleepCommand, + Marker: test_cases.CurrentJob, + }}, + SuccessMessage: "✓ Received 1 entry for the remaining running job", + } + if err := jobsBuiltinTestCase2.Run(asserter, shell, logger); err != nil { + return err + } + + return logAndQuit(asserter, nil) +} diff --git a/internal/stage_bg8.go b/internal/stage_bg8.go new file mode 100644 index 00000000..3c160078 --- /dev/null +++ b/internal/stage_bg8.go @@ -0,0 +1,174 @@ +package internal + +import ( + "fmt" + "time" + + "github.com/codecrafters-io/shell-tester/internal/logged_shell_asserter" + "github.com/codecrafters-io/shell-tester/internal/shell_executable" + "github.com/codecrafters-io/shell-tester/internal/test_cases" + "github.com/codecrafters-io/tester-utils/random" + "github.com/codecrafters-io/tester-utils/test_case_harness" +) + +func testBG8(stageHarness *test_case_harness.TestCaseHarness) error { + logger := stageHarness.Logger + + if err := testBg8ResetToZero(stageHarness); err != nil { + return err + } + + logger.Infof("Tearing down shell") + + return testBg8Recycle(stageHarness) +} + +func testBg8ResetToZero(stageHarness *test_case_harness.TestCaseHarness) error { + shell := shell_executable.NewShellExecutable(stageHarness) + asserter := logged_shell_asserter.NewLoggedShellAsserter(shell) + logger := stageHarness.Logger + + fifoPath1 := fmt.Sprintf("/tmp/%s-%d", random.RandomWord(), random.RandomInt(1, 100)) + if err := CreateRandomFIFOWithTeardown(stageHarness, fifoPath1, 0644); err != nil { + return err + } + + if err := asserter.StartShellAndAssertPrompt(true); err != nil { + return err + } + + grepPattern1 := random.RandomWord() + bgGrepCommand1 := fmt.Sprintf("grep -q %s %s", grepPattern1, fifoPath1) + bgGrepTestCase := &test_cases.BackgroundCommandResponseTestCase{ + Command: bgGrepCommand1, + ExpectedJobNumber: 1, + SuccessMessage: "✓ Received entry for the launched job", + } + if err := bgGrepTestCase.Run(asserter, shell, logger); err != nil { + return err + } + + // Write to fifo to make job 1 'Done' + if err := WriteToFile(stageHarness, fifoPath1, grepPattern1); err != nil { + return err + } + + time.Sleep(time.Millisecond) + + echoArg := random.RandomWord() + echoTestCase := test_cases.CommandResponseWithReapedJobsTestCase{ + Command: fmt.Sprintf("echo %s", echoArg), + ExpectedCommandOutput: echoArg, + ExpectedReapedJobEntries: []*test_cases.BackgroundJobStatusEntry{ + {JobNumber: 1, Status: "Done", LaunchCommand: bgGrepCommand1, Marker: test_cases.CurrentJob}, + }, + SuccessMessage: "✓ Received command output followed by an entry for the reaped job", + } + + if err := echoTestCase.Run(asserter, shell, logger); err != nil { + return err + } + + jobsEmptyTestCase := test_cases.JobsBuiltinResponseTestCase{ + ExpectedOutputEntries: []test_cases.BackgroundJobStatusEntry{}, + SuccessMessage: "✓ Received no entries", + } + + if err := jobsEmptyTestCase.Run(asserter, shell, logger); err != nil { + return err + } + + sleepCommand := "sleep 10" + if err := (&test_cases.BackgroundCommandResponseTestCase{Command: sleepCommand, ExpectedJobNumber: 1}).Run(asserter, shell, logger); err != nil { + return err + } + + jobsOneEntryTestCase := test_cases.JobsBuiltinResponseTestCase{ + ExpectedOutputEntries: []test_cases.BackgroundJobStatusEntry{{ + JobNumber: 1, Status: "Running", LaunchCommand: sleepCommand, Marker: test_cases.CurrentJob, + }}, + SuccessMessage: "✓ Received 1 entry for the running job", + } + + if err := jobsOneEntryTestCase.Run(asserter, shell, logger); err != nil { + return err + } + + return nil +} + +func testBg8Recycle(stageHarness *test_case_harness.TestCaseHarness) error { + shell := shell_executable.NewShellExecutable(stageHarness) + asserter := logged_shell_asserter.NewLoggedShellAsserter(shell) + logger := stageHarness.Logger + + fifoPath := fmt.Sprintf("/tmp/%s-%d", random.RandomWord(), random.RandomInt(1, 100)) + if err := CreateRandomFIFOWithTeardown(stageHarness, fifoPath, 0644); err != nil { + return err + } + + if err := asserter.StartShellAndAssertPrompt(true); err != nil { + return err + } + + sleepLong := "sleep 100" + bgLongCmd := &test_cases.BackgroundCommandResponseTestCase{ + Command: sleepLong, + ExpectedJobNumber: 1, + SuccessMessage: "✓ Received entry for the launched job", + } + if err := bgLongCmd.Run(asserter, shell, logger); err != nil { + return err + } + + grepPattern := random.RandomWord() + bgGrepCommand := fmt.Sprintf("grep -q %s %s", grepPattern, fifoPath) + if err := (&test_cases.BackgroundCommandResponseTestCase{ + Command: bgGrepCommand, + ExpectedJobNumber: 2, + SuccessMessage: "✓ Received entry for the launched job", + }).Run(asserter, shell, logger); err != nil { + return err + } + + if err := WriteToFile(stageHarness, fifoPath, grepPattern); err != nil { + return err + } + time.Sleep(time.Millisecond) + + echoArgument := random.RandomWord() + echoTestCase := test_cases.CommandResponseWithReapedJobsTestCase{ + Command: fmt.Sprintf("echo %s", echoArgument), + ExpectedCommandOutput: echoArgument, + ExpectedReapedJobEntries: []*test_cases.BackgroundJobStatusEntry{{ + JobNumber: 2, Status: "Done", LaunchCommand: bgGrepCommand, Marker: test_cases.CurrentJob, + }}, + SuccessMessage: "✓ Received command output followed by an entry for the reaped job", + } + if err := echoTestCase.Run(asserter, shell, logger); err != nil { + return err + } + + sleep50 := "sleep 50" + if err := (&test_cases.BackgroundCommandResponseTestCase{ + Command: sleep50, + ExpectedJobNumber: 2, + SuccessMessage: "✓ Received entry for the launched job", + }).Run(asserter, shell, logger); err != nil { + return err + } + + jobsTwo := test_cases.JobsBuiltinResponseTestCase{ + ExpectedOutputEntries: []test_cases.BackgroundJobStatusEntry{ + {JobNumber: 1, Status: "Running", LaunchCommand: sleepLong, Marker: test_cases.PreviousJob}, + {JobNumber: 2, Status: "Running", LaunchCommand: sleep50, Marker: test_cases.CurrentJob}, + }, + SuccessMessage: "✓ Received 2 entries for the running jobs", + } + + if err := jobsTwo.Run(asserter, shell, logger); err != nil { + return err + } + + return logAndQuit(asserter, nil) +} diff --git a/internal/stages_test.go b/internal/stages_test.go index 0f178b14..4f370fbd 100644 --- a/internal/stages_test.go +++ b/internal/stages_test.go @@ -139,6 +139,13 @@ func TestStages(t *testing.T) { StdoutFixturePath: "./test_helpers/fixtures/background_jobs_jobs_builtin_not_reaped", NormalizeOutputFunc: normalizeTesterOutput, }, + "background_jobs_job_number_not_recycled": { + StageSlugs: []string{"fy4"}, + CodePath: "./test_helpers/scenarios/background_jobs_job_number_not_recycled", + ExpectedExitCode: 1, + StdoutFixturePath: "./test_helpers/fixtures/background_jobs_job_number_not_recycled", + NormalizeOutputFunc: normalizeTesterOutput, + }, "pipelines_pass_bash": { StageSlugs: []string{"br6", "ny9", "xk3"}, CodePath: "./test_helpers/bash", diff --git a/internal/test_cases/command_response_with_reaped_jobs_test_case.go b/internal/test_cases/command_response_with_reaped_jobs_test_case.go new file mode 100644 index 00000000..78c710bf --- /dev/null +++ b/internal/test_cases/command_response_with_reaped_jobs_test_case.go @@ -0,0 +1,80 @@ +package test_cases + +import ( + "fmt" + "regexp" + "time" + + "github.com/codecrafters-io/shell-tester/internal/assertions" + "github.com/codecrafters-io/shell-tester/internal/logged_shell_asserter" + "github.com/codecrafters-io/shell-tester/internal/shell_executable" + "github.com/codecrafters-io/tester-utils/logger" +) + +type CommandResponseWithReapedJobsTestCase struct { + // Command is the command to send to the shell + Command string + + // ExpectedCommandOutput is the expected output string to match against + ExpectedCommandOutput string + + // FallbackPatterns is a list of regex patterns to match against + FallbackPatterns []*regexp.Regexp + + // SuccessMessage is the message to log in case of success + SuccessMessage string + + // ExpectedReapedJobEntries is the list of entries expected after the command output appears + ExpectedReapedJobEntries []*BackgroundJobStatusEntry +} + +func (t CommandResponseWithReapedJobsTestCase) Run(asserter *logged_shell_asserter.LoggedShellAsserter, shell *shell_executable.ShellExecutable, logger *logger.Logger) error { + allSingleLinesAssertion := []assertions.SingleLineAssertion{} + + // Add assertion for command's actual output first + allSingleLinesAssertion = append(allSingleLinesAssertion, assertions.SingleLineAssertion{ + ExpectedOutput: t.ExpectedCommandOutput, + FallbackPatterns: t.FallbackPatterns, + }) + + // Add assertion for reaped job entries now + for _, expectedOutputEntry := range t.ExpectedReapedJobEntries { + expectedJobMarkerString := convertJobMarkerToString(expectedOutputEntry.Marker) + + expectedOutput := fmt.Sprintf( + "[%d]%s %s %s", + expectedOutputEntry.JobNumber, expectedJobMarkerString, expectedOutputEntry.Status, expectedOutputEntry.LaunchCommand, + ) + + // This regex aims to match lines like: [1]+ Done sleep 5 & + regexString := fmt.Sprintf( + `^\[%d\]\s*%s\s+(?i)%s\s+(?-i)%s$`, + expectedOutputEntry.JobNumber, + regexp.QuoteMeta(expectedJobMarkerString), + regexp.QuoteMeta(expectedOutputEntry.Status), + regexp.QuoteMeta(expectedOutputEntry.LaunchCommand), + ) + + allSingleLinesAssertion = append(allSingleLinesAssertion, assertions.SingleLineAssertion{ + ExpectedOutput: expectedOutput, + FallbackPatterns: []*regexp.Regexp{regexp.MustCompile(regexString)}, + }) + } + + // A small delay to ensure that the grep process has exitted + time.Sleep(time.Millisecond) + + commandWithMultilineResponseTestCase := CommandWithMultilineResponseTestCase{ + Command: t.Command, + MultiLineAssertion: assertions.MultiLineAssertion{ + SingleLineAssertions: allSingleLinesAssertion, + }, + SuccessMessage: t.SuccessMessage, + } + + if err := commandWithMultilineResponseTestCase.Run(asserter, shell, logger); err != nil { + return err + } + + return nil +} diff --git a/internal/test_cases/jobs_builtin_response_test_case.go b/internal/test_cases/jobs_builtin_response_test_case.go index f2c0dc38..d9b6f950 100644 --- a/internal/test_cases/jobs_builtin_response_test_case.go +++ b/internal/test_cases/jobs_builtin_response_test_case.go @@ -16,7 +16,7 @@ const ( PreviousJob ) -type JobsBuiltinOutputEntry struct { +type BackgroundJobStatusEntry struct { // The job number value in the square brackets JobNumber int // Status: "Running", "Done", "Terminated", "1 Exit", etc @@ -28,7 +28,7 @@ type JobsBuiltinOutputEntry struct { } type JobsBuiltinResponseTestCase struct { - ExpectedOutputEntries []JobsBuiltinOutputEntry + ExpectedOutputEntries []BackgroundJobStatusEntry SuccessMessage string } diff --git a/internal/test_helpers/course_definition.yml b/internal/test_helpers/course_definition.yml index 49ee1e59..b6fc6041 100644 --- a/internal/test_helpers/course_definition.yml +++ b/internal/test_helpers/course_definition.yml @@ -1287,6 +1287,20 @@ stages: marketing_md: |- In this stage, you'll extend the `jobs` builtin to reap multiple jobs and update markers dynamically. + - slug: "bv8" + primary_extension_slug: "background-jobs" + name: "Reap before the next prompt" + difficulty: medium + marketing_md: |- + In this stage, you'll add suport for reaping jobs before printing the next prompt. + + - slug: "fy4" + primary_extension_slug: "background-jobs" + name: "Job number reset" + difficulty: easy + marketing_md: |- + In this stage, you'll implement recycling job number indices. + - slug: "br6" primary_extension_slug: "pipelines" name: "Dual-command pipeline" diff --git a/internal/test_helpers/fixtures/background_jobs_job_number_not_recycled b/internal/test_helpers/fixtures/background_jobs_job_number_not_recycled new file mode 100644 index 00000000..4afb8e24 --- /dev/null +++ b/internal/test_helpers/fixtures/background_jobs_job_number_not_recycled @@ -0,0 +1,23 @@ +Debug = true + +[tester::#FY4] Running tests for Stage #FY4 (fy4) +[tester::#FY4] [setup] mkfifo /tmp/pear-70 +[tester::#FY4] Running ./your_program.sh +[your-program] $ grep -q raspberry /tmp/pear-70 & +[your-program] [1] 2199 +[tester::#FY4] ✓ Received entry for the launched job +[tester::#FY4] [setup] echo "raspberry" > "/tmp/pear-70" +[your-program] $ echo mango +[your-program] mango +[your-program] [1]+ Done grep -q raspberry /tmp/pear-70 +[tester::#FY4] ✓ Received command output followed by an entry for the reaped job +[your-program] $ jobs +[tester::#FY4] ✓ Received no entries +[your-program] $ sleep 10 & +[your-program] [2] 2200 +[tester::#FY4] ^ Line does not match expected value. +[tester::#FY4] Expected: "[1] " +[tester::#FY4] Received: "[2] 2200" +[your-program] $  +[tester::#FY4] Assertion failed. +[tester::#FY4] Test failed diff --git a/internal/test_helpers/scenarios/background_jobs_job_number_not_recycled/codecrafters.yml b/internal/test_helpers/scenarios/background_jobs_job_number_not_recycled/codecrafters.yml new file mode 100644 index 00000000..a3e7b727 --- /dev/null +++ b/internal/test_helpers/scenarios/background_jobs_job_number_not_recycled/codecrafters.yml @@ -0,0 +1 @@ +debug: true \ No newline at end of file diff --git a/internal/test_helpers/scenarios/background_jobs_job_number_not_recycled/main.py b/internal/test_helpers/scenarios/background_jobs_job_number_not_recycled/main.py new file mode 100644 index 00000000..dd6876c7 --- /dev/null +++ b/internal/test_helpers/scenarios/background_jobs_job_number_not_recycled/main.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +"""Minimal shell that passes base stages oo8--ip1: prompt, invalid command, REPL, exit, echo, type, run.""" + +import cmd +import os +import sys +import subprocess +import shlex + +PROMPT = "$ " +BUILTINS = {"echo", "exit", "type", "jobs"} + +# List of background jobs: {"job_id": int, "cmd_line": str, "process": Popen} +BACKGROUND_JOBS: list[dict] = [] +# Next job number to assign; never recycled after reaping (intentional error scenario). +NEXT_JOB_ID: int = 1 + + +def reap_finished_jobs() -> None: + """Remove any background job whose process has exited.""" + global BACKGROUND_JOBS + BACKGROUND_JOBS = [j for j in BACKGROUND_JOBS if j["process"].poll() is None] + +def notify_and_reap_jobs() -> None: + """Print finished jobs, then remove them.""" + global BACKGROUND_JOBS + alive = [] + for job in BACKGROUND_JOBS: + proc = job["process"] + if proc.poll() is not None: + jid = job["job_id"] + cmd_line = job["cmd_line"] + # For Done, don't print trailing & + cmd_line = cmd_line.removesuffix(" &") + # Since this is only used for fy4; hardcoding this for now + print(f"[{jid}]+ Done {cmd_line}") + else: + alive.append(job) + BACKGROUND_JOBS = alive + +def find_in_path(name: str) -> str | None: + """First executable in PATH with this name (skip non-executable).""" + path = os.environ.get("PATH", "") + for part in path.split(os.pathsep): + if not part: + continue + full = os.path.join(part, name) + if os.path.isfile(full) and os.access(full, os.X_OK): + return full + return None + + +def run_builtin(args: list[str], last_exit_code: int) -> tuple[str | None, int | None]: + """Run builtin. Returns (output_line_or_None, exit_code_or_None). None means continue (no exit).""" + if not args: + return None, None + cmd = args[0].lower() + if cmd == "exit": + code = int(args[1]) if len(args) > 1 else last_exit_code + if code < 0: + code = 0 + return None, code + if cmd == "echo": + out = " ".join(args[1:]) if len(args) > 1 else "" + return out, None + if cmd == "type": + if len(args) < 2: + return None, None + name = args[1] + if name in BUILTINS: + return f"{name} is a shell builtin", None + path = find_in_path(name) + if path: + return f"{name} is {path}", None + return f"{name}: not found", None + if cmd == "jobs": + reap_finished_jobs() + lines = [] + for i, job in enumerate(BACKGROUND_JOBS): + jid = job["job_id"] + cmd_line = job["cmd_line"] + # + for current (last), - for previous (second-last), space otherwise + n = len(BACKGROUND_JOBS) + if i == n - 1: + marker = "+" + elif n >= 2 and i == n - 2: + marker = "-" + else: + marker = " " + line = f"[{jid}]{marker} Running {cmd_line}" + lines.append(line) + return "\n".join(lines) if lines else None, None + return None, None + + +def run_external(args: list[str], background: bool = False) -> tuple[int | None, subprocess.Popen | None]: + """Run external command. Returns (exit_code, None) or (None, proc) if background.""" + name = args[0] + path = find_in_path(name) + if path is None: + print(f"{name}: command not found", file=sys.stderr) + return (127, None) + try: + if background: + proc = subprocess.Popen( + [path] + args[1:], + env=os.environ, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + return (None, proc) + proc = subprocess.run( + [path] + args[1:], + env=os.environ, + capture_output=False, + timeout=30, + ) + return (proc.returncode, None) + except (OSError, subprocess.TimeoutExpired): + return (127, None) + + +def main() -> None: + global NEXT_JOB_ID + last_exit_code = 0 + while True: + sys.stdout.write(PROMPT) + sys.stdout.flush() + try: + line = sys.stdin.readline() + except (EOFError, KeyboardInterrupt): + break + if not line: + break + line = line.rstrip("\n\r") + background = line.rstrip().endswith("&") + if background: + line = line.rstrip()[:-1].rstrip() + parts = shlex.split(line) + if not parts: + continue + cmd = parts[0] + args = parts[1:] + + if cmd in BUILTINS: + out, exit_code = run_builtin([cmd] + args, last_exit_code) + if exit_code is not None: + sys.exit(exit_code) + if out is not None: + print(out) + sys.stdout.flush() + else: + exit_code, bg_proc = run_external([cmd] + args, background=background) + if exit_code is not None: + last_exit_code = exit_code + else: + job_id = NEXT_JOB_ID + NEXT_JOB_ID += 1 + cmd_line = " ".join([cmd] + args) + " &" + BACKGROUND_JOBS.append({"job_id": job_id, "cmd_line": cmd_line, "process": bg_proc}) + print(f"[{job_id}] {bg_proc.pid}") + sys.stdout.flush() + notify_and_reap_jobs() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/internal/test_helpers/scenarios/background_jobs_job_number_not_recycled/your_program.sh b/internal/test_helpers/scenarios/background_jobs_job_number_not_recycled/your_program.sh new file mode 100755 index 00000000..f667ad73 --- /dev/null +++ b/internal/test_helpers/scenarios/background_jobs_job_number_not_recycled/your_program.sh @@ -0,0 +1,3 @@ +#!/bin/sh +# This buggy implementation of shell will never recycle job numbers and keep on increasing +exec python3 $(dirname "$0")/main.py "$@" \ No newline at end of file diff --git a/internal/tester_definition.go b/internal/tester_definition.go index 3bd10ce3..ba73c7f4 100644 --- a/internal/tester_definition.go +++ b/internal/tester_definition.go @@ -222,6 +222,16 @@ var testerDefinition = tester_definition.TesterDefinition{ TestFunc: testBG6, Timeout: 15 * time.Second, }, + { + Slug: "bv8", + TestFunc: testBG7, + Timeout: 15 * time.Second, + }, + { + Slug: "fy4", + TestFunc: testBG8, + Timeout: 15 * time.Second, + }, // Pipelines { Slug: "br6", From c7400f68e4e2bfd3ac1eaf6b185bfcadb8140c9f Mon Sep 17 00:00:00 2001 From: Udeshya Dhungana <075bct095.udeshya@pcampus.edu.np> Date: Thu, 26 Feb 2026 14:36:01 +0545 Subject: [PATCH 2/9] Update normalizeTesterOutput with a new regex for error case fixture --- internal/stages_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/stages_test.go b/internal/stages_test.go index 4f370fbd..b03c9b81 100644 --- a/internal/stages_test.go +++ b/internal/stages_test.go @@ -258,6 +258,7 @@ func normalizeTesterOutput(testerOutput []byte) []byte { "[your-program] [JOB_NUM]PID": {regexp.MustCompile(`\[your-program\].*\[\d+\]\d+`)}, "[tester::#AT7] Received: \"[JOB_NUM]PID\"": {regexp.MustCompile(`\[tester::#AT7\].*Received:.*"\[\d+\]\d+"`)}, "[tester::#AT7] Received: \"[JOB_NUM] PID\"": {regexp.MustCompile(`\[tester::#AT7\].*Received:.*"\[\d+\] \d+"`)}, + "[tester::#FY4] Received: \"[JOB_NUM] PID\"": {regexp.MustCompile(`\[tester::#FY4\].*Received:.*"\[\d+\] \d+"`)}, // For background jobs incorrect job number } From 35119abc88ad871f97f127f99b3eee4180662c86 Mon Sep 17 00:00:00 2001 From: Udeshya Dhungana <075bct095.udeshya@pcampus.edu.np> Date: Thu, 26 Feb 2026 14:45:17 +0545 Subject: [PATCH 3/9] Add fixtures for bash: Stage 7 and 8d --- internal/stages_test.go | 2 +- .../fixtures/bash/background_jobs/pass | 84 +++++++++++++++++-- 2 files changed, 76 insertions(+), 10 deletions(-) diff --git a/internal/stages_test.go b/internal/stages_test.go index b03c9b81..3f9f3a6d 100644 --- a/internal/stages_test.go +++ b/internal/stages_test.go @@ -98,7 +98,7 @@ func TestStages(t *testing.T) { NormalizeOutputFunc: normalizeTesterOutput, }, "background_jobs_pass_bash": { - StageSlugs: []string{"af3", "at7", "jd6", "dk5", "ma9", "rq2"}, + StageSlugs: []string{"af3", "at7", "jd6", "dk5", "ma9", "rq2", "bv8", "fy4"}, CodePath: "./test_helpers/bash", ExpectedExitCode: 0, StdoutFixturePath: "./test_helpers/fixtures/bash/background_jobs/pass", diff --git a/internal/test_helpers/fixtures/bash/background_jobs/pass b/internal/test_helpers/fixtures/bash/background_jobs/pass index a2351810..63357805 100644 --- a/internal/test_helpers/fixtures/bash/background_jobs/pass +++ b/internal/test_helpers/fixtures/bash/background_jobs/pass @@ -13,7 +13,7 @@ Debug = true [tester::#AT7] Running tests for Stage #AT7 (at7) [tester::#AT7] Running ./your_shell.sh [your-program] $ sleep 100 & -[your-program] [1] 2538 +[your-program] [1] 2817 [tester::#AT7] ✓ Received next prompt [your-program] $  [tester::#AT7] Test passed. @@ -21,7 +21,7 @@ Debug = true [tester::#JD6] Running tests for Stage #JD6 (jd6) [tester::#JD6] Running ./your_shell.sh [your-program] $ sleep 100 & -[your-program] [1] 2540 +[your-program] [1] 2819 [tester::#JD6] ✓ Received next prompt [your-program] $ jobs [your-program] [1]+ Running sleep 100 & @@ -32,20 +32,20 @@ Debug = true [tester::#DK5] Running tests for Stage #DK5 (dk5) [tester::#DK5] Running ./your_shell.sh [your-program] $ sleep 100 & -[your-program] [1] 2542 +[your-program] [1] 2821 [tester::#DK5] ✓ Received next prompt [your-program] $ jobs [your-program] [1]+ Running sleep 100 & [tester::#DK5] ✓ Expected 1 entry for jobs builtin found [your-program] $ sleep 200 & -[your-program] [2] 2543 +[your-program] [2] 2822 [tester::#DK5] ✓ Received next prompt [your-program] $ jobs [your-program] [1]- Running sleep 100 & [your-program] [2]+ Running sleep 200 & [tester::#DK5] ✓ Expected 2 entries for jobs builtin found [your-program] $ sleep 300 & -[your-program] [3] 2544 +[your-program] [3] 2823 [tester::#DK5] ✓ Received next prompt [your-program] $ jobs [your-program] [1] Running sleep 100 & @@ -59,7 +59,7 @@ Debug = true [tester::#MA9] Running ./your_shell.sh [tester::#MA9] [setup] mkfifo /tmp/pear-70 [your-program] $ grep -q raspberry /tmp/pear-70 & -[your-program] [1] 2551 +[your-program] [1] 2825 [your-program] $ jobs [your-program] [1]+ Running grep -q raspberry /tmp/pear-70 & [tester::#MA9] Expected 1 entry found in the output @@ -75,11 +75,11 @@ Debug = true [tester::#RQ2] [setup] mkfifo /tmp/banana-67 [tester::#RQ2] Running ./your_shell.sh [your-program] $ sleep 500 & -[your-program] [1] 2553 +[your-program] [1] 2827 [your-program] $ grep -q banana /tmp/mango-20 & -[your-program] [2] 2554 +[your-program] [2] 2828 [your-program] $ grep -q pear /tmp/banana-67 & -[your-program] [3] 2555 +[your-program] [3] 2829 [tester::#RQ2] [setup] echo "banana" > "/tmp/mango-20" [your-program] $ jobs [your-program] [1] Running sleep 500 & @@ -96,3 +96,69 @@ Debug = true [tester::#RQ2] Expected 1 entries found in the output [your-program] $  [tester::#RQ2] Test passed. + +[tester::#BV8] Running tests for Stage #BV8 (bv8) +[tester::#BV8] [setup] mkfifo /tmp/orange-80 +[tester::#BV8] Running ./your_shell.sh +[your-program] $ sleep 500 & +[your-program] [1] 2831 +[tester::#BV8] ✓ Received entry for the launched job +[your-program] $ grep -q grape /tmp/orange-80 & +[your-program] [2] 2832 +[tester::#BV8] ✓ Received entry for the launched job +[your-program] $ jobs +[your-program] [1]- Running sleep 500 & +[your-program] [2]+ Running grep -q grape /tmp/orange-80 & +[tester::#BV8] ✓ Received 2 entries for the running jobs +[tester::#BV8] [setup] echo "grape" > "/tmp/orange-80" +[your-program] $ echo apple +[your-program] apple +[your-program] [2]+ Done grep -q grape /tmp/orange-80 +[tester::#BV8] ✓ Received command output followed by an entry for the reaped job +[your-program] $ jobs +[your-program] [1]+ Running sleep 500 & +[tester::#BV8] ✓ Received 1 entry for the remaining running job +[your-program] $  +[tester::#BV8] Test passed. + +[tester::#FY4] Running tests for Stage #FY4 (fy4) +[tester::#FY4] [setup] mkfifo /tmp/apple-16 +[tester::#FY4] Running ./your_shell.sh +[your-program] $ grep -q mango /tmp/apple-16 & +[your-program] [1] 2834 +[tester::#FY4] ✓ Received entry for the launched job +[tester::#FY4] [setup] echo "mango" > "/tmp/apple-16" +[your-program] $ echo grape +[your-program] grape +[your-program] [1]+ Done grep -q mango /tmp/apple-16 +[tester::#FY4] ✓ Received command output followed by an entry for the reaped job +[your-program] $ jobs +[tester::#FY4] ✓ Received no entries +[your-program] $ sleep 10 & +[your-program] [1] 2835 +[your-program] $ jobs +[your-program] [1]+ Running sleep 10 & +[tester::#FY4] ✓ Received 1 entry for the running job +[tester::#FY4] Tearing down shell +[tester::#FY4] [setup] mkfifo /tmp/banana-47 +[tester::#FY4] Running ./your_shell.sh +[your-program] $ sleep 100 & +[your-program] [1] 2837 +[tester::#FY4] ✓ Received entry for the launched job +[your-program] $ grep -q apple /tmp/banana-47 & +[your-program] [2] 2838 +[tester::#FY4] ✓ Received entry for the launched job +[tester::#FY4] [setup] echo "apple" > "/tmp/banana-47" +[your-program] $ echo pear +[your-program] pear +[your-program] [2]+ Done grep -q apple /tmp/banana-47 +[tester::#FY4] ✓ Received command output followed by an entry for the reaped job +[your-program] $ sleep 50 & +[your-program] [2] 2839 +[tester::#FY4] ✓ Received entry for the launched job +[your-program] $ jobs +[your-program] [1]- Running sleep 100 & +[your-program] [2]+ Running sleep 50 & +[tester::#FY4] ✓ Received 2 entries for the running jobs +[your-program] $  +[tester::#FY4] Test passed. From 7debb09362bdd9b4366fae1d1ffcebe9599ab3ec Mon Sep 17 00:00:00 2001 From: Udeshya Dhungana <075bct095.udeshya@pcampus.edu.np> Date: Thu, 26 Feb 2026 14:45:45 +0545 Subject: [PATCH 4/9] Fix duplicated stage name in background jobs stages --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 42a1cee9..0a157233 100644 --- a/Makefile +++ b/Makefile @@ -179,8 +179,8 @@ define _BACKGROUND_JOBS_STAGES {"slug":"dk5","tester_log_prefix":"tester::#dk5","title":"Stage#4: List multiple jobs"}, \ {"slug":"ma9","tester_log_prefix":"tester::#ma9","title":"Stage#5: Reaping one job using jobs"}, \ {"slug":"rq2","tester_log_prefix":"tester::#rq2","title":"Stage#6: Reaping multiple jobs using jobs"}, \ - {"slug":"bv8","tester_log_prefix":"tester::#bv8","title":"Stage#6: Reap before the next prompt"}, \ - {"slug":"fy4","tester_log_prefix":"tester::#fy4","title":"Stage#6: Job number reset"} \ + {"slug":"bv8","tester_log_prefix":"tester::#bv8","title":"Stage#7: Reap before the next prompt"}, \ + {"slug":"fy4","tester_log_prefix":"tester::#fy4","title":"Stage#8: Job number reset"} \ ] endef From df903152396ad0904755b8ea9a8c1e4ddcdfa157 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 26 Feb 2026 09:16:06 +0000 Subject: [PATCH 5/9] Fix indentation bug and consolidate job formatting logic - Fix Bug 1: Move return statement outside for loop in main.py (line 92) to process all background jobs instead of returning after the first one - Fix Bug 2: Add ExpectedOutputAndRegex() method to BackgroundJobStatusEntry to centralize job entry formatting logic. Update both CommandResponseWithReapedJobsTestCase and JobsBuiltinResponseTestCase to use the shared method, including proper 'Running' status handling. --- ...and_response_with_reaped_jobs_test_case.go | 19 +----- .../jobs_builtin_response_test_case.go | 67 ++++++++++--------- .../main.py | 2 +- 3 files changed, 40 insertions(+), 48 deletions(-) diff --git a/internal/test_cases/command_response_with_reaped_jobs_test_case.go b/internal/test_cases/command_response_with_reaped_jobs_test_case.go index 78c710bf..a47cfb8e 100644 --- a/internal/test_cases/command_response_with_reaped_jobs_test_case.go +++ b/internal/test_cases/command_response_with_reaped_jobs_test_case.go @@ -1,7 +1,6 @@ package test_cases import ( - "fmt" "regexp" "time" @@ -39,25 +38,11 @@ func (t CommandResponseWithReapedJobsTestCase) Run(asserter *logged_shell_assert // Add assertion for reaped job entries now for _, expectedOutputEntry := range t.ExpectedReapedJobEntries { - expectedJobMarkerString := convertJobMarkerToString(expectedOutputEntry.Marker) - - expectedOutput := fmt.Sprintf( - "[%d]%s %s %s", - expectedOutputEntry.JobNumber, expectedJobMarkerString, expectedOutputEntry.Status, expectedOutputEntry.LaunchCommand, - ) - - // This regex aims to match lines like: [1]+ Done sleep 5 & - regexString := fmt.Sprintf( - `^\[%d\]\s*%s\s+(?i)%s\s+(?-i)%s$`, - expectedOutputEntry.JobNumber, - regexp.QuoteMeta(expectedJobMarkerString), - regexp.QuoteMeta(expectedOutputEntry.Status), - regexp.QuoteMeta(expectedOutputEntry.LaunchCommand), - ) + expectedOutput, regexPattern := expectedOutputEntry.ExpectedOutputAndRegex() allSingleLinesAssertion = append(allSingleLinesAssertion, assertions.SingleLineAssertion{ ExpectedOutput: expectedOutput, - FallbackPatterns: []*regexp.Regexp{regexp.MustCompile(regexString)}, + FallbackPatterns: []*regexp.Regexp{regexPattern}, }) } diff --git a/internal/test_cases/jobs_builtin_response_test_case.go b/internal/test_cases/jobs_builtin_response_test_case.go index d9b6f950..82782d43 100644 --- a/internal/test_cases/jobs_builtin_response_test_case.go +++ b/internal/test_cases/jobs_builtin_response_test_case.go @@ -27,6 +27,41 @@ type BackgroundJobStatusEntry struct { Marker int } +// ExpectedOutputAndRegex returns the expected output string and regex pattern for this job entry. +func (e BackgroundJobStatusEntry) ExpectedOutputAndRegex() (string, *regexp.Regexp) { + expectedJobMarkerString := convertJobMarkerToString(e.Marker) + + // This regex aims to match lines like: [1]+ Running sleep 5 & + regexString := fmt.Sprintf( + `^\[%d\]\s*%s\s+(?i)%s\s+(?-i)%s`, + e.JobNumber, + regexp.QuoteMeta(expectedJobMarkerString), + regexp.QuoteMeta(e.Status), + regexp.QuoteMeta(e.LaunchCommand), + ) + + // For 'Running' jobs, bash displays the trailing & sign + // Users shall comply with bash for consistency (Ensured this by appending this to expected output) + // But this should be optional since ZSH doesn't use this + if e.Status == "Running" { + regexString += "( &)?$" + } else { + regexString += "$" + } + + expectedOutput := fmt.Sprintf( + "[%d]%s %s %s", + e.JobNumber, expectedJobMarkerString, e.Status, e.LaunchCommand, + ) + + // For 'Running' jobs, the trailing sign is expected + if e.Status == "Running" { + expectedOutput += " &" + } + + return expectedOutput, regexp.MustCompile(regexString) +} + type JobsBuiltinResponseTestCase struct { ExpectedOutputEntries []BackgroundJobStatusEntry SuccessMessage string @@ -56,39 +91,11 @@ func (t JobsBuiltinResponseTestCase) Run(asserter *logged_shell_asserter.LoggedS } for i, expectedOutputEntry := range t.ExpectedOutputEntries { - expectedJobMarkerString := convertJobMarkerToString(expectedOutputEntry.Marker) - - // This regex aims to match lines like: [1]+ Running sleep 5 & - regexString := fmt.Sprintf( - `^\[%d\]\s*%s\s+(?i)%s\s+(?-i)%s`, - expectedOutputEntry.JobNumber, - regexp.QuoteMeta(expectedJobMarkerString), - regexp.QuoteMeta(expectedOutputEntry.Status), - regexp.QuoteMeta(expectedOutputEntry.LaunchCommand), - ) - - // For 'Running' jobs, bash displays the trailing & sign - // Users shall comply with bash for consistency (Ensured this by appending this to expected output) - // But this should be optional since ZSH doesn't use this - if expectedOutputEntry.Status == "Running" { - regexString += "( &)?$" - } else { - regexString += "$" - } - - expectedOutput := fmt.Sprintf( - "[%d]%s %s %s", - expectedOutputEntry.JobNumber, expectedJobMarkerString, expectedOutputEntry.Status, expectedOutputEntry.LaunchCommand, - ) - - // For 'Running' jobs, the trailing sign is expected - if expectedOutputEntry.Status == "Running" { - expectedOutput += " &" - } + expectedOutput, regexPattern := expectedOutputEntry.ExpectedOutputAndRegex() asserter.AddAssertion(assertions.SingleLineAssertion{ ExpectedOutput: expectedOutput, - FallbackPatterns: []*regexp.Regexp{regexp.MustCompile(regexString)}, + FallbackPatterns: []*regexp.Regexp{regexPattern}, }) assertWithPrompt := false diff --git a/internal/test_helpers/scenarios/background_jobs_job_number_not_recycled/main.py b/internal/test_helpers/scenarios/background_jobs_job_number_not_recycled/main.py index dd6876c7..482e20d4 100644 --- a/internal/test_helpers/scenarios/background_jobs_job_number_not_recycled/main.py +++ b/internal/test_helpers/scenarios/background_jobs_job_number_not_recycled/main.py @@ -89,7 +89,7 @@ def run_builtin(args: list[str], last_exit_code: int) -> tuple[str | None, int | marker = " " line = f"[{jid}]{marker} Running {cmd_line}" lines.append(line) - return "\n".join(lines) if lines else None, None + return "\n".join(lines) if lines else None, None return None, None From f1bb7533f4b8afec3f8e07d84229970cb5a28e63 Mon Sep 17 00:00:00 2001 From: Udeshya Dhungana <075bct095.udeshya@pcampus.edu.np> Date: Thu, 26 Feb 2026 16:33:06 +0545 Subject: [PATCH 6/9] Add missing success message for bg8 --- internal/stage_bg8.go | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/internal/stage_bg8.go b/internal/stage_bg8.go index 3c160078..ad4cce5a 100644 --- a/internal/stage_bg8.go +++ b/internal/stage_bg8.go @@ -79,7 +79,11 @@ func testBg8ResetToZero(stageHarness *test_case_harness.TestCaseHarness) error { } sleepCommand := "sleep 10" - if err := (&test_cases.BackgroundCommandResponseTestCase{Command: sleepCommand, ExpectedJobNumber: 1}).Run(asserter, shell, logger); err != nil { + if err := (&test_cases.BackgroundCommandResponseTestCase{ + Command: sleepCommand, + ExpectedJobNumber: 1, + SuccessMessage: "✓ Received entry for started job", + }).Run(asserter, shell, logger); err != nil { return err } @@ -111,13 +115,13 @@ func testBg8Recycle(stageHarness *test_case_harness.TestCaseHarness) error { return err } - sleepLong := "sleep 100" - bgLongCmd := &test_cases.BackgroundCommandResponseTestCase{ - Command: sleepLong, + sleepCommand := "sleep 100" + sleepCommandTestCase := &test_cases.BackgroundCommandResponseTestCase{ + Command: sleepCommand, ExpectedJobNumber: 1, SuccessMessage: "✓ Received entry for the launched job", } - if err := bgLongCmd.Run(asserter, shell, logger); err != nil { + if err := sleepCommandTestCase.Run(asserter, shell, logger); err != nil { return err } @@ -149,24 +153,24 @@ func testBg8Recycle(stageHarness *test_case_harness.TestCaseHarness) error { return err } - sleep50 := "sleep 50" + sleepCommand2 := "sleep 50" if err := (&test_cases.BackgroundCommandResponseTestCase{ - Command: sleep50, + Command: sleepCommand2, ExpectedJobNumber: 2, SuccessMessage: "✓ Received entry for the launched job", }).Run(asserter, shell, logger); err != nil { return err } - jobsTwo := test_cases.JobsBuiltinResponseTestCase{ + jobsTestCase := test_cases.JobsBuiltinResponseTestCase{ ExpectedOutputEntries: []test_cases.BackgroundJobStatusEntry{ - {JobNumber: 1, Status: "Running", LaunchCommand: sleepLong, Marker: test_cases.PreviousJob}, - {JobNumber: 2, Status: "Running", LaunchCommand: sleep50, Marker: test_cases.CurrentJob}, + {JobNumber: 1, Status: "Running", LaunchCommand: sleepCommand, Marker: test_cases.PreviousJob}, + {JobNumber: 2, Status: "Running", LaunchCommand: sleepCommand2, Marker: test_cases.CurrentJob}, }, SuccessMessage: "✓ Received 2 entries for the running jobs", } - if err := jobsTwo.Run(asserter, shell, logger); err != nil { + if err := jobsTestCase.Run(asserter, shell, logger); err != nil { return err } From 61cb3b5c001ea9ea983ea8f72102f3fcab39a3e4 Mon Sep 17 00:00:00 2001 From: Udeshya Dhungana <075bct095.udeshya@pcampus.edu.np> Date: Thu, 26 Feb 2026 16:35:51 +0545 Subject: [PATCH 7/9] Record fixtures --- .../fixtures/bash/background_jobs/pass | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/internal/test_helpers/fixtures/bash/background_jobs/pass b/internal/test_helpers/fixtures/bash/background_jobs/pass index 9c3fc995..77ad384a 100644 --- a/internal/test_helpers/fixtures/bash/background_jobs/pass +++ b/internal/test_helpers/fixtures/bash/background_jobs/pass @@ -13,7 +13,7 @@ Debug = true [tester::#AT7] Running tests for Stage #AT7 (at7) [tester::#AT7] Running ./your_shell.sh [your-program] $ sleep 100 & -[your-program] [1] 2222 +[your-program] [1] 2011 [tester::#AT7] ✓ Received next prompt [your-program] $  [tester::#AT7] Test passed. @@ -21,7 +21,7 @@ Debug = true [tester::#JD6] Running tests for Stage #JD6 (jd6) [tester::#JD6] Running ./your_shell.sh [your-program] $ sleep 100 & -[your-program] [1] 2225 +[your-program] [1] 2014 [tester::#JD6] ✓ Received entry for the started job [your-program] $ jobs [your-program] [1]+ Running sleep 100 & @@ -32,20 +32,20 @@ Debug = true [tester::#DK5] Running tests for Stage #DK5 (dk5) [tester::#DK5] Running ./your_shell.sh [your-program] $ sleep 100 & -[your-program] [1] 2228 +[your-program] [1] 2017 [tester::#DK5] ✓ Received entry for the started job [your-program] $ jobs [your-program] [1]+ Running sleep 100 & [tester::#DK5] ✓ Received 1 entry for the running jobs [your-program] $ sleep 200 & -[your-program] [2] 2229 +[your-program] [2] 2019 [tester::#DK5] ✓ Received entry for the started job [your-program] $ jobs [your-program] [1]- Running sleep 100 & [your-program] [2]+ Running sleep 200 & [tester::#DK5] ✓ Received 2 entries for the running jobs [your-program] $ sleep 300 & -[your-program] [3] 2233 +[your-program] [3] 2021 [tester::#DK5] ✓ Received entry for the started job [your-program] $ jobs [your-program] [1] Running sleep 100 & @@ -59,7 +59,7 @@ Debug = true [tester::#MA9] Running ./your_shell.sh [tester::#MA9] [setup] mkfifo /tmp/pear-70 [your-program] $ grep -q raspberry /tmp/pear-70 & -[your-program] [1] 2239 +[your-program] [1] 2023 [tester::#MA9] ✓ Received entry for the started job [your-program] $ jobs [your-program] [1]+ Running grep -q raspberry /tmp/pear-70 & @@ -76,13 +76,13 @@ Debug = true [tester::#RQ2] [setup] mkfifo /tmp/mango-16 [tester::#RQ2] Running ./your_shell.sh [your-program] $ sleep 500 & -[your-program] [1] 2241 +[your-program] [1] 2025 [tester::#RQ2] ✓ Received entry for the started job [your-program] $ grep -q mango /tmp/blueberry-26 & -[your-program] [2] 2242 +[your-program] [2] 2026 [tester::#RQ2] ✓ Received entry for the started job [your-program] $ grep -q grape /tmp/mango-16 & -[your-program] [3] 2243 +[your-program] [3] 2027 [tester::#RQ2] ✓ Received entry for the started job [tester::#RQ2] [setup] echo "mango" > "/tmp/blueberry-26" [your-program] $ jobs @@ -105,10 +105,10 @@ Debug = true [tester::#BV8] [setup] mkfifo /tmp/banana-47 [tester::#BV8] Running ./your_shell.sh [your-program] $ sleep 500 & -[your-program] [1] 2245 +[your-program] [1] 2030 [tester::#BV8] ✓ Received entry for the launched job [your-program] $ grep -q apple /tmp/banana-47 & -[your-program] [2] 2246 +[your-program] [2] 2031 [tester::#BV8] ✓ Received entry for the launched job [your-program] $ jobs [your-program] [1]- Running sleep 500 & @@ -129,7 +129,7 @@ Debug = true [tester::#FY4] [setup] mkfifo /tmp/banana-39 [tester::#FY4] Running ./your_shell.sh [your-program] $ grep -q raspberry /tmp/banana-39 & -[your-program] [1] 2248 +[your-program] [1] 2033 [tester::#FY4] ✓ Received entry for the launched job [tester::#FY4] [setup] echo "raspberry" > "/tmp/banana-39" [your-program] $ echo apple @@ -139,7 +139,8 @@ Debug = true [your-program] $ jobs [tester::#FY4] ✓ Received no entries [your-program] $ sleep 10 & -[your-program] [1] 2249 +[your-program] [1] 2035 +[tester::#FY4] ✓ Received entry for started job [your-program] $ jobs [your-program] [1]+ Running sleep 10 & [tester::#FY4] ✓ Received 1 entry for the running job @@ -147,10 +148,10 @@ Debug = true [tester::#FY4] [setup] mkfifo /tmp/apple-52 [tester::#FY4] Running ./your_shell.sh [your-program] $ sleep 100 & -[your-program] [1] 2253 +[your-program] [1] 2037 [tester::#FY4] ✓ Received entry for the launched job [your-program] $ grep -q banana /tmp/apple-52 & -[your-program] [2] 2254 +[your-program] [2] 2039 [tester::#FY4] ✓ Received entry for the launched job [tester::#FY4] [setup] echo "banana" > "/tmp/apple-52" [your-program] $ echo pear @@ -158,7 +159,7 @@ Debug = true [your-program] [2]+ Done grep -q banana /tmp/apple-52 [tester::#FY4] ✓ Received command output followed by an entry for the reaped job [your-program] $ sleep 50 & -[your-program] [2] 2255 +[your-program] [2] 2040 [tester::#FY4] ✓ Received entry for the launched job [your-program] $ jobs [your-program] [1]- Running sleep 100 & From 34386dfb50f33d8fcee7aac92a90f25399c313a5 Mon Sep 17 00:00:00 2001 From: Udeshya Dhungana <075bct095.udeshya@pcampus.edu.np> Date: Fri, 27 Feb 2026 09:16:53 +0545 Subject: [PATCH 8/9] Add a test case when command output is not followed by an entry; record fixtures --- internal/stage_bg7.go | 10 +-- internal/stage_bg8.go | 37 ++++++--- .../background_jobs_job_number_not_recycled | 17 ++-- .../fixtures/bash/background_jobs/pass | 79 ++++++++++--------- 4 files changed, 80 insertions(+), 63 deletions(-) diff --git a/internal/stage_bg7.go b/internal/stage_bg7.go index b2c9c853..783bd03b 100644 --- a/internal/stage_bg7.go +++ b/internal/stage_bg7.go @@ -29,7 +29,7 @@ func testBG7(stageHarness *test_case_harness.TestCaseHarness) error { bgSleepTestCase := test_cases.BackgroundCommandResponseTestCase{ Command: sleepCommand, ExpectedJobNumber: 1, - SuccessMessage: "✓ Received entry for the launched job", + SuccessMessage: "✓ Output includes job number with PID", } if err := bgSleepTestCase.Run(asserter, shell, logger); err != nil { return err @@ -41,7 +41,7 @@ func testBG7(stageHarness *test_case_harness.TestCaseHarness) error { bgGrepTestCase := test_cases.BackgroundCommandResponseTestCase{ Command: bgGrepCommand, ExpectedJobNumber: 2, - SuccessMessage: "✓ Received entry for the launched job", + SuccessMessage: "✓ Output includes job number with PID", } if err := bgGrepTestCase.Run(asserter, shell, logger); err != nil { return err @@ -53,7 +53,7 @@ func testBG7(stageHarness *test_case_harness.TestCaseHarness) error { {JobNumber: 1, Status: "Running", LaunchCommand: sleepCommand, Marker: test_cases.PreviousJob}, {JobNumber: 2, Status: "Running", LaunchCommand: bgGrepCommand, Marker: test_cases.CurrentJob}, }, - SuccessMessage: "✓ Received 2 entries for the running jobs", + SuccessMessage: "✓ Found 2 entries for the running jobs", } if err := jobsBuiltinTestCase.Run(asserter, shell, logger); err != nil { return err @@ -75,7 +75,7 @@ func testBG7(stageHarness *test_case_harness.TestCaseHarness) error { LaunchCommand: bgGrepCommand, Marker: test_cases.CurrentJob, }}, - SuccessMessage: "✓ Received command output followed by an entry for the reaped job", + SuccessMessage: "✓ Found command output followed by an entry for the reaped job", } if err := echoTestCase.Run(asserter, shell, logger); err != nil { @@ -90,7 +90,7 @@ func testBG7(stageHarness *test_case_harness.TestCaseHarness) error { LaunchCommand: sleepCommand, Marker: test_cases.CurrentJob, }}, - SuccessMessage: "✓ Received 1 entry for the remaining running job", + SuccessMessage: "✓ 1 entry matches the running job", } if err := jobsBuiltinTestCase2.Run(asserter, shell, logger); err != nil { return err diff --git a/internal/stage_bg8.go b/internal/stage_bg8.go index ad4cce5a..4d15a807 100644 --- a/internal/stage_bg8.go +++ b/internal/stage_bg8.go @@ -42,7 +42,7 @@ func testBg8ResetToZero(stageHarness *test_case_harness.TestCaseHarness) error { bgGrepTestCase := &test_cases.BackgroundCommandResponseTestCase{ Command: bgGrepCommand1, ExpectedJobNumber: 1, - SuccessMessage: "✓ Received entry for the launched job", + SuccessMessage: "✓ Output includes job number with PID", } if err := bgGrepTestCase.Run(asserter, shell, logger); err != nil { return err @@ -55,23 +55,34 @@ func testBg8ResetToZero(stageHarness *test_case_harness.TestCaseHarness) error { time.Sleep(time.Millisecond) - echoArg := random.RandomWord() + echoArgs := random.RandomWords(2) echoTestCase := test_cases.CommandResponseWithReapedJobsTestCase{ - Command: fmt.Sprintf("echo %s", echoArg), - ExpectedCommandOutput: echoArg, + Command: fmt.Sprintf("echo %s", echoArgs[0]), + ExpectedCommandOutput: echoArgs[0], ExpectedReapedJobEntries: []*test_cases.BackgroundJobStatusEntry{ {JobNumber: 1, Status: "Done", LaunchCommand: bgGrepCommand1, Marker: test_cases.CurrentJob}, }, - SuccessMessage: "✓ Received command output followed by an entry for the reaped job", + SuccessMessage: "✓ Received output for echo followed by an entry for the reaped job", } if err := echoTestCase.Run(asserter, shell, logger); err != nil { return err } + // Test command output not followed by reaped jobs entry + echoTestCase2 := test_cases.CommandResponseTestCase{ + Command: fmt.Sprintf("echo %s", echoArgs[1]), + ExpectedOutput: echoArgs[1], + SuccessMessage: "✓ Received output for echo", + } + + if err := echoTestCase2.Run(asserter, shell, logger); err != nil { + return err + } + jobsEmptyTestCase := test_cases.JobsBuiltinResponseTestCase{ ExpectedOutputEntries: []test_cases.BackgroundJobStatusEntry{}, - SuccessMessage: "✓ Received no entries", + SuccessMessage: "✓ No jobs", } if err := jobsEmptyTestCase.Run(asserter, shell, logger); err != nil { @@ -82,7 +93,7 @@ func testBg8ResetToZero(stageHarness *test_case_harness.TestCaseHarness) error { if err := (&test_cases.BackgroundCommandResponseTestCase{ Command: sleepCommand, ExpectedJobNumber: 1, - SuccessMessage: "✓ Received entry for started job", + SuccessMessage: "✓ Output includes job number with PID", }).Run(asserter, shell, logger); err != nil { return err } @@ -91,7 +102,7 @@ func testBg8ResetToZero(stageHarness *test_case_harness.TestCaseHarness) error { ExpectedOutputEntries: []test_cases.BackgroundJobStatusEntry{{ JobNumber: 1, Status: "Running", LaunchCommand: sleepCommand, Marker: test_cases.CurrentJob, }}, - SuccessMessage: "✓ Received 1 entry for the running job", + SuccessMessage: "✓ 1 entry matches the running job", } if err := jobsOneEntryTestCase.Run(asserter, shell, logger); err != nil { @@ -119,7 +130,7 @@ func testBg8Recycle(stageHarness *test_case_harness.TestCaseHarness) error { sleepCommandTestCase := &test_cases.BackgroundCommandResponseTestCase{ Command: sleepCommand, ExpectedJobNumber: 1, - SuccessMessage: "✓ Received entry for the launched job", + SuccessMessage: "✓ Output includes job number with PID", } if err := sleepCommandTestCase.Run(asserter, shell, logger); err != nil { return err @@ -130,7 +141,7 @@ func testBg8Recycle(stageHarness *test_case_harness.TestCaseHarness) error { if err := (&test_cases.BackgroundCommandResponseTestCase{ Command: bgGrepCommand, ExpectedJobNumber: 2, - SuccessMessage: "✓ Received entry for the launched job", + SuccessMessage: "✓ Output includes job number with PID", }).Run(asserter, shell, logger); err != nil { return err } @@ -147,7 +158,7 @@ func testBg8Recycle(stageHarness *test_case_harness.TestCaseHarness) error { ExpectedReapedJobEntries: []*test_cases.BackgroundJobStatusEntry{{ JobNumber: 2, Status: "Done", LaunchCommand: bgGrepCommand, Marker: test_cases.CurrentJob, }}, - SuccessMessage: "✓ Received command output followed by an entry for the reaped job", + SuccessMessage: "✓ Received output for echo followed by an entry for the reaped job", } if err := echoTestCase.Run(asserter, shell, logger); err != nil { return err @@ -157,7 +168,7 @@ func testBg8Recycle(stageHarness *test_case_harness.TestCaseHarness) error { if err := (&test_cases.BackgroundCommandResponseTestCase{ Command: sleepCommand2, ExpectedJobNumber: 2, - SuccessMessage: "✓ Received entry for the launched job", + SuccessMessage: "✓ Output includes job number with PID", }).Run(asserter, shell, logger); err != nil { return err } @@ -167,7 +178,7 @@ func testBg8Recycle(stageHarness *test_case_harness.TestCaseHarness) error { {JobNumber: 1, Status: "Running", LaunchCommand: sleepCommand, Marker: test_cases.PreviousJob}, {JobNumber: 2, Status: "Running", LaunchCommand: sleepCommand2, Marker: test_cases.CurrentJob}, }, - SuccessMessage: "✓ Received 2 entries for the running jobs", + SuccessMessage: "✓ 2 entries match the running jobs", } if err := jobsTestCase.Run(asserter, shell, logger); err != nil { diff --git a/internal/test_helpers/fixtures/background_jobs_job_number_not_recycled b/internal/test_helpers/fixtures/background_jobs_job_number_not_recycled index 4afb8e24..7314f000 100644 --- a/internal/test_helpers/fixtures/background_jobs_job_number_not_recycled +++ b/internal/test_helpers/fixtures/background_jobs_job_number_not_recycled @@ -4,20 +4,23 @@ Debug = true [tester::#FY4] [setup] mkfifo /tmp/pear-70 [tester::#FY4] Running ./your_program.sh [your-program] $ grep -q raspberry /tmp/pear-70 & -[your-program] [1] 2199 -[tester::#FY4] ✓ Received entry for the launched job +[your-program] [1] 2378 +[tester::#FY4] ✓ Output includes job number with PID [tester::#FY4] [setup] echo "raspberry" > "/tmp/pear-70" +[your-program] $ echo blueberry +[your-program] blueberry +[your-program] [1]+ Done grep -q raspberry /tmp/pear-70 +[tester::#FY4] ✓ Received output for echo followed by an entry for the reaped job [your-program] $ echo mango [your-program] mango -[your-program] [1]+ Done grep -q raspberry /tmp/pear-70 -[tester::#FY4] ✓ Received command output followed by an entry for the reaped job +[tester::#FY4] ✓ Received output for echo [your-program] $ jobs -[tester::#FY4] ✓ Received no entries +[tester::#FY4] ✓ No jobs [your-program] $ sleep 10 & -[your-program] [2] 2200 +[your-program] [2] 2382 [tester::#FY4] ^ Line does not match expected value. [tester::#FY4] Expected: "[1] " -[tester::#FY4] Received: "[2] 2200" +[tester::#FY4] Received: "[2] 2382" [your-program] $  [tester::#FY4] Assertion failed. [tester::#FY4] Test failed diff --git a/internal/test_helpers/fixtures/bash/background_jobs/pass b/internal/test_helpers/fixtures/bash/background_jobs/pass index 77ad384a..78b56e61 100644 --- a/internal/test_helpers/fixtures/bash/background_jobs/pass +++ b/internal/test_helpers/fixtures/bash/background_jobs/pass @@ -13,7 +13,7 @@ Debug = true [tester::#AT7] Running tests for Stage #AT7 (at7) [tester::#AT7] Running ./your_shell.sh [your-program] $ sleep 100 & -[your-program] [1] 2011 +[your-program] [1] 2385 [tester::#AT7] ✓ Received next prompt [your-program] $  [tester::#AT7] Test passed. @@ -21,7 +21,7 @@ Debug = true [tester::#JD6] Running tests for Stage #JD6 (jd6) [tester::#JD6] Running ./your_shell.sh [your-program] $ sleep 100 & -[your-program] [1] 2014 +[your-program] [1] 2387 [tester::#JD6] ✓ Received entry for the started job [your-program] $ jobs [your-program] [1]+ Running sleep 100 & @@ -32,20 +32,20 @@ Debug = true [tester::#DK5] Running tests for Stage #DK5 (dk5) [tester::#DK5] Running ./your_shell.sh [your-program] $ sleep 100 & -[your-program] [1] 2017 +[your-program] [1] 2392 [tester::#DK5] ✓ Received entry for the started job [your-program] $ jobs [your-program] [1]+ Running sleep 100 & [tester::#DK5] ✓ Received 1 entry for the running jobs [your-program] $ sleep 200 & -[your-program] [2] 2019 +[your-program] [2] 2393 [tester::#DK5] ✓ Received entry for the started job [your-program] $ jobs [your-program] [1]- Running sleep 100 & [your-program] [2]+ Running sleep 200 & [tester::#DK5] ✓ Received 2 entries for the running jobs [your-program] $ sleep 300 & -[your-program] [3] 2021 +[your-program] [3] 2394 [tester::#DK5] ✓ Received entry for the started job [your-program] $ jobs [your-program] [1] Running sleep 100 & @@ -59,7 +59,7 @@ Debug = true [tester::#MA9] Running ./your_shell.sh [tester::#MA9] [setup] mkfifo /tmp/pear-70 [your-program] $ grep -q raspberry /tmp/pear-70 & -[your-program] [1] 2023 +[your-program] [1] 2397 [tester::#MA9] ✓ Received entry for the started job [your-program] $ jobs [your-program] [1]+ Running grep -q raspberry /tmp/pear-70 & @@ -76,13 +76,13 @@ Debug = true [tester::#RQ2] [setup] mkfifo /tmp/mango-16 [tester::#RQ2] Running ./your_shell.sh [your-program] $ sleep 500 & -[your-program] [1] 2025 +[your-program] [1] 2399 [tester::#RQ2] ✓ Received entry for the started job [your-program] $ grep -q mango /tmp/blueberry-26 & -[your-program] [2] 2026 +[your-program] [2] 2400 [tester::#RQ2] ✓ Received entry for the started job [your-program] $ grep -q grape /tmp/mango-16 & -[your-program] [3] 2027 +[your-program] [3] 2401 [tester::#RQ2] ✓ Received entry for the started job [tester::#RQ2] [setup] echo "mango" > "/tmp/blueberry-26" [your-program] $ jobs @@ -105,23 +105,23 @@ Debug = true [tester::#BV8] [setup] mkfifo /tmp/banana-47 [tester::#BV8] Running ./your_shell.sh [your-program] $ sleep 500 & -[your-program] [1] 2030 -[tester::#BV8] ✓ Received entry for the launched job +[your-program] [1] 2405 +[tester::#BV8] ✓ Output includes job number with PID [your-program] $ grep -q apple /tmp/banana-47 & -[your-program] [2] 2031 -[tester::#BV8] ✓ Received entry for the launched job +[your-program] [2] 2406 +[tester::#BV8] ✓ Output includes job number with PID [your-program] $ jobs [your-program] [1]- Running sleep 500 & [your-program] [2]+ Running grep -q apple /tmp/banana-47 & -[tester::#BV8] ✓ Received 2 entries for the running jobs +[tester::#BV8] ✓ Found 2 entries for the running jobs [tester::#BV8] [setup] echo "apple" > "/tmp/banana-47" [your-program] $ echo pear [your-program] pear [your-program] [2]+ Done grep -q apple /tmp/banana-47 -[tester::#BV8] ✓ Received command output followed by an entry for the reaped job +[tester::#BV8] ✓ Found command output followed by an entry for the reaped job [your-program] $ jobs [your-program] [1]+ Running sleep 500 & -[tester::#BV8] ✓ Received 1 entry for the remaining running job +[tester::#BV8] ✓ 1 entry matches the running job [your-program] $  [tester::#BV8] Test passed. @@ -129,41 +129,44 @@ Debug = true [tester::#FY4] [setup] mkfifo /tmp/banana-39 [tester::#FY4] Running ./your_shell.sh [your-program] $ grep -q raspberry /tmp/banana-39 & -[your-program] [1] 2033 -[tester::#FY4] ✓ Received entry for the launched job +[your-program] [1] 2410 +[tester::#FY4] ✓ Output includes job number with PID [tester::#FY4] [setup] echo "raspberry" > "/tmp/banana-39" +[your-program] $ echo banana +[your-program] banana +[your-program] [1]+ Done grep -q raspberry /tmp/banana-39 +[tester::#FY4] ✓ Received output for echo followed by an entry for the reaped job [your-program] $ echo apple [your-program] apple -[your-program] [1]+ Done grep -q raspberry /tmp/banana-39 -[tester::#FY4] ✓ Received command output followed by an entry for the reaped job +[tester::#FY4] ✓ Received output for echo [your-program] $ jobs -[tester::#FY4] ✓ Received no entries +[tester::#FY4] ✓ No jobs [your-program] $ sleep 10 & -[your-program] [1] 2035 -[tester::#FY4] ✓ Received entry for started job +[your-program] [1] 2411 +[tester::#FY4] ✓ Output includes job number with PID [your-program] $ jobs [your-program] [1]+ Running sleep 10 & -[tester::#FY4] ✓ Received 1 entry for the running job +[tester::#FY4] ✓ 1 entry matches the running job [tester::#FY4] Tearing down shell -[tester::#FY4] [setup] mkfifo /tmp/apple-52 +[tester::#FY4] [setup] mkfifo /tmp/strawberry-88 [tester::#FY4] Running ./your_shell.sh [your-program] $ sleep 100 & -[your-program] [1] 2037 -[tester::#FY4] ✓ Received entry for the launched job -[your-program] $ grep -q banana /tmp/apple-52 & -[your-program] [2] 2039 -[tester::#FY4] ✓ Received entry for the launched job -[tester::#FY4] [setup] echo "banana" > "/tmp/apple-52" -[your-program] $ echo pear -[your-program] pear -[your-program] [2]+ Done grep -q banana /tmp/apple-52 -[tester::#FY4] ✓ Received command output followed by an entry for the reaped job +[your-program] [1] 2414 +[tester::#FY4] ✓ Output includes job number with PID +[your-program] $ grep -q pineapple /tmp/strawberry-88 & +[your-program] [2] 2415 +[tester::#FY4] ✓ Output includes job number with PID +[tester::#FY4] [setup] echo "pineapple" > "/tmp/strawberry-88" +[your-program] $ echo apple +[your-program] apple +[your-program] [2]+ Done grep -q pineapple /tmp/strawberry-88 +[tester::#FY4] ✓ Received output for echo followed by an entry for the reaped job [your-program] $ sleep 50 & -[your-program] [2] 2040 -[tester::#FY4] ✓ Received entry for the launched job +[your-program] [2] 2416 +[tester::#FY4] ✓ Output includes job number with PID [your-program] $ jobs [your-program] [1]- Running sleep 100 & [your-program] [2]+ Running sleep 50 & -[tester::#FY4] ✓ Received 2 entries for the running jobs +[tester::#FY4] ✓ 2 entries match the running jobs [your-program] $  [tester::#FY4] Test passed. From 02bd1bad74302bf1c02356c04af9fb796ee2825d Mon Sep 17 00:00:00 2001 From: Udeshya Dhungana <075bct095.udeshya@pcampus.edu.np> Date: Wed, 4 Mar 2026 08:06:06 +0545 Subject: [PATCH 9/9] Add background output test case in stage 7 and 8 --- internal/stage_bg7.go | 18 +++++- internal/stage_bg8.go | 32 ++++++++-- internal/stage_p1.go | 8 +-- ...and_response_with_reaped_jobs_test_case.go | 7 ++- ...mmand_with_multiline_response_test_case.go | 18 ++++-- .../background_jobs_job_number_not_recycled | 14 +++-- .../fixtures/bash/background_jobs/pass | 58 ++++++++++--------- .../main.py | 2 - 8 files changed, 107 insertions(+), 50 deletions(-) diff --git a/internal/stage_bg7.go b/internal/stage_bg7.go index 783bd03b..80f270e1 100644 --- a/internal/stage_bg7.go +++ b/internal/stage_bg7.go @@ -3,6 +3,7 @@ package internal import ( "fmt" + "al.essio.dev/pkg/shellescape" "github.com/codecrafters-io/shell-tester/internal/logged_shell_asserter" "github.com/codecrafters-io/shell-tester/internal/shell_executable" "github.com/codecrafters-io/shell-tester/internal/test_cases" @@ -37,7 +38,7 @@ func testBG7(stageHarness *test_case_harness.TestCaseHarness) error { // Grep read pattern grepPattern := random.RandomWord() - bgGrepCommand := fmt.Sprintf("grep -q %s %s", grepPattern, fifoPath) + bgGrepCommand := fmt.Sprintf("grep %s %s", grepPattern, fifoPath) bgGrepTestCase := test_cases.BackgroundCommandResponseTestCase{ Command: bgGrepCommand, ExpectedJobNumber: 2, @@ -64,11 +65,22 @@ func testBG7(stageHarness *test_case_harness.TestCaseHarness) error { return err } + // Background output test case + backgroundOutputTestCase := test_cases.BackgroundCommandOutputOnlyTestCase{ + ExpectedOutputLines: []string{grepPattern}, + SuccessMessage: fmt.Sprintf("✓ Output of %s found", shellescape.Quote(bgGrepCommand)), + } + + if err := backgroundOutputTestCase.Run(asserter, shell, logger); err != nil { + return err + } + // Issue an echo command and expect the reaped job entry will follow the echoed text echoArgument := random.RandomWord() echoTestCase := test_cases.CommandResponseWithReapedJobsTestCase{ - Command: fmt.Sprintf("echo %s", echoArgument), - ExpectedCommandOutput: echoArgument, + Command: fmt.Sprintf("echo %s", echoArgument), + ExpectedCommandOutput: echoArgument, + ShouldSkipCurrentPromptAssertion: true, ExpectedReapedJobEntries: []*test_cases.BackgroundJobStatusEntry{{ JobNumber: 2, Status: "Done", diff --git a/internal/stage_bg8.go b/internal/stage_bg8.go index 4d15a807..41c2c030 100644 --- a/internal/stage_bg8.go +++ b/internal/stage_bg8.go @@ -4,6 +4,7 @@ import ( "fmt" "time" + "al.essio.dev/pkg/shellescape" "github.com/codecrafters-io/shell-tester/internal/logged_shell_asserter" "github.com/codecrafters-io/shell-tester/internal/shell_executable" "github.com/codecrafters-io/shell-tester/internal/test_cases" @@ -38,7 +39,7 @@ func testBg8ResetToZero(stageHarness *test_case_harness.TestCaseHarness) error { } grepPattern1 := random.RandomWord() - bgGrepCommand1 := fmt.Sprintf("grep -q %s %s", grepPattern1, fifoPath1) + bgGrepCommand1 := fmt.Sprintf("grep %s %s", grepPattern1, fifoPath1) bgGrepTestCase := &test_cases.BackgroundCommandResponseTestCase{ Command: bgGrepCommand1, ExpectedJobNumber: 1, @@ -55,6 +56,16 @@ func testBg8ResetToZero(stageHarness *test_case_harness.TestCaseHarness) error { time.Sleep(time.Millisecond) + // Background output test case for grep + backgroundOutputTestCase := test_cases.BackgroundCommandOutputOnlyTestCase{ + ExpectedOutputLines: []string{grepPattern1}, + SuccessMessage: fmt.Sprintf("✓ Output of %s found", shellescape.Quote(bgGrepCommand1)), + } + + if err := backgroundOutputTestCase.Run(asserter, shell, logger); err != nil { + return err + } + echoArgs := random.RandomWords(2) echoTestCase := test_cases.CommandResponseWithReapedJobsTestCase{ Command: fmt.Sprintf("echo %s", echoArgs[0]), @@ -62,7 +73,8 @@ func testBg8ResetToZero(stageHarness *test_case_harness.TestCaseHarness) error { ExpectedReapedJobEntries: []*test_cases.BackgroundJobStatusEntry{ {JobNumber: 1, Status: "Done", LaunchCommand: bgGrepCommand1, Marker: test_cases.CurrentJob}, }, - SuccessMessage: "✓ Received output for echo followed by an entry for the reaped job", + ShouldSkipCurrentPromptAssertion: true, + SuccessMessage: "✓ Received output for echo followed by an entry for the reaped job", } if err := echoTestCase.Run(asserter, shell, logger); err != nil { @@ -137,7 +149,7 @@ func testBg8Recycle(stageHarness *test_case_harness.TestCaseHarness) error { } grepPattern := random.RandomWord() - bgGrepCommand := fmt.Sprintf("grep -q %s %s", grepPattern, fifoPath) + bgGrepCommand := fmt.Sprintf("grep %s %s", grepPattern, fifoPath) if err := (&test_cases.BackgroundCommandResponseTestCase{ Command: bgGrepCommand, ExpectedJobNumber: 2, @@ -151,6 +163,17 @@ func testBg8Recycle(stageHarness *test_case_harness.TestCaseHarness) error { } time.Sleep(time.Millisecond) + // Background output test case for grep + + backgroundOutputTestCase := test_cases.BackgroundCommandOutputOnlyTestCase{ + ExpectedOutputLines: []string{grepPattern}, + SuccessMessage: fmt.Sprintf("✓ Output of %s found", shellescape.Quote(bgGrepCommand)), + } + + if err := backgroundOutputTestCase.Run(asserter, shell, logger); err != nil { + return err + } + echoArgument := random.RandomWord() echoTestCase := test_cases.CommandResponseWithReapedJobsTestCase{ Command: fmt.Sprintf("echo %s", echoArgument), @@ -158,7 +181,8 @@ func testBg8Recycle(stageHarness *test_case_harness.TestCaseHarness) error { ExpectedReapedJobEntries: []*test_cases.BackgroundJobStatusEntry{{ JobNumber: 2, Status: "Done", LaunchCommand: bgGrepCommand, Marker: test_cases.CurrentJob, }}, - SuccessMessage: "✓ Received output for echo followed by an entry for the reaped job", + ShouldSkipCurrentPromptAssertion: true, + SuccessMessage: "✓ Received output for echo followed by an entry for the reaped job", } if err := echoTestCase.Run(asserter, shell, logger); err != nil { return err diff --git a/internal/stage_p1.go b/internal/stage_p1.go index 130d3d02..bc416d28 100644 --- a/internal/stage_p1.go +++ b/internal/stage_p1.go @@ -78,10 +78,10 @@ func testP1(stageHarness *test_case_harness.TestCaseHarness) error { input = fmt.Sprintf(`tail -f %s | head -n 5`, filePath) expectedMultiLineOutput := strings.Split(strings.Trim(fileContent, "\n"), "\n") multiLineTestCase := test_cases.CommandWithMultilineResponseTestCase{ - Command: input, - MultiLineAssertion: assertions.NewMultiLineAssertion(expectedMultiLineOutput), - SuccessMessage: "✓ Received redirected file content", - SkipPromptAssertion: true, + Command: input, + MultiLineAssertion: assertions.NewMultiLineAssertion(expectedMultiLineOutput), + SuccessMessage: "✓ Received redirected file content", + ShouldSkipNextPromptAssertion: true, } if err := multiLineTestCase.Run(asserter, shell, logger); err != nil { return err diff --git a/internal/test_cases/command_response_with_reaped_jobs_test_case.go b/internal/test_cases/command_response_with_reaped_jobs_test_case.go index a47cfb8e..af681a98 100644 --- a/internal/test_cases/command_response_with_reaped_jobs_test_case.go +++ b/internal/test_cases/command_response_with_reaped_jobs_test_case.go @@ -25,6 +25,10 @@ type CommandResponseWithReapedJobsTestCase struct { // ExpectedReapedJobEntries is the list of entries expected after the command output appears ExpectedReapedJobEntries []*BackgroundJobStatusEntry + + // ShouldSkipCurrentPromptAssertion should be set to true if the prompt symbol is not expected in the command reflection + // This is usually true when a background command's output has consumed the current prompt line + ShouldSkipCurrentPromptAssertion bool } func (t CommandResponseWithReapedJobsTestCase) Run(asserter *logged_shell_asserter.LoggedShellAsserter, shell *shell_executable.ShellExecutable, logger *logger.Logger) error { @@ -54,7 +58,8 @@ func (t CommandResponseWithReapedJobsTestCase) Run(asserter *logged_shell_assert MultiLineAssertion: assertions.MultiLineAssertion{ SingleLineAssertions: allSingleLinesAssertion, }, - SuccessMessage: t.SuccessMessage, + ShouldSkipCurrentPromptAssertion: t.ShouldSkipCurrentPromptAssertion, + SuccessMessage: t.SuccessMessage, } if err := commandWithMultilineResponseTestCase.Run(asserter, shell, logger); err != nil { diff --git a/internal/test_cases/command_with_multiline_response_test_case.go b/internal/test_cases/command_with_multiline_response_test_case.go index bf640697..cdcc1903 100644 --- a/internal/test_cases/command_with_multiline_response_test_case.go +++ b/internal/test_cases/command_with_multiline_response_test_case.go @@ -19,8 +19,11 @@ type CommandWithMultilineResponseTestCase struct { // SuccessMessage is the message to log in case of success SuccessMessage string - // SkipAssertPrompt is a flag to indicate that the prompt should not be asserted - SkipPromptAssertion bool + // ShouldSkipCurrentPromptAssertion should be set if prompt is not expected in the command reflection + ShouldSkipCurrentPromptAssertion bool + + // ShouldSkipNextPromptAssertion is a flag to indicate that the prompt should not be asserted + ShouldSkipNextPromptAssertion bool } func (t CommandWithMultilineResponseTestCase) Run(asserter *logged_shell_asserter.LoggedShellAsserter, shell *shell_executable.ShellExecutable, logger *logger.Logger) error { @@ -28,14 +31,21 @@ func (t CommandWithMultilineResponseTestCase) Run(asserter *logged_shell_asserte return fmt.Errorf("Error sending command to shell: %v", err) } - commandReflection := fmt.Sprintf("$ %s", t.Command) + var commandReflection string + + if t.ShouldSkipCurrentPromptAssertion { + commandReflection = t.Command + } else { + commandReflection = fmt.Sprintf("$ %s", t.Command) + } + asserter.AddAssertion(assertions.SingleLineAssertion{ ExpectedOutput: commandReflection, }) asserter.AddAssertion(&t.MultiLineAssertion) - if !t.SkipPromptAssertion { + if !t.ShouldSkipNextPromptAssertion { if err := asserter.AssertWithPrompt(); err != nil { return err } diff --git a/internal/test_helpers/fixtures/background_jobs_job_number_not_recycled b/internal/test_helpers/fixtures/background_jobs_job_number_not_recycled index 7314f000..a28ca6b0 100644 --- a/internal/test_helpers/fixtures/background_jobs_job_number_not_recycled +++ b/internal/test_helpers/fixtures/background_jobs_job_number_not_recycled @@ -3,13 +3,15 @@ Debug = true [tester::#FY4] Running tests for Stage #FY4 (fy4) [tester::#FY4] [setup] mkfifo /tmp/pear-70 [tester::#FY4] Running ./your_program.sh -[your-program] $ grep -q raspberry /tmp/pear-70 & -[your-program] [1] 2378 +[your-program] $ grep raspberry /tmp/pear-70 & +[your-program] [1] 2442 [tester::#FY4] ✓ Output includes job number with PID [tester::#FY4] [setup] echo "raspberry" > "/tmp/pear-70" -[your-program] $ echo blueberry +[your-program] $ raspberry +[tester::#FY4] ✓ Output of 'grep raspberry /tmp/pear-70' found +[your-program] echo blueberry [your-program] blueberry -[your-program] [1]+ Done grep -q raspberry /tmp/pear-70 +[your-program] [1]+ Done grep raspberry /tmp/pear-70 [tester::#FY4] ✓ Received output for echo followed by an entry for the reaped job [your-program] $ echo mango [your-program] mango @@ -17,10 +19,10 @@ Debug = true [your-program] $ jobs [tester::#FY4] ✓ No jobs [your-program] $ sleep 10 & -[your-program] [2] 2382 +[your-program] [2] 2443 [tester::#FY4] ^ Line does not match expected value. [tester::#FY4] Expected: "[1] " -[tester::#FY4] Received: "[2] 2382" +[tester::#FY4] Received: "[2] 2443" [your-program] $  [tester::#FY4] Assertion failed. [tester::#FY4] Test failed diff --git a/internal/test_helpers/fixtures/bash/background_jobs/pass b/internal/test_helpers/fixtures/bash/background_jobs/pass index 42b7686b..69c92cae 100644 --- a/internal/test_helpers/fixtures/bash/background_jobs/pass +++ b/internal/test_helpers/fixtures/bash/background_jobs/pass @@ -13,7 +13,7 @@ Debug = true [tester::#AT7] Running tests for Stage #AT7 (at7) [tester::#AT7] Running ./your_shell.sh [your-program] $ sleep 100 & -[your-program] [1] 2041 +[your-program] [1] 2832 [tester::#AT7] ✓ Received next prompt [your-program] $  [tester::#AT7] Test passed. @@ -21,7 +21,7 @@ Debug = true [tester::#JD6] Running tests for Stage #JD6 (jd6) [tester::#JD6] Running ./your_shell.sh [your-program] $ sleep 100 & -[your-program] [1] 2044 +[your-program] [1] 2834 [tester::#JD6] ✓ Output includes job number with PID [your-program] $ jobs [your-program] [1]+ Running sleep 100 & @@ -32,20 +32,20 @@ Debug = true [tester::#DK5] Running tests for Stage #DK5 (dk5) [tester::#DK5] Running ./your_shell.sh [your-program] $ sleep 100 & -[your-program] [1] 2047 +[your-program] [1] 2836 [tester::#DK5] ✓ Output includes job number with PID [your-program] $ jobs [your-program] [1]+ Running sleep 100 & [tester::#DK5] ✓ 1 entry match the running job [your-program] $ sleep 200 & -[your-program] [2] 2049 +[your-program] [2] 2837 [tester::#DK5] ✓ Output includes job number with PID [your-program] $ jobs [your-program] [1]- Running sleep 100 & [your-program] [2]+ Running sleep 200 & [tester::#DK5] ✓ 2 entries match the running jobs [your-program] $ sleep 300 & -[your-program] [3] 2050 +[your-program] [3] 2838 [tester::#DK5] ✓ Output includes job number with PID [your-program] $ jobs [your-program] [1] Running sleep 100 & @@ -59,7 +59,7 @@ Debug = true [tester::#MA9] Running ./your_shell.sh [tester::#MA9] [setup] mkfifo /tmp/pear-70 [your-program] $ grep raspberry /tmp/pear-70 & -[your-program] [1] 2052 +[your-program] [1] 2840 [tester::#MA9] ✓ Output includes job number with PID [your-program] $ jobs [your-program] [1]+ Running grep raspberry /tmp/pear-70 & @@ -78,13 +78,13 @@ Debug = true [tester::#RQ2] [setup] mkfifo /tmp/mango-16 [tester::#RQ2] Running ./your_shell.sh [your-program] $ sleep 500 & -[your-program] [1] 2055 +[your-program] [1] 2842 [tester::#RQ2] ✓ Output includes job number with PID [your-program] $ grep mango /tmp/blueberry-26 & -[your-program] [2] 2056 +[your-program] [2] 2843 [tester::#RQ2] ✓ Output includes job number with PID [your-program] $ grep grape /tmp/mango-16 & -[your-program] [3] 2057 +[your-program] [3] 2844 [tester::#RQ2] ✓ Output includes job number with PID [tester::#RQ2] [setup] echo "mango" > "/tmp/blueberry-26" [your-program] $ mango @@ -111,19 +111,21 @@ Debug = true [tester::#BV8] [setup] mkfifo /tmp/banana-47 [tester::#BV8] Running ./your_shell.sh [your-program] $ sleep 500 & -[your-program] [1] 2060 +[your-program] [1] 2846 [tester::#BV8] ✓ Output includes job number with PID -[your-program] $ grep -q apple /tmp/banana-47 & -[your-program] [2] 2061 +[your-program] $ grep apple /tmp/banana-47 & +[your-program] [2] 2847 [tester::#BV8] ✓ Output includes job number with PID [your-program] $ jobs [your-program] [1]- Running sleep 500 & -[your-program] [2]+ Running grep -q apple /tmp/banana-47 & +[your-program] [2]+ Running grep apple /tmp/banana-47 & [tester::#BV8] ✓ Found 2 entries for the running jobs [tester::#BV8] [setup] echo "apple" > "/tmp/banana-47" -[your-program] $ echo pear +[your-program] $ apple +[tester::#BV8] ✓ Output of 'grep apple /tmp/banana-47' found +[your-program] echo pear [your-program] pear -[your-program] [2]+ Done grep -q apple /tmp/banana-47 +[your-program] [2]+ Done grep apple /tmp/banana-47 [tester::#BV8] ✓ Found command output followed by an entry for the reaped job [your-program] $ jobs [your-program] [1]+ Running sleep 500 & @@ -134,13 +136,15 @@ Debug = true [tester::#FY4] Running tests for Stage #FY4 (fy4) [tester::#FY4] [setup] mkfifo /tmp/banana-39 [tester::#FY4] Running ./your_shell.sh -[your-program] $ grep -q raspberry /tmp/banana-39 & -[your-program] [1] 2064 +[your-program] $ grep raspberry /tmp/banana-39 & +[your-program] [1] 2849 [tester::#FY4] ✓ Output includes job number with PID [tester::#FY4] [setup] echo "raspberry" > "/tmp/banana-39" -[your-program] $ echo banana +[your-program] $ raspberry +[tester::#FY4] ✓ Output of 'grep raspberry /tmp/banana-39' found +[your-program] echo banana [your-program] banana -[your-program] [1]+ Done grep -q raspberry /tmp/banana-39 +[your-program] [1]+ Done grep raspberry /tmp/banana-39 [tester::#FY4] ✓ Received output for echo followed by an entry for the reaped job [your-program] $ echo apple [your-program] apple @@ -148,7 +152,7 @@ Debug = true [your-program] $ jobs [tester::#FY4] ✓ No jobs [your-program] $ sleep 10 & -[your-program] [1] 2066 +[your-program] [1] 2850 [tester::#FY4] ✓ Output includes job number with PID [your-program] $ jobs [your-program] [1]+ Running sleep 10 & @@ -157,18 +161,20 @@ Debug = true [tester::#FY4] [setup] mkfifo /tmp/strawberry-88 [tester::#FY4] Running ./your_shell.sh [your-program] $ sleep 100 & -[your-program] [1] 2068 +[your-program] [1] 2852 [tester::#FY4] ✓ Output includes job number with PID -[your-program] $ grep -q pineapple /tmp/strawberry-88 & -[your-program] [2] 2069 +[your-program] $ grep pineapple /tmp/strawberry-88 & +[your-program] [2] 2853 [tester::#FY4] ✓ Output includes job number with PID [tester::#FY4] [setup] echo "pineapple" > "/tmp/strawberry-88" -[your-program] $ echo apple +[your-program] $ pineapple +[tester::#FY4] ✓ Output of 'grep pineapple /tmp/strawberry-88' found +[your-program] echo apple [your-program] apple -[your-program] [2]+ Done grep -q pineapple /tmp/strawberry-88 +[your-program] [2]+ Done grep pineapple /tmp/strawberry-88 [tester::#FY4] ✓ Received output for echo followed by an entry for the reaped job [your-program] $ sleep 50 & -[your-program] [2] 2071 +[your-program] [2] 2854 [tester::#FY4] ✓ Output includes job number with PID [your-program] $ jobs [your-program] [1]- Running sleep 100 & diff --git a/internal/test_helpers/scenarios/background_jobs_job_number_not_recycled/main.py b/internal/test_helpers/scenarios/background_jobs_job_number_not_recycled/main.py index 482e20d4..c1df8a35 100644 --- a/internal/test_helpers/scenarios/background_jobs_job_number_not_recycled/main.py +++ b/internal/test_helpers/scenarios/background_jobs_job_number_not_recycled/main.py @@ -105,8 +105,6 @@ def run_external(args: list[str], background: bool = False) -> tuple[int | None, proc = subprocess.Popen( [path] + args[1:], env=os.environ, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, ) return (None, proc) proc = subprocess.run(