Skip to content

Implement custom shell command tests and pipeline stages #125

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 45 commits into from
Apr 30, 2025
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
6304ea8
feat: implement testP1 function for shell command testing
ryan-gang Apr 22, 2025
7284dc8
refactor: improve file writing logic in testP1 function
ryan-gang Apr 22, 2025
e14caf7
feat: add testP2 function for custom shell command testing
ryan-gang Apr 22, 2025
2819379
feat: add additional custom commands for shell utilities
ryan-gang Apr 22, 2025
dbd8c37
feat: add testP1 and testP2 to tester definition
ryan-gang Apr 22, 2025
a7951ca
feat: implement testP3 function for custom shell command testing
ryan-gang Apr 22, 2025
8fea7df
feat: add pipeline stages and implement testP3 function for testing
ryan-gang Apr 22, 2025
467c5d6
feat: update course definition with corrected cat command examples an…
ryan-gang Apr 23, 2025
91ade74
feat: enhance testP3 function to write file content and add prompt as…
ryan-gang Apr 27, 2025
424d86b
feat: enhance testP3 function to append content to file during execut…
ryan-gang Apr 27, 2025
837952f
feat: enhance testP3 function to append additional content and assert…
ryan-gang Apr 27, 2025
d1bcbe0
refactor: update how empty lines are handled in singleLineAssertion
ryan-gang Apr 27, 2025
3beaae8
feat: add testP4 function to validate piping from builtins
ryan-gang Apr 27, 2025
0fdc314
feat: add testP5 function to validate piping into builtins
ryan-gang Apr 27, 2025
46377a5
feat: add testP6 function to validate multi command pipelines
ryan-gang Apr 27, 2025
707257e
fix: add panic for missing ExpectedOutput and FallbackPatterns in Sin…
ryan-gang Apr 28, 2025
142a5ce
revert: undo change made to single_line_assertion
ryan-gang Apr 28, 2025
b04bd84
refactor: improve file handling in writeFile and appendFile functions
ryan-gang Apr 28, 2025
87b4c78
chore: add tests to tester context
ryan-gang Apr 28, 2025
49163fb
feat: add internal grep implementation
ryan-gang Apr 28, 2025
ebd636c
feat: add additional test case for directory listing and file verific…
ryan-gang Apr 28, 2025
b62ff1b
refactor: merge stages
ryan-gang Apr 28, 2025
8d83848
refactor: merge stages
ryan-gang Apr 28, 2025
1160437
feat: add grep test and build steps to Makefile
ryan-gang Apr 28, 2025
8dd88a3
refactor: rename pipeline stages and update test function mappings
ryan-gang Apr 28, 2025
e65f068
feat: add test cases for bash and ash pipeline stages
ryan-gang Apr 28, 2025
2de9842
fix: update fallback patterns in test case for shell builtin exit
ryan-gang Apr 28, 2025
db8a2a5
refactor: remove unused pipeline stage slugs from course definition
ryan-gang Apr 28, 2025
ae742f0
fix: update .gitignore to remove trailing slash from .devcontainer
ryan-gang Apr 28, 2025
fb7e177
refactor: remove commented-out code in grep test function
ryan-gang Apr 28, 2025
2e4debc
feat: add regex pattern for ls -la output normalization in tests
ryan-gang Apr 28, 2025
d621300
Merge branch 'main' into add-pipelines-tests
ryan-gang Apr 29, 2025
fff2ad3
fix: ensure ExpectedOutput or fallbackPatterns are provided in Single…
ryan-gang Apr 29, 2025
fed0f89
fix: trim newlines from fileContent before splitting into expectedMul…
ryan-gang Apr 29, 2025
ab0ac88
feat: add custom executable creation for additional commands in SetUp…
ryan-gang Apr 29, 2025
dc6b11d
feat: add function to create grep executable for custom commands
ryan-gang Apr 29, 2025
decf73d
fix: improve line reading logic to preserve original line endings in …
ryan-gang Apr 29, 2025
00ad590
fix: adjust expected output formatting for line, word, and byte count…
ryan-gang Apr 29, 2025
7e7f6bd
fix: remove redundant file write operation in testP1 function
ryan-gang Apr 29, 2025
dd8567f
test: update fixtures
ryan-gang Apr 29, 2025
255b42e
refactor: remove build step for grep executable in tests
ryan-gang Apr 29, 2025
51adce2
chore: update pipeline stage titles for clarity and improve command m…
ryan-gang Apr 29, 2025
076853e
chore: fix lint issues
ryan-gang Apr 29, 2025
4baeada
fix: correct command syntax in Makefile for testing grep
ryan-gang Apr 30, 2025
4894ec3
chore: update course def
ryan-gang Apr 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ anticheat
notes.md
main.go
shell_notes.md
.devcontainer
19 changes: 18 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -41,6 +44,7 @@ test_yes_against_bsd_yes:

test_executables_against_their_bsd_counterparts:
make test_cat_against_bsd_cat
test_grep_against_bsd_grep
make test_head_against_bsd_head
make test_ls_against_bsd_ls
make test_tail_against_bsd_tail
Expand Down Expand Up @@ -81,8 +85,9 @@ build_executables:
for os in $$oses; do \
for arch in $$arches; do \
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; \
Expand Down Expand Up @@ -145,6 +150,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: Pipeline"}, \
{"slug":"ny9","tester_log_prefix":"tester::#ny9","title":"Stage#4: Pipeline"}, \
{"slug":"xk3","tester_log_prefix":"tester::#xk3","title":"Stage#6: Pipeline"} \
]
endef

# Use eval to properly escape the stage arrays
define quote_strings
$(shell echo '$(1)' | sed 's/"/\\"/g')
Expand All @@ -169,6 +182,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)
Expand Down Expand Up @@ -200,6 +214,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)

Expand Down
Binary file added built_executables/grep_darwin_amd64
Binary file not shown.
Binary file added built_executables/grep_darwin_arm64
Binary file not shown.
Binary file added built_executables/grep_linux_amd64
Binary file not shown.
Binary file added built_executables/grep_linux_arm64
Binary file not shown.
2 changes: 1 addition & 1 deletion internal/assertions/single_line_assertion.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ 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")
return 0, nil
}

processedRowCount = 1
Expand Down
38 changes: 38 additions & 0 deletions internal/custom_executable/grep/grep.go
Original file line number Diff line number Diff line change
@@ -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)
}
209 changes: 209 additions & 0 deletions internal/custom_executable/grep/grep_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
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) {
// Build step needed before running tests if not using system grep
// and executable isn't pre-built by another process.
if !*useSystemGrep {
// We need to ensure the executable exists. Let's build it.
// Assuming we are in the 'grep' directory when 'go test' is run.
buildCmd := exec.Command("go", "build", "-o", "grep", ".")
buildOutput, err := buildCmd.CombinedOutput()
if err != nil {
t.Fatalf("Failed to build grep executable for testing: %v\nOutput:\n%s", err, string(buildOutput))
}
// Ensure cleanup of the built executable
t.Cleanup(func() {
os.Remove("./grep")
})
}

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()
} else {

Check failure on line 87 in internal/custom_executable/grep/grep_test.go

View workflow job for this annotation

GitHub Actions / lint

empty branch (SA9003)
// If it's not an ExitError, it might be a different execution problem (e.g., command not found)
// We return it so the test can decide how to handle it.
}
}

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.Contains 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
if _, ok := runErr.(*exec.ExitError); !ok {
// 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.
}
})
}
}

// TODO: Add tests for file arguments once file handling is implemented in grep.go
// func TestGrepFile(t *testing.T) { ... }

// Helper functions for file creation and cleanup (can be copied from cat_test.go/wc_test.go if needed)
// type testFile struct { ... }
// func createTestFiles(t *testing.T, dir string, files []testFile) { ... }
// func cleanupDirectories(dirs []string) { ... }
Loading
Loading