diff --git a/.gitignore b/.gitignore index bfc90337..6f68989a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,5 +11,4 @@ my_exe anticheat notes.md shell_notes.md - .devcontainer \ No newline at end of file diff --git a/Makefile b/Makefile index 9faefef5..13f14fe8 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,9 @@ tests_excluding_ash: test_cat_against_bsd_cat: TESTER_DIR=$(shell pwd) go test -count=1 -p 1 -v ./internal/custom_executable/cat/... -system +test_grep_against_bsd_grep: + TESTER_DIR=$(shell pwd) go test -count=1 -p 1 -v ./internal/custom_executable/grep/... -system + test_head_against_bsd_head: TESTER_DIR=$(shell pwd) go test -count=1 -p 1 -v ./internal/custom_executable/head/... -system @@ -41,6 +44,7 @@ test_yes_against_bsd_yes: test_executables_against_their_bsd_counterparts: make test_cat_against_bsd_cat + make test_grep_against_bsd_grep make test_head_against_bsd_head make test_ls_against_bsd_ls make test_tail_against_bsd_tail @@ -82,8 +86,9 @@ build_executables: for arch in $$arches; do \ GOOS="$$os" GOARCH="$$arch" go build -o built_executables/signature_printer_$${os}_$${arch} ./internal/custom_executable/signature_printer/main.go; \ GOOS="$$os" GOARCH="$$arch" go build -o built_executables/cat_$${os}_$${arch} ./internal/custom_executable/cat/cat.go; \ - GOOS="$$os" GOARCH="$$arch" go build -o built_executables/ls_$${os}_$${arch} ./internal/custom_executable/ls/ls.go; \ + GOOS="$$os" GOARCH="$$arch" go build -o built_executables/grep_$${os}_$${arch} ./internal/custom_executable/grep/grep.go; \ GOOS="$$os" GOARCH="$$arch" go build -o built_executables/head_$${os}_$${arch} ./internal/custom_executable/head/head.go; \ + GOOS="$$os" GOARCH="$$arch" go build -o built_executables/ls_$${os}_$${arch} ./internal/custom_executable/ls/ls.go; \ GOOS="$$os" GOARCH="$$arch" go build -o built_executables/tail_$${os}_$${arch} ./internal/custom_executable/tail/tail.go; \ GOOS="$$os" GOARCH="$$arch" go build -o built_executables/wc_$${os}_$${arch} ./internal/custom_executable/wc/wc.go; \ GOOS="$$os" GOARCH="$$arch" go build -o built_executables/yes_$${os}_$${arch} ./internal/custom_executable/yes/yes.go; \ @@ -146,6 +151,14 @@ define _COMPLETIONS_STAGES_COMPLEX {"slug":"wh6","tester_log_prefix":"tester::#wh6","title":"Stage#5: completion with multiple executables"} endef +define _PIPELINE_STAGES +[ \ + {"slug":"br6","tester_log_prefix":"tester::#br6","title":"Stage#1: Basic dual-command pipeline"}, \ + {"slug":"ny9","tester_log_prefix":"tester::#ny9","title":"Stage#4: Pipelines with built-ins"}, \ + {"slug":"xk3","tester_log_prefix":"tester::#xk3","title":"Stage#6: Multi-command pipelines"} \ +] +endef + # Use eval to properly escape the stage arrays define quote_strings $(shell echo '$(1)' | sed 's/"/\\"/g') @@ -170,6 +183,7 @@ QUOTING_STAGES = $(call quote_strings,$(_QUOTING_STAGES)) REDIRECTIONS_STAGES = $(call quote_strings,$(_REDIRECTIONS_STAGES)) COMPLETIONS_STAGES_ZSH = $(call quote_strings,$(_COMPLETION_STAGES_BASE)) COMPLETIONS_STAGES = $(shell echo '$(_COMPLETION_STAGES_BASE)' | sed 's/]$$/, $(_COMPLETIONS_STAGES_COMPLEX)]/' | sed 's/"/\\"/g') +PIPELINE_STAGES = $(call quote_strings,$(_PIPELINE_STAGES)) test_base_w_ash: build $(call run_test,$(BASE_STAGES),ash) @@ -201,6 +215,9 @@ test_redirections_w_bash: build test_completions_w_bash: build $(call run_test,$(COMPLETIONS_STAGES),bash) +test_pipeline_w_bash: build + $(call run_test,$(PIPELINE_STAGES),bash) + test_base_w_dash: build $(call run_test,$(BASE_STAGES),dash) diff --git a/built_executables/grep_darwin_amd64 b/built_executables/grep_darwin_amd64 new file mode 100755 index 00000000..42b339e4 Binary files /dev/null and b/built_executables/grep_darwin_amd64 differ diff --git a/built_executables/grep_darwin_arm64 b/built_executables/grep_darwin_arm64 new file mode 100755 index 00000000..f5db831b Binary files /dev/null and b/built_executables/grep_darwin_arm64 differ diff --git a/built_executables/grep_linux_amd64 b/built_executables/grep_linux_amd64 new file mode 100755 index 00000000..5c742316 Binary files /dev/null and b/built_executables/grep_linux_amd64 differ diff --git a/built_executables/grep_linux_arm64 b/built_executables/grep_linux_arm64 new file mode 100755 index 00000000..f716419b Binary files /dev/null and b/built_executables/grep_linux_arm64 differ diff --git a/built_executables/head_darwin_amd64 b/built_executables/head_darwin_amd64 index eba9162b..e8ec433c 100755 Binary files a/built_executables/head_darwin_amd64 and b/built_executables/head_darwin_amd64 differ diff --git a/built_executables/head_darwin_arm64 b/built_executables/head_darwin_arm64 index e0bd4997..e3958822 100755 Binary files a/built_executables/head_darwin_arm64 and b/built_executables/head_darwin_arm64 differ diff --git a/built_executables/head_linux_amd64 b/built_executables/head_linux_amd64 index ae3f0268..723f29d1 100755 Binary files a/built_executables/head_linux_amd64 and b/built_executables/head_linux_amd64 differ diff --git a/built_executables/head_linux_arm64 b/built_executables/head_linux_arm64 index 77c6fce5..37e69378 100755 Binary files a/built_executables/head_linux_arm64 and b/built_executables/head_linux_arm64 differ diff --git a/internal/assertions/single_line_assertion.go b/internal/assertions/single_line_assertion.go index 73cfd377..85b4589b 100644 --- a/internal/assertions/single_line_assertion.go +++ b/internal/assertions/single_line_assertion.go @@ -27,8 +27,8 @@ func (a SingleLineAssertion) Inspect() string { } func (a SingleLineAssertion) Run(screenState [][]string, startRowIndex int) (processedRowCount int, err *AssertionError) { - if a.ExpectedOutput == "" { - panic("CodeCrafters Internal Error: ExpectedOutput must be provided") + if a.ExpectedOutput == "" && len(a.FallbackPatterns) == 0 { + panic("CodeCrafters Internal Error: ExpectedOutput or fallbackPatterns must be provided") } processedRowCount = 1 diff --git a/internal/custom_executable/build/grep.go b/internal/custom_executable/build/grep.go new file mode 100644 index 00000000..57d244c1 --- /dev/null +++ b/internal/custom_executable/build/grep.go @@ -0,0 +1,11 @@ +package custom_executable + +import "fmt" + +func CreateGrepExecutable(outputPath string) error { + err := createExecutableForOSAndArch("grep", outputPath) + if err != nil { + return fmt.Errorf("CodeCrafters Internal Error: creating executable %s failed: %w", "grep", err) + } + return nil +} diff --git a/internal/custom_executable/grep/grep.go b/internal/custom_executable/grep/grep.go new file mode 100644 index 00000000..e6e3505b --- /dev/null +++ b/internal/custom_executable/grep/grep.go @@ -0,0 +1,38 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "strings" +) + +func main() { + if len(os.Args) < 2 { + fmt.Fprintln(os.Stderr, "Usage: grep PATTERN") + os.Exit(2) // Standard exit code for grep usage error + } + + pattern := os.Args[1] + scanner := bufio.NewScanner(os.Stdin) + found := false + + for scanner.Scan() { + line := scanner.Text() + if strings.Contains(line, pattern) { + fmt.Println(line) + found = true + } + } + + if err := scanner.Err(); err != nil { + fmt.Fprintf(os.Stderr, "grep: error reading input: %v\n", err) + os.Exit(2) + } + + if !found { + os.Exit(1) // Standard exit code for grep when no lines match + } + + os.Exit(0) +} diff --git a/internal/custom_executable/grep/grep_test.go b/internal/custom_executable/grep/grep_test.go new file mode 100644 index 00000000..a5efef53 --- /dev/null +++ b/internal/custom_executable/grep/grep_test.go @@ -0,0 +1,183 @@ +package main + +import ( + "bytes" + "errors" + "flag" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" +) + +// Pass the -system flag to use system grep instead of custom implementation +// go test ./... -system +// Tests only pass against BSD implementation of grep, not GNU implementation +// Run on darwin only +var useSystemGrep = flag.Bool("system", false, "Use system grep instead of custom implementation") + +func getGrepExecutable(t *testing.T) string { + testerDir := filepath.Join(os.Getenv("TESTER_DIR"), "built_executables") + if *useSystemGrep { + return "grep" + } + + switch runtime.GOOS { + case "darwin": + switch runtime.GOARCH { + case "arm64": + return filepath.Join(testerDir, "grep_darwin_arm64") + case "amd64": + return filepath.Join(testerDir, "grep_darwin_amd64") + } + case "linux": + switch runtime.GOARCH { + case "amd64": + return filepath.Join(testerDir, "grep_linux_amd64") + case "arm64": + return filepath.Join(testerDir, "grep_linux_arm64") + } + } + t.Fatalf("Unsupported OS: %s", runtime.GOOS) + return "" +} + +// runGrep runs the grep executable with given arguments and returns its output and error if any +func runGrep(t *testing.T, stdinContent string, args ...string) (string, string, int, error) { + executable := getGrepExecutable(t) + + t.Helper() + prettyPrintCommand(args) + cmd := exec.Command(executable, args...) + + if stdinContent != "" { + cmd.Stdin = strings.NewReader(stdinContent) + } + + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + + exitCode := 0 + if err != nil { + var exitError *exec.ExitError + if errors.As(err, &exitError) { + exitCode = exitError.ExitCode() + } + } + + return stdout.String(), stderr.String(), exitCode, err // Return err as well +} + +func prettyPrintCommand(args []string) { + // Basic pretty printing, similar to cat/wc tests + displayArgs := make([]string, len(args)) + copy(displayArgs, args) + // Potentially shorten paths or quote arguments if needed here + + out := fmt.Sprintf("=== RUN: > grep %s", strings.Join(displayArgs, " ")) + fmt.Println(out) +} + +// TestGrepStdin tests grep functionality reading from standard input. +func TestGrepStdin(t *testing.T) { + tests := []struct { + name string + pattern string + input string + expectedOut string + expectedErr string + expectedExit int + }{ + { + name: "Simple match", + pattern: "hello", + input: "hello world\nthis is a test\nhello again", + expectedOut: "hello world\nhello again\n", + expectedErr: "", + expectedExit: 0, + }, + { + name: "No match", + pattern: "goodbye", + input: "hello world\nthis is a test", + expectedOut: "", + expectedErr: "", + expectedExit: 1, + }, + { + name: "Empty input", + pattern: "test", + input: "", + expectedOut: "", + expectedErr: "", + expectedExit: 1, + }, + { + name: "Match on empty string pattern", + pattern: "", // Our simple strings.Contain matches everything + input: "line1\nline2", + expectedOut: "line1\nline2\n", + expectedErr: "", + expectedExit: 0, + // Note: Real grep might error or behave differently with empty pattern + }, + { + name: "Usage error - no pattern", + pattern: "", // Args will be empty + input: "test", + expectedOut: "", + expectedErr: "Usage: grep PATTERN\n", + expectedExit: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var args []string + + if *useSystemGrep && tt.name == "Usage error - no pattern" { + return // Skip this test for System grep + } + if tt.name != "Usage error - no pattern" { + args = append(args, tt.pattern) + } // else args remains empty + + stdout, stderr, exitCode, runErr := runGrep(t, tt.input, args...) + + // Check exit code first, as stderr/stdout might be irrelevant if exit code is wrong + if exitCode != tt.expectedExit { + // Include stdout/stderr in the error message for better debugging + t.Errorf("Expected exit code %d, got %d. Stderr: %q, Stdout: %q, RunErr: %v", + tt.expectedExit, exitCode, stderr, stdout, runErr) + } + + // Now check stdout and stderr + if stdout != tt.expectedOut { + t.Errorf("Expected stdout %q, got %q", tt.expectedOut, stdout) + } + + if stderr != tt.expectedErr { + t.Errorf("Expected stderr %q, got %q", tt.expectedErr, stderr) + } + + // Handle expected usage error specifically + if tt.expectedExit == 2 && runErr == nil { + t.Errorf("Expected a non-nil error for usage error case, but got nil") + } else if tt.expectedExit != 2 && runErr != nil { + // If we didn't expect an error exit, but got one, check if it was *exec.ExitError + var exitError *exec.ExitError + if !errors.As(runErr, &exitError) { + // It was some other error during execution + t.Errorf("Command execution failed unexpectedly: %v", runErr) + } + // If it *was* an ExitError, we already checked the exit code above. + } + }) + } +} diff --git a/internal/custom_executable/head/head.go b/internal/custom_executable/head/head.go index 35071d77..4cfb19fa 100644 --- a/internal/custom_executable/head/head.go +++ b/internal/custom_executable/head/head.go @@ -2,6 +2,7 @@ package main import ( "bufio" + "errors" "fmt" "io" "os" @@ -237,7 +238,7 @@ func processBytes(reader io.Reader, byteCount int) error { // Print first N bytes buffer := make([]byte, byteCount) n, err := io.ReadFull(reader, buffer) - if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF { + if err != nil && err != io.EOF && !errors.Is(err, io.ErrUnexpectedEOF) { return err } os.Stdout.Write(buffer[:n]) @@ -264,20 +265,40 @@ func processLines(reader io.Reader, lineCount int) error { return nil } - scanner := bufio.NewScanner(reader) - if lineCount > 0 { // Print first N lines linesRead := 0 - for scanner.Scan() && linesRead < lineCount { - fmt.Println(scanner.Text()) - linesRead++ + // Use bufio.Reader to preserve original line endings + bufReader := bufio.NewReader(reader) + for linesRead < lineCount { + lineBytes, err := bufReader.ReadBytes('\n') + if len(lineBytes) > 0 { + // Write the bytes read, including the newline if present + _, writeErr := os.Stdout.Write(lineBytes) + if writeErr != nil { + return writeErr + } + // Only increment linesRead if we actually wrote something that ended with a newline + // or if it was the last line of the file without a newline + if err == nil || (err == io.EOF && len(lineBytes) > 0) { + linesRead++ + } + } + if err != nil { + if err == io.EOF { + break // Reached end of file + } + return err // Return other errors + } } - return scanner.Err() + return nil // Success } // For negative line count (all but last N lines) - // We need to keep a rolling buffer of the last N lines + // Scanner logic is likely okay here as we print lines *before* the last N + // Keep existing scanner logic for negative count + scanner := bufio.NewScanner(reader) + absCount := -lineCount var lines []string for scanner.Scan() { diff --git a/internal/custom_executable/tail/tail_test.go b/internal/custom_executable/tail/tail_test.go index a4d184e3..6d8a0a17 100644 --- a/internal/custom_executable/tail/tail_test.go +++ b/internal/custom_executable/tail/tail_test.go @@ -586,7 +586,7 @@ func TestTailFollow(t *testing.T) { output.Write(buf[:n]) // Send partial output or wait until done? Let's send full when done. } - if err == io.EOF || err == io.ErrClosedPipe { + if err == io.EOF || errors.Is(err, io.ErrClosedPipe) { break } if err != nil { @@ -606,7 +606,7 @@ func TestTailFollow(t *testing.T) { if n > 0 { output.Write(buf[:n]) } - if err == io.EOF || err == io.ErrClosedPipe { + if err == io.EOF || errors.Is(err, io.ErrClosedPipe) { break } if err != nil { diff --git a/internal/stage_p1.go b/internal/stage_p1.go new file mode 100644 index 00000000..a56abdf9 --- /dev/null +++ b/internal/stage_p1.go @@ -0,0 +1,121 @@ +package internal + +import ( + "fmt" + "path" + "strings" + + "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/shell-tester/internal/test_cases" + "github.com/codecrafters-io/tester-utils/random" + "github.com/codecrafters-io/tester-utils/test_case_harness" +) + +func testP1(stageHarness *test_case_harness.TestCaseHarness) error { + logger := stageHarness.Logger + shell := shell_executable.NewShellExecutable(stageHarness) + _, err := SetUpCustomCommands(stageHarness, shell, []CommandDetails{ + {CommandType: "cat", CommandName: CUSTOM_CAT_COMMAND, CommandMetadata: ""}, + {CommandType: "head", CommandName: CUSTOM_HEAD_COMMAND, CommandMetadata: ""}, + {CommandType: "tail", CommandName: CUSTOM_TAIL_COMMAND, CommandMetadata: ""}, + {CommandType: "wc", CommandName: CUSTOM_WC_COMMAND, CommandMetadata: ""}, + }, false) + if err != nil { + return err + } + + asserter := logged_shell_asserter.NewLoggedShellAsserter(shell) + + // Test-1 + randomDir, err := GetShortRandomDirectory(stageHarness) + if err != nil { + return err + } + + filePath := path.Join(randomDir, fmt.Sprintf("file-%d", random.RandomInt(1, 100))) + randomWords := random.RandomWords(10) + fileContent := fmt.Sprintf("%s %s\n%s %s\n%s %s\n%s %s\n%s %s", randomWords[0], randomWords[1], randomWords[2], randomWords[3], randomWords[4], randomWords[5], randomWords[6], randomWords[7], randomWords[8], randomWords[9]) + + lines := strings.Count(fileContent, "\n") + 1 + words := strings.Count(strings.ReplaceAll(fileContent, "\n", " "), " ") + 1 + bytes := len(fileContent) + + if err := asserter.StartShellAndAssertPrompt(true); err != nil { + return err + } + + input := fmt.Sprintf(`cat %s | wc`, filePath) + expectedOutput := fmt.Sprintf("%8d%8d%8d", lines, words, bytes) + + if err := writeFiles([]string{filePath}, []string{fileContent}, logger); err != nil { + return err + } + + testCase := test_cases.CommandResponseTestCase{ + Command: input, + ExpectedOutput: expectedOutput, + SuccessMessage: "✓ Received expected response", + } + if err := testCase.Run(asserter, shell, logger); err != nil { + return err + } + + // Test-2 + randomDir, err = GetShortRandomDirectory(stageHarness) + if err != nil { + return err + } + + filePath = path.Join(randomDir, fmt.Sprintf("file-%d", random.RandomInt(1, 100))) + randomWords = random.RandomWords(6) + fileContent = fmt.Sprintf("%s %s\n%s %s\n%s %s\n", randomWords[0], randomWords[1], randomWords[2], randomWords[3], randomWords[4], randomWords[5]) + if err := writeFiles([]string{filePath}, []string{fileContent}, logger); err != nil { + return err + } + + 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, + } + if err := multiLineTestCase.Run(asserter, shell, logger); err != nil { + return err + } + + // Append content to the file while command is running + if err := appendFile(filePath, "This is line 4.\n"); err != nil { + return err + } + + firstSingleLineAssertion := assertions.SingleLineAssertion{ + ExpectedOutput: "This is line 4.", + } + asserter.AddAssertion(&firstSingleLineAssertion) + + if err := asserter.AssertWithoutPrompt(); err != nil { + return err + } + logger.Successf("✓ Received appended line 4") + + // Append again + if err := appendFile(filePath, "This is line 5.\n"); err != nil { + return err + } + + secondSingleLineAssertion := assertions.SingleLineAssertion{ + ExpectedOutput: "This is line 5.", + } + asserter.AddAssertion(&secondSingleLineAssertion) + + if err := asserter.AssertWithoutPrompt(); err != nil { + return err + } + logger.Successf("✓ Received appended line 5") + + return logAndQuit(asserter, nil) +} diff --git a/internal/stage_p2.go b/internal/stage_p2.go new file mode 100644 index 00000000..7cf0c67e --- /dev/null +++ b/internal/stage_p2.go @@ -0,0 +1,67 @@ +package internal + +import ( + "fmt" + "regexp" + "strings" + + "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 testP2(stageHarness *test_case_harness.TestCaseHarness) error { + logger := stageHarness.Logger + shell := shell_executable.NewShellExecutable(stageHarness) + _, err := SetUpCustomCommands(stageHarness, shell, []CommandDetails{ + {CommandType: "wc", CommandName: CUSTOM_WC_COMMAND, CommandMetadata: ""}, + }, false) + if err != nil { + return err + } + + asserter := logged_shell_asserter.NewLoggedShellAsserter(shell) + + if err := asserter.StartShellAndAssertPrompt(true); err != nil { + return err + } + + // Test-1 + data := fmt.Sprintf(`%s\n%s`, random.RandomWord(), random.RandomWord()) + lines := strings.Count(data, `\n`) + words := strings.Count(strings.ReplaceAll(data, "\n", " "), " ") + 1 + bytes := len(data) + + input := fmt.Sprintf(`echo %s | wc`, data) + expectedOutput := fmt.Sprintf("%8d%8d%8d", lines, words, bytes) + + singleLineTestCase := test_cases.CommandResponseTestCase{ + Command: input, + ExpectedOutput: expectedOutput, + FallbackPatterns: nil, + SuccessMessage: "✓ Received expected output", + } + if err := singleLineTestCase.Run(asserter, shell, logger); err != nil { + return err + } + + // Test-2 + + input = `ls | type exit` + expectedOutput = `exit is a shell builtin` + + singleLineTestCase = test_cases.CommandResponseTestCase{ + Command: input, + ExpectedOutput: expectedOutput, + FallbackPatterns: []*regexp.Regexp{ + regexp.MustCompile(`exit is a special shell builtin`)}, + SuccessMessage: "✓ Received expected output", + } + if err := singleLineTestCase.Run(asserter, shell, logger); err != nil { + return err + } + + return logAndQuit(asserter, nil) +} diff --git a/internal/stage_p3.go b/internal/stage_p3.go new file mode 100644 index 00000000..5b6174d1 --- /dev/null +++ b/internal/stage_p3.go @@ -0,0 +1,100 @@ +package internal + +import ( + "fmt" + "path" + "regexp" + "sort" + "strings" + + "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 testP3(stageHarness *test_case_harness.TestCaseHarness) error { + logger := stageHarness.Logger + shell := shell_executable.NewShellExecutable(stageHarness) + _, err := SetUpCustomCommands(stageHarness, shell, []CommandDetails{ + {CommandType: "cat", CommandName: CUSTOM_CAT_COMMAND, CommandMetadata: ""}, + {CommandType: "head", CommandName: CUSTOM_HEAD_COMMAND, CommandMetadata: ""}, + {CommandType: "wc", CommandName: CUSTOM_WC_COMMAND, CommandMetadata: ""}, + }, false) + if err != nil { + return err + } + + asserter := logged_shell_asserter.NewLoggedShellAsserter(shell) + + // Test-1 + randomDir, err := GetShortRandomDirectory(stageHarness) + if err != nil { + return err + } + filePath := path.Join(randomDir, fmt.Sprintf("file-%d", random.RandomInt(1, 100))) + randomWords := random.RandomWords(5) + fileContent := fmt.Sprintf("%s\n%s\n%s\n%s\n%s", randomWords[0], randomWords[1], randomWords[2], randomWords[3], randomWords[4]) + if err := writeFiles([]string{filePath}, []string{fileContent}, logger); err != nil { + return err + } + + if err := asserter.StartShellAndAssertPrompt(true); err != nil { + return err + } + + lines := strings.Count(fileContent, "\n") + 1 + words := strings.Count(strings.ReplaceAll(fileContent, "\n", " "), " ") + 1 + bytes := len(fileContent) + + input := fmt.Sprintf(`cat %s | head -n 5 | wc`, filePath) + expectedOutput := fmt.Sprintf("%8d%8d%8d", lines, words, bytes) + + singleLineTestCase := test_cases.CommandResponseTestCase{ + Command: input, + ExpectedOutput: expectedOutput, + FallbackPatterns: nil, + SuccessMessage: "✓ Received expected output", + } + if err := singleLineTestCase.Run(asserter, shell, logger); err != nil { + return err + } + + // Test-2 + newRandomDir, err := GetShortRandomDirectory(stageHarness) + if err != nil { + return err + } + randomUniqueFileNames := random.RandomInts(1, 100, 6) + filePaths := []string{ + path.Join(newRandomDir, fmt.Sprintf("f-%d", randomUniqueFileNames[0])), + path.Join(newRandomDir, fmt.Sprintf("f-%d", randomUniqueFileNames[1])), + path.Join(newRandomDir, fmt.Sprintf("f-%d", randomUniqueFileNames[2])), + path.Join(newRandomDir, fmt.Sprintf("f-%d", randomUniqueFileNames[3])), + path.Join(newRandomDir, fmt.Sprintf("f-%d", randomUniqueFileNames[4])), + path.Join(newRandomDir, fmt.Sprintf("f-%d", randomUniqueFileNames[5])), + } + fileContents := random.RandomWords(6) + if err := writeFiles(filePaths, fileContents, logger); err != nil { + return err + } + + sort.Ints(randomUniqueFileNames) + availableEntries := randomUniqueFileNames[1:4] + + input = fmt.Sprintf(`ls -la %s | tail -n 5 | head -n 3 | grep "f-%d"`, newRandomDir, availableEntries[2]) + expectedRegexPattern := fmt.Sprintf("^[rwx-]* .* f-%d", availableEntries[2]) + + singleLineTestCase2 := test_cases.CommandResponseTestCase{ + Command: input, + ExpectedOutput: "NIL", + FallbackPatterns: []*regexp.Regexp{regexp.MustCompile(expectedRegexPattern)}, + SuccessMessage: "✓ Received expected output", + } + if err := singleLineTestCase2.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 8344a015..e9d7934b 100644 --- a/internal/stages_test.go +++ b/internal/stages_test.go @@ -69,6 +69,13 @@ func TestStages(t *testing.T) { StdoutFixturePath: "./test_helpers/fixtures/bash/completions/pass", NormalizeOutputFunc: normalizeTesterOutput, }, + "pipelines_pass_bash": { + StageSlugs: []string{"br6", "ny9", "xk3"}, + CodePath: "./test_helpers/bash", + ExpectedExitCode: 0, + StdoutFixturePath: "./test_helpers/fixtures/bash/pipelines/pass", + NormalizeOutputFunc: normalizeTesterOutput, + }, "base_pass_ash": { UntilStageSlug: "ip1", CodePath: "./test_helpers/ash", @@ -106,6 +113,13 @@ func TestStages(t *testing.T) { StdoutFixturePath: "./test_helpers/fixtures/ash/completions/pass", NormalizeOutputFunc: normalizeTesterOutput, }, + "pipelines_pass_ash": { + StageSlugs: []string{"br6", "ny9", "xk3"}, + CodePath: "./test_helpers/ash", + ExpectedExitCode: 0, + StdoutFixturePath: "./test_helpers/fixtures/ash/pipelines/pass", + NormalizeOutputFunc: normalizeTesterOutput, + }, } if runtime.GOOS == "darwin" { @@ -131,6 +145,7 @@ func normalizeTesterOutput(testerOutput []byte) []byte { "/bin/$1": {regexp.MustCompile(`\/usr/bin/(\w+)`)}, "[your-program] my_exe is ": {regexp.MustCompile(`\[your-program\] .{4}my_exe is .*`)}, "[your-program] ": {regexp.MustCompile(`\[your-program\] .{4}/(workspaces|home|Users)/.*`)}, + "ls-la-output-line": {regexp.MustCompile(`-rw-r--r-- .*`)}, } for replacement, regexes := range replacements { diff --git a/internal/test_cases/command_multiple_completions_test_case.go b/internal/test_cases/command_multiple_completions_test_case.go index 05fc70f8..47d0b295 100644 --- a/internal/test_cases/command_multiple_completions_test_case.go +++ b/internal/test_cases/command_multiple_completions_test_case.go @@ -67,7 +67,7 @@ func (t CommandMultipleCompletionsTestCase) Run(asserter *logged_shell_asserter. // Send TAB for i := range t.TabCount { - shouldRingBell := (i == 0 && t.CheckForBell) + shouldRingBell := i == 0 && t.CheckForBell logTab(logger, t.ExpectedReflection, shouldRingBell) // Node's readline doesn't register 2nd tab if sent instantly 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 b03ba1d7..bf640697 100644 --- a/internal/test_cases/command_with_multiline_response_test_case.go +++ b/internal/test_cases/command_with_multiline_response_test_case.go @@ -18,6 +18,9 @@ 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 } func (t CommandWithMultilineResponseTestCase) Run(asserter *logged_shell_asserter.LoggedShellAsserter, shell *shell_executable.ShellExecutable, logger *logger.Logger) error { @@ -32,8 +35,14 @@ func (t CommandWithMultilineResponseTestCase) Run(asserter *logged_shell_asserte asserter.AddAssertion(&t.MultiLineAssertion) - if err := asserter.AssertWithPrompt(); err != nil { - return err + if !t.SkipPromptAssertion { + if err := asserter.AssertWithPrompt(); err != nil { + return err + } + } else { + if err := asserter.AssertWithoutPrompt(); err != nil { + return err + } } logger.Successf("%s", t.SuccessMessage) diff --git a/internal/test_helpers/course_definition.yml b/internal/test_helpers/course_definition.yml index 09372b03..3e141d7f 100644 --- a/internal/test_helpers/course_definition.yml +++ b/internal/test_helpers/course_definition.yml @@ -19,6 +19,7 @@ languages: - slug: "c" - slug: "cpp" - slug: "csharp" + - slug: "gleam" - slug: "go" - slug: "java" - slug: "javascript" @@ -70,6 +71,20 @@ extensions: Redirection allows you to redirect the output of a command to a file or another command. + - slug: "completions" + name: "Autocompletion" + description_markdown: | + In this challenge extension, you'll add programmable completion support to your shell. + + Programmable completion allows you to autocomplete commands and executable files. + + - slug: "pipelines" + name: "Pipelines" + description_markdown: | + In this challenge extension, you'll add support for pipelines to your shell. + + Pipelines allow you to connect multiple commands together, so the output of one command becomes the input of the next command. + stages: - slug: "oo8" name: "Print a prompt" @@ -291,7 +306,7 @@ stages: The tester will execute your program with a custom `PATH` like this: ```bash - PATH="/usr/bin:/usr/local/bin" ./your_program.sh + PATH="/usr/bin:/usr/local/bin:$PATH" ./your_program.sh ``` It'll then send a series of `type` commands to your shell: @@ -313,6 +328,7 @@ stages: - The actual value of the `PATH` environment variable will be random for each test case. - `PATH` can contain multiple directories separated by colons (`:`), your program should search for programs in each directory in order and return the first match. + - Some commands, such as `echo`, can exist as both builtin commands and executable files. In such cases, the `type` command should identify them as builtins. marketing_md: |- In this stage, you'll implement the `type` builtin command for your shell. @@ -344,11 +360,12 @@ stages: The command (`custom_exe_1234`) in the example above will be present in `PATH` and will be an executable file. - The tester will check if your shell correctly executes the given command and prints the output. + The executable file will print information about the arguments it was passed along with a random "program signature". The tester will verify that your program prints output from the executable. ### Notes - The program name, arguments and the expected output will be random for each test case. + - The output in the example ("Program was passed N args...") comes from the executable. It's not something you need to implement manually. marketing_md: |- In this stage, you'll implement the ability for your shell to run external programs with arguments. @@ -555,12 +572,16 @@ stages: Then it will also send a `cat` command, with the file name parameter enclosed in single quotes: ```bash - $ cat '/tmp/file name' '/tmp/file name with spaces' + $ cat '/tmp/file name' '/tmp/file name with spaces' content1 content2 ``` The tester will check if the `cat` command correctly prints the file content. + ### Notes + + - The `cat` command is an executable available on most systems, so there’s no need to implement it yourself. + marketing_md: |- In this stage, you'll implement support for quoting with single quotes. @@ -596,7 +617,7 @@ stages: Then it will also send a `cat` command, with the file name parameter enclosed in double quotes: ```bash - $ cat "/tmp/file name" "/tmp/'file name' with spaces" + $ cat "/tmp/file name" "/tmp/'file name' with spaces" content1 content2 ``` @@ -637,7 +658,7 @@ stages: Then it will also send a `cat` command, with the file name parameters consisting of backslashes inside quotes: ```bash - $ cat "/tmp/file\\name" "/tmp/file\ name" + $ cat "/tmp/file\\name" "/tmp/file\ name" content1 content2 ``` @@ -678,7 +699,7 @@ stages: Then it will also send a `cat` command, with the file name parameters consisting of backslashes inside single quotes: ```bash - $ cat "/tmp/file/'name'" "/tmp/file/'\name\'" + $ cat "/tmp/file/'name'" "/tmp/file/'\name\'" content1 content2 ``` @@ -719,7 +740,7 @@ stages: Then it will also send a `cat` command, with the file name parameters consisting of backslashes inside double quotes: ```bash - $ cat "/tmp/"file\name"" "/tmp/"file name"" + $ cat "/tmp/"file\name"" "/tmp/"file name"" content1 content2 ``` @@ -931,8 +952,332 @@ stages: In this stage, you'll implement support for appending the standard error of a command to a file. - slug: "qp2" + primary_extension_slug: "completions" + name: "Builtin completion" + difficulty: medium + description_md: |- + In this stage, you'll implement support for autocompleting builtin commands. + + Your shell should be able to complete builtin commands when the user presses the `` key. Specifically, you'll need to implement completion for the `echo` and `exit` builtins. + + ### Tests + + The tester will execute your program like this: + + ```bash + ./your_program.sh + ``` + + It will then send the following inputs, simulating user input and tab presses: + + 1. **Input:** `ech` + * The tester expects the prompt to display `echo ` after the tab press. + + 2. **Input:** `exi` + * The tester expects the prompt to display `exit ` after the tab press. + + The tester checks if the completion works as expected and if your shell outputs the correct output for `echo` and `exit` command. + Note the space at the end of the completion. + + ### Notes: + + - We recommend using a library like [readline](https://en.wikipedia.org/wiki/GNU_Readline) for your implementation. Most modern shells and REPLs (like the Python REPL) use readline under the hood. + - Different shells handle autocompletion differently. For consistency, we recommend using [Bash](https://www.gnu.org/software/bash/) for development and testing. + + marketing_md: |- + In this stage, you'll implement support for autocompleting builtin commands. + - slug: "gm9" + primary_extension_slug: "completions" + name: "Completion with arguments" + difficulty: medium + description_md: |- + In this stage, you'll extend your shell's tab completion to handle commands with arguments. + + Your shell should now not only complete the command itself but also correctly handle the subsequent arguments that the user types. + This means that after completing the command with ``, it should allow the user to continue typing arguments, and those arguments should also be interpreted correctly. + You'll need to ensure commands like `echo ` and `type` autocomplete and still function correctly with arguments. + + ### Tests + + The tester will execute your program like this: + + ```bash + ./your_program.sh + ``` + + The tests will simulate user input with tab presses and will execute builtin commands, similar to the previous stage, with added arguments: + + 1. **Input:** `ech` `hello` + * The tester expects the shell to first complete the `ech` to `echo` after ``, then accept the `hello` argument, and after the `` key press, execute `echo hello`. + * The shell should output `hello`. + + 2. **Input:** `typ` `type` + * The tester expects the shell to first complete `typ` to `type` after ``, then accept the `type` argument, and after the `` key press, execute `type type`. + * The shell should output `type is a shell builtin`. + + The tester will verify that your shell properly completes the commands and executes the commands with the given arguments. + + marketing_md: |- + In this stage, you'll implement support for allowing arguments to be used after completion. + - slug: "qm8" + primary_extension_slug: "completions" + name: "Missing completions" + difficulty: easy + description_md: |- + In this stage, you'll refine your shell's tab completion behavior to handle cases where the user types an invalid command. + + When the user types a command that is not a known builtin and presses ``, your shell should not attempt to autocomplete it. Instead, it should just keep what the user typed and should ring a bell. + This means that if you type "xyz" and press ``, the command should not change and you should hear a bell indicating that there are no valid completion options for "xyz". + + ### Tests + + The tester will execute your program like this: + + ```bash + ./your_program.sh + ``` + + The tests will simulate the user typing an invalid command and pressing the `` key: + + 1. **Input:** `xyz` + * The tester will first type `xyz` and then press ``. The tester expects that the prompt still shows "xyz" and there is a bell sound. + + The tester will verify that your shell does not attempt completion on invalid commands, the bell is sent. + The bell is sent by printing the `\a` character. + + marketing_md: |- + In this stage, you'll implement support for handling invalid commands gracefully. + - slug: "gy5" + primary_extension_slug: "completions" + name: "Executable completion" + difficulty: medium + description_md: |- + In this stage, you'll extend your shell's tab completion to include external executable files in the user's `PATH`. + + Your shell should now be able to complete commands that are not built-ins, but exist as executable files in the directories listed in the `PATH` environment variable. + When the user types the beginning of an external command and presses ``, your shell should complete the command to the full executable file name. + This means that if you have a command `custom_executable` in the path and type `custom` and press ``, the shell should complete that to `custom_executable`. + + ### Tests + + The tester will execute your program like this: + + ```bash + ./your_program.sh + ``` + + Before executing your shell, the tester will create an executable file named `custom_executable` and add its directory to the `PATH`. + + The test will simulate the user typing the start of the external command and pressing ``: + + 1. **Input:** `custom` + * The tester types "custom" and presses ``. The tester expects that the prompt line changes to `custom_executable `. + + The tester will verify that your shell correctly completes the command to the external executable file name. + Note the space at the end of the completion. + + marketing_md: |- + In this stage, you'll implement support for autocompleting external executables. + - slug: "wh6" + primary_extension_slug: "completions" + name: "Multiple completions" + difficulty: hard + description_md: |- + In this stage, you'll implement tab completion for executables, specifically when multiple executables share a common prefix. + + When the user types a command prefix and presses ``, your shell should: + + - Identify all executables in the `PATH` that match the prefix. + - If there are multiple matches, + - On the first TAB press, just ring a bell. (`\a` rings the bell) + - On the second TAB press, print all the matching executables separated by 2 spaces, on the next line, and follow it with the prompt on a new line. + + ### Tests + + The tester will execute your program like this: + + ```bash + ./your_program.sh + ``` + + It will then set up a specific `PATH` and place multiple executables starting with a common prefix into different directories in the `PATH`. Finally, it will type the common prefix, and then press the Tab key twice. + + ```bash + $ ./your_program.sh + $ xyz_ + xyz_bar xyz_baz xyz_quz + $ xyz_ + ``` + + The tester will verify that: + + 1. Your shell displays the prompt with the common prefix after receiving the partial command. + 2. Upon the first Tab key press, your shell prints a bell character. + 3. Upon the second Tab key press, your shell prints the list of matching executables separated by 2 spaces, on the next line, and follow it with the prompt on a new line. + + marketing_md: |- + In this stage, you'll implement support for handling multiple completions. + - slug: "wt6" + primary_extension_slug: "completions" + name: "Partial completions" + difficulty: hard + description_md: |- + In this stage, you'll extend your shell's tab completion to handle cases with multiple matching executables where one is a prefix of another. + + When the user types a partial command and presses the Tab key, your shell should attempt to complete the command name. If there are multiple executable files in the PATH that match the prefix, and one of those matches is a prefix of another, then the shell should complete to the longest common prefix of all matching executables. If there is only one match after performing completion, then the shell should complete the command name as in previous stages. + + For example, if `xyz_foo`, `xyz_foo_bar`, and `xyz_foo_bar_baz` are all available executables and the user types `xyz_` and presses tab, then your shell should complete the command to `xyz_foo`. If the user then types `_` and presses tab again, it should complete to `xyz_foo_bar`. If the user then types `_` and presses tab again, it should complete to `xyz_foo_bar_baz`. + + ### Tests + + The tester will execute your program like this: + + ```bash + ./your_program.sh + ``` + + It will then set up a specific `PATH` and place executables `xyz_foo`, `xyz_foo_bar`, and `xyz_foo_bar_baz` into different directories in the `PATH`. Finally, it will type `xyz_` and then press Tab, then type `_` and press Tab, then type `_` and press Tab. + + ```bash + $ export PATH=/tmp/bar:$PATH + $ export PATH=/tmp/baz:$PATH + $ export PATH=/tmp/qux:$PATH + $ ./your_program.sh + $ xyz_ + $ xyz_foo_ + $ xyz_foo_bar_ + $ xyz_foo_bar_baz + ``` + Note: The prompt lines above are on the same line. + + The tester will verify that: + + 1. After typing `xyz_` and pressing Tab, your shell completes to `xyz_foo`. + 2. After typing `_`, the prompt line matches `$ xyz_foo_`. + 3. After typing `_` and pressing Tab, your shell completes to `xyz_foo_bar`. + 4. After typing `_`, the prompt line matches `$ xyz_foo_bar_`. + 5. After typing `_` and pressing Tab, your shell completes to `xyz_foo_bar_baz`. + 6. The prompt line matches `$ xyz_foo_bar_baz ` after the final completion. + + marketing_md: |- + In this stage, you'll implement support for handling multiple completions with common prefixes. + + - slug: "br6" + primary_extension_slug: "pipelines" + name: "Dual-command pipeline" + difficulty: hard + description_md: |- + In this stage, you'll implement support for basic pipelines involving two external commands. + + A [pipeline](https://www.gnu.org/software/bash/manual/bash.html#Pipelines) connects the standard output of one command to the standard input of the next command using the `|` operator. + + ### Tests + + The tester will execute your program like this: + + ```bash + ./your_program.sh + ``` + + It'll then send commands involving a two-stage pipeline: + + ```bash + $ cat /tmp/foo/file | wc + 5 10 77 + $ tail -f /tmp/foo/file-1 | head -n 5 + raspberry strawberry + pear mango + pineapple apple + # (tester appends more lines to /tmp/foo/file-1) + # (And expects the running command to keep printing new lines) + This is line 4. + This is line 5. + $ + ``` + + The tester will check if the final output matches the expected output after the pipeline execution. For the `tail -f` command, the tester will append content to the the input file while the pipeline is running. + + ### Notes + + - The executables (`cat`, `wc`, `tail`, `head`) will be available in the `PATH`. + - You need to handle creating a pipe, forking processes for each command, and setting up the standard input/output redirection between them. + marketing_md: |- + Implement support for basic two-command pipelines like `command1 | command2`. + + - slug: "ny9" + primary_extension_slug: "pipelines" + name: "Pipelines with built-ins" + difficulty: hard + description_md: |- + In this stage, you'll extend pipeline support to include shell built-in commands. + + Built-in commands (like `echo`, `type`) need to be handled correctly when they appear as part of a pipeline, whether at the beginning, middle, or end. + + ### Tests + + The tester will execute your program like this: + + ```bash + ./your_program.sh + ``` + + It'll then send commands involving pipelines with built-ins: + + ```bash + $ echo raspberry\\nblueberry | wc + 1 1 20 + $ ls | type exit + exit is a shell builtin + $ + ``` + + The tester will check if the final output matches the expected output after the pipeline execution, correctly handling the built-in commands. + For the `type` command, the tester will check if the command correctly handles the built-in command and prints the correct output, the `ls` output is not supposed to be printed. + + ### Notes + + - Built-in commands don't typically involve creating a new process via `fork`/`exec`. You'll need to handle their execution within the shell process while still managing the input/output redirection required by the pipeline. + marketing_md: |- + Extend pipeline support to handle built-in commands like `echo` or `type` within pipelines. + + - slug: "xk3" + primary_extension_slug: "pipelines" + name: "Multi-command pipelines" + difficulty: hard + description_md: |- + In this stage, you'll implement support for pipelines involving more than two commands. + + Pipelines can chain multiple commands together, connecting the output of each command to the input of the next one. + + ### Tests + + The tester will execute your program like this: + + ```bash + ./your_program.sh + ``` + + It'll then send commands involving pipelines with three or more stages: + + ```bash + $ cat /tmp/foo/file | head -n 5 | wc + 5 5 10 + $ ls -la /tmp/foo | tail -n 5 | head -n 3 | grep "file" + -rw-r--r-- 1 user user 5 Apr 29 10:06 file + $ + ``` + + The tester will check if the final output matches the expected output after the multi-stage pipeline execution. + + ### Notes + + - This requires managing multiple pipes and processes. + - Ensure correct setup of stdin/stdout for each command in the chain (except the first command's stdin and the last command's stdout, which usually connect to the terminal or file redirections). + - Proper process cleanup and waiting are crucial. + marketing_md: |- + Implement support for multi-command pipelines like `command1 | command2 | command3`. \ No newline at end of file diff --git a/internal/test_helpers/fixtures/ash/pipelines/pass b/internal/test_helpers/fixtures/ash/pipelines/pass new file mode 100644 index 00000000..9cf08441 --- /dev/null +++ b/internal/test_helpers/fixtures/ash/pipelines/pass @@ -0,0 +1,51 @@ +Debug = true + +[stage-3] Running tests for Stage #3: br6 +[stage-3] [setup] export PATH=/tmp/pear/orange/raspberry:$PATH +[stage-3] Running ./your_shell.sh +[stage-3] [setup] echo -n "strawberry apple\npear banana\nraspberry mango\nblueberry pineapple\norange grape" > "/tmp/foo/file-24" +[your-program] $ cat /tmp/foo/file-24 | wc +[your-program]  5 10 77 +[stage-3] ✓ Received expected response +[stage-3] [setup] echo -n "orange blueberry\nstrawberry pineapple\nbanana mango" > "/tmp/baz/file-3" +[your-program] $ tail -f /tmp/baz/file-3 | head -n 5 +[your-program] orange blueberry +[your-program] strawberry pineapple +[your-program] banana mango +[stage-3] ✓ Received redirected file content +[your-program] This is line 4. +[stage-3] ✓ Received appended line 4 +[your-program] This is line 5. +[stage-3] ✓ Received appended line 5 +[stage-3] Test passed. + +[stage-2] Running tests for Stage #2: ny9 +[stage-2] [setup] export PATH=/tmp/apple/blueberry/banana:$PATH +[stage-2] Running ./your_shell.sh +[your-program] $ echo apple\npineapple | wc +[your-program]  1 1 16 +[stage-2] ✓ Received expected output +[your-program] $ ls | type exit +[your-program] exit is a special shell builtin +[stage-2] ✓ Received expected output +[your-program] $ +[stage-2] Test passed. + +[stage-1] Running tests for Stage #1: xk3 +[stage-1] [setup] export PATH=/tmp/blueberry/grape/raspberry:$PATH +[stage-1] [setup] echo -n "raspberry\norange\napple\npear\nmango" > "/tmp/foo/file-25" +[stage-1] Running ./your_shell.sh +[your-program] $ cat /tmp/foo/file-25 | head -n 5 | wc +[your-program]  5 5 33 +[stage-1] ✓ Received expected output +[stage-1] [setup] echo -n "apple" > "/tmp/qux/f-56" +[stage-1] [setup] echo -n "banana" > "/tmp/qux/f-17" +[stage-1] [setup] echo -n "pineapple" > "/tmp/qux/f-81" +[stage-1] [setup] echo -n "grape" > "/tmp/qux/f-2" +[stage-1] [setup] echo -n "orange" > "/tmp/qux/f-20" +[stage-1] [setup] echo -n "mango" > "/tmp/qux/f-16" +[your-program] $ ls -la /tmp/qux | tail -n 5 | head -n 3 | grep "f-20" +[your-program] -rw-r--r-- 1 vscode vscode 6 Apr 29 08:45 f-20 +[stage-1] ✓ Received expected output +[your-program] $ +[stage-1] Test passed. diff --git a/internal/test_helpers/fixtures/bash/pipelines/pass b/internal/test_helpers/fixtures/bash/pipelines/pass new file mode 100644 index 00000000..98f50f68 --- /dev/null +++ b/internal/test_helpers/fixtures/bash/pipelines/pass @@ -0,0 +1,51 @@ +Debug = true + +[stage-3] Running tests for Stage #3: br6 +[stage-3] [setup] export PATH=/tmp/pear/orange/raspberry:$PATH +[stage-3] Running ./your_shell.sh +[stage-3] [setup] echo -n "strawberry apple\npear banana\nraspberry mango\nblueberry pineapple\norange grape" > "/tmp/foo/file-24" +[your-program] $ cat /tmp/foo/file-24 | wc +[your-program]  5 10 77 +[stage-3] ✓ Received expected response +[stage-3] [setup] echo -n "orange blueberry\nstrawberry pineapple\nbanana mango" > "/tmp/baz/file-3" +[your-program] $ tail -f /tmp/baz/file-3 | head -n 5 +[your-program] orange blueberry +[your-program] strawberry pineapple +[your-program] banana mango +[stage-3] ✓ Received redirected file content +[your-program] This is line 4. +[stage-3] ✓ Received appended line 4 +[your-program] This is line 5. +[stage-3] ✓ Received appended line 5 +[stage-3] Test passed. + +[stage-2] Running tests for Stage #2: ny9 +[stage-2] [setup] export PATH=/tmp/apple/blueberry/banana:$PATH +[stage-2] Running ./your_shell.sh +[your-program] $ echo apple\npineapple | wc +[your-program]  1 1 16 +[stage-2] ✓ Received expected output +[your-program] $ ls | type exit +[your-program] exit is a shell builtin +[stage-2] ✓ Received expected output +[your-program] $ +[stage-2] Test passed. + +[stage-1] Running tests for Stage #1: xk3 +[stage-1] [setup] export PATH=/tmp/blueberry/grape/raspberry:$PATH +[stage-1] [setup] echo -n "raspberry\norange\napple\npear\nmango" > "/tmp/foo/file-25" +[stage-1] Running ./your_shell.sh +[your-program] $ cat /tmp/foo/file-25 | head -n 5 | wc +[your-program]  5 5 33 +[stage-1] ✓ Received expected output +[stage-1] [setup] echo -n "apple" > "/tmp/qux/f-56" +[stage-1] [setup] echo -n "banana" > "/tmp/qux/f-17" +[stage-1] [setup] echo -n "pineapple" > "/tmp/qux/f-81" +[stage-1] [setup] echo -n "grape" > "/tmp/qux/f-2" +[stage-1] [setup] echo -n "orange" > "/tmp/qux/f-20" +[stage-1] [setup] echo -n "mango" > "/tmp/qux/f-16" +[your-program] $ ls -la /tmp/qux | tail -n 5 | head -n 3 | grep "f-20" +[your-program] -rw-r--r-- 1 vscode vscode 6 Apr 29 08:45 f-20 +[stage-1] ✓ Received expected output +[your-program] $ +[stage-1] Test passed. diff --git a/internal/tester_definition.go b/internal/tester_definition.go index ccecdd70..42e950ed 100644 --- a/internal/tester_definition.go +++ b/internal/tester_definition.go @@ -150,5 +150,20 @@ var testerDefinition = tester_definition.TesterDefinition{ TestFunc: testA6, Timeout: 15 * time.Second, }, + { + Slug: "br6", + TestFunc: testP1, + Timeout: 15 * time.Second, + }, + { + Slug: "ny9", + TestFunc: testP2, + Timeout: 15 * time.Second, + }, + { + Slug: "xk3", + TestFunc: testP3, + Timeout: 15 * time.Second, + }, }, } diff --git a/internal/utils.go b/internal/utils.go index a947a79c..fbf3dfea 100644 --- a/internal/utils.go +++ b/internal/utils.go @@ -11,8 +11,12 @@ import ( var SMALL_WORDS = []string{"foo", "bar", "baz", "qux", "quz"} var LARGE_WORDS = []string{"hello", "world", "test", "example", "shell", "script"} -const CUSTOM_LS_COMMAND = "ls" const CUSTOM_CAT_COMMAND = "cat" +const CUSTOM_HEAD_COMMAND = "head" +const CUSTOM_LS_COMMAND = "ls" +const CUSTOM_TAIL_COMMAND = "tail" +const CUSTOM_WC_COMMAND = "wc" +const CUSTOM_YES_COMMAND = "yes" type testCaseContent struct { Input string diff --git a/internal/utils_build.go b/internal/utils_build.go index eda6c42f..152b23a2 100644 --- a/internal/utils_build.go +++ b/internal/utils_build.go @@ -1,6 +1,7 @@ package internal import ( + "fmt" "path" custom_executable "github.com/codecrafters-io/shell-tester/internal/custom_executable/build" @@ -19,9 +20,8 @@ type CommandDetails struct { // CommandName is the name of the generated executable, e.g. "custom_exe_1234" CommandName string // CommandMetadata is any other metadata required for generating the command - // SignaturePrinter: random code - // Ls: nothing - // Cat: nothing + // signaturePrinter: random code + // cat, grep, head, ls, tail, wc, yes: nothing CommandMetadata string } @@ -43,15 +43,45 @@ func SetUpCustomCommands(stageHarness *test_case_harness.TestCaseHarness, shell for _, commandDetail := range commands { switch commandDetail.CommandType { + case "cat": + customCatPath := path.Join(executableDir, commandDetail.CommandName) + err = custom_executable.CreateCatExecutable(customCatPath) + if err != nil { + return "", err + } + case "grep": + customGrepPath := path.Join(executableDir, commandDetail.CommandName) + err = custom_executable.CreateGrepExecutable(customGrepPath) + if err != nil { + return "", err + } + case "head": + customHeadPath := path.Join(executableDir, commandDetail.CommandName) + err = custom_executable.CreateHeadExecutable(customHeadPath) + if err != nil { + return "", err + } case "ls": customLsPath := path.Join(executableDir, commandDetail.CommandName) err = custom_executable.CreateLsExecutable(customLsPath) if err != nil { return "", err } - case "cat": - customCatPath := path.Join(executableDir, commandDetail.CommandName) - err = custom_executable.CreateCatExecutable(customCatPath) + case "tail": + customTailPath := path.Join(executableDir, commandDetail.CommandName) + err = custom_executable.CreateTailExecutable(customTailPath) + if err != nil { + return "", err + } + case "wc": + customWcPath := path.Join(executableDir, commandDetail.CommandName) + err = custom_executable.CreateWcExecutable(customWcPath) + if err != nil { + return "", err + } + case "yes": + customYesPath := path.Join(executableDir, commandDetail.CommandName) + err = custom_executable.CreateYesExecutable(customYesPath) if err != nil { return "", err } @@ -61,6 +91,8 @@ func SetUpCustomCommands(stageHarness *test_case_harness.TestCaseHarness, shell if err != nil { return "", err } + default: + panic(fmt.Sprintf("CodeCrafters Internal Error: unknown command type %s", commandDetail.CommandType)) } } stageHarness.Logger.ResetSecondaryPrefix() diff --git a/internal/utils_fs.go b/internal/utils_fs.go index 7c4f683f..fa7677e0 100644 --- a/internal/utils_fs.go +++ b/internal/utils_fs.go @@ -102,8 +102,25 @@ func GetShortRandomDirectories(stageHarness *test_case_harness.TestCaseHarness, } // writeFile writes a file to the given path with the given content -func writeFile(path string, content string) error { - return os.WriteFile(path, []byte(content), 0644) +func writeFile(filePath string, content string) error { + return os.WriteFile(filePath, []byte(content), 0644) +} + +func appendFile(filePath string, content string) error { + file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer file.Close() + if _, err := file.WriteString(content); err != nil { + return err + } + + // ensure the file is flushed to disk immediately + // we don't care about the error here + file.Sync() + + return nil } // writeFiles writes a list of files to the given paths with the given contents