Skip to content

Commit ccb05ad

Browse files
authored
Merge pull request #125 from codecrafters-io/add-pipelines-tests
Implement custom shell command tests and pipeline stages
2 parents e7797b0 + 4894ec3 commit ccb05ad

29 files changed

+1129
-33
lines changed

.gitignore

-1
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,4 @@ my_exe
1111
anticheat
1212
notes.md
1313
shell_notes.md
14-
1514
.devcontainer

Makefile

+18-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ tests_excluding_ash:
2424
test_cat_against_bsd_cat:
2525
TESTER_DIR=$(shell pwd) go test -count=1 -p 1 -v ./internal/custom_executable/cat/... -system
2626

27+
test_grep_against_bsd_grep:
28+
TESTER_DIR=$(shell pwd) go test -count=1 -p 1 -v ./internal/custom_executable/grep/... -system
29+
2730
test_head_against_bsd_head:
2831
TESTER_DIR=$(shell pwd) go test -count=1 -p 1 -v ./internal/custom_executable/head/... -system
2932

@@ -41,6 +44,7 @@ test_yes_against_bsd_yes:
4144

4245
test_executables_against_their_bsd_counterparts:
4346
make test_cat_against_bsd_cat
47+
make test_grep_against_bsd_grep
4448
make test_head_against_bsd_head
4549
make test_ls_against_bsd_ls
4650
make test_tail_against_bsd_tail
@@ -82,8 +86,9 @@ build_executables:
8286
for arch in $$arches; do \
8387
GOOS="$$os" GOARCH="$$arch" go build -o built_executables/signature_printer_$${os}_$${arch} ./internal/custom_executable/signature_printer/main.go; \
8488
GOOS="$$os" GOARCH="$$arch" go build -o built_executables/cat_$${os}_$${arch} ./internal/custom_executable/cat/cat.go; \
85-
GOOS="$$os" GOARCH="$$arch" go build -o built_executables/ls_$${os}_$${arch} ./internal/custom_executable/ls/ls.go; \
89+
GOOS="$$os" GOARCH="$$arch" go build -o built_executables/grep_$${os}_$${arch} ./internal/custom_executable/grep/grep.go; \
8690
GOOS="$$os" GOARCH="$$arch" go build -o built_executables/head_$${os}_$${arch} ./internal/custom_executable/head/head.go; \
91+
GOOS="$$os" GOARCH="$$arch" go build -o built_executables/ls_$${os}_$${arch} ./internal/custom_executable/ls/ls.go; \
8792
GOOS="$$os" GOARCH="$$arch" go build -o built_executables/tail_$${os}_$${arch} ./internal/custom_executable/tail/tail.go; \
8893
GOOS="$$os" GOARCH="$$arch" go build -o built_executables/wc_$${os}_$${arch} ./internal/custom_executable/wc/wc.go; \
8994
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
146151
{"slug":"wh6","tester_log_prefix":"tester::#wh6","title":"Stage#5: completion with multiple executables"}
147152
endef
148153

154+
define _PIPELINE_STAGES
155+
[ \
156+
{"slug":"br6","tester_log_prefix":"tester::#br6","title":"Stage#1: Basic dual-command pipeline"}, \
157+
{"slug":"ny9","tester_log_prefix":"tester::#ny9","title":"Stage#4: Pipelines with built-ins"}, \
158+
{"slug":"xk3","tester_log_prefix":"tester::#xk3","title":"Stage#6: Multi-command pipelines"} \
159+
]
160+
endef
161+
149162
# Use eval to properly escape the stage arrays
150163
define quote_strings
151164
$(shell echo '$(1)' | sed 's/"/\\"/g')
@@ -170,6 +183,7 @@ QUOTING_STAGES = $(call quote_strings,$(_QUOTING_STAGES))
170183
REDIRECTIONS_STAGES = $(call quote_strings,$(_REDIRECTIONS_STAGES))
171184
COMPLETIONS_STAGES_ZSH = $(call quote_strings,$(_COMPLETION_STAGES_BASE))
172185
COMPLETIONS_STAGES = $(shell echo '$(_COMPLETION_STAGES_BASE)' | sed 's/]$$/, $(_COMPLETIONS_STAGES_COMPLEX)]/' | sed 's/"/\\"/g')
186+
PIPELINE_STAGES = $(call quote_strings,$(_PIPELINE_STAGES))
173187

174188
test_base_w_ash: build
175189
$(call run_test,$(BASE_STAGES),ash)
@@ -201,6 +215,9 @@ test_redirections_w_bash: build
201215
test_completions_w_bash: build
202216
$(call run_test,$(COMPLETIONS_STAGES),bash)
203217

218+
test_pipeline_w_bash: build
219+
$(call run_test,$(PIPELINE_STAGES),bash)
220+
204221
test_base_w_dash: build
205222
$(call run_test,$(BASE_STAGES),dash)
206223

built_executables/grep_darwin_amd64

2.29 MB
Binary file not shown.

built_executables/grep_darwin_arm64

2.28 MB
Binary file not shown.

built_executables/grep_linux_amd64

2.16 MB
Binary file not shown.

built_executables/grep_linux_arm64

2.22 MB
Binary file not shown.

built_executables/head_darwin_amd64

4.42 KB
Binary file not shown.

built_executables/head_darwin_arm64

448 Bytes
Binary file not shown.

built_executables/head_linux_amd64

10.7 KB
Binary file not shown.

built_executables/head_linux_arm64

2.4 KB
Binary file not shown.

internal/assertions/single_line_assertion.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ func (a SingleLineAssertion) Inspect() string {
2727
}
2828

2929
func (a SingleLineAssertion) Run(screenState [][]string, startRowIndex int) (processedRowCount int, err *AssertionError) {
30-
if a.ExpectedOutput == "" {
31-
panic("CodeCrafters Internal Error: ExpectedOutput must be provided")
30+
if a.ExpectedOutput == "" && len(a.FallbackPatterns) == 0 {
31+
panic("CodeCrafters Internal Error: ExpectedOutput or fallbackPatterns must be provided")
3232
}
3333

3434
processedRowCount = 1
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package custom_executable
2+
3+
import "fmt"
4+
5+
func CreateGrepExecutable(outputPath string) error {
6+
err := createExecutableForOSAndArch("grep", outputPath)
7+
if err != nil {
8+
return fmt.Errorf("CodeCrafters Internal Error: creating executable %s failed: %w", "grep", err)
9+
}
10+
return nil
11+
}
+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"os"
7+
"strings"
8+
)
9+
10+
func main() {
11+
if len(os.Args) < 2 {
12+
fmt.Fprintln(os.Stderr, "Usage: grep PATTERN")
13+
os.Exit(2) // Standard exit code for grep usage error
14+
}
15+
16+
pattern := os.Args[1]
17+
scanner := bufio.NewScanner(os.Stdin)
18+
found := false
19+
20+
for scanner.Scan() {
21+
line := scanner.Text()
22+
if strings.Contains(line, pattern) {
23+
fmt.Println(line)
24+
found = true
25+
}
26+
}
27+
28+
if err := scanner.Err(); err != nil {
29+
fmt.Fprintf(os.Stderr, "grep: error reading input: %v\n", err)
30+
os.Exit(2)
31+
}
32+
33+
if !found {
34+
os.Exit(1) // Standard exit code for grep when no lines match
35+
}
36+
37+
os.Exit(0)
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"flag"
7+
"fmt"
8+
"os"
9+
"os/exec"
10+
"path/filepath"
11+
"runtime"
12+
"strings"
13+
"testing"
14+
)
15+
16+
// Pass the -system flag to use system grep instead of custom implementation
17+
// go test ./... -system
18+
// Tests only pass against BSD implementation of grep, not GNU implementation
19+
// Run on darwin only
20+
var useSystemGrep = flag.Bool("system", false, "Use system grep instead of custom implementation")
21+
22+
func getGrepExecutable(t *testing.T) string {
23+
testerDir := filepath.Join(os.Getenv("TESTER_DIR"), "built_executables")
24+
if *useSystemGrep {
25+
return "grep"
26+
}
27+
28+
switch runtime.GOOS {
29+
case "darwin":
30+
switch runtime.GOARCH {
31+
case "arm64":
32+
return filepath.Join(testerDir, "grep_darwin_arm64")
33+
case "amd64":
34+
return filepath.Join(testerDir, "grep_darwin_amd64")
35+
}
36+
case "linux":
37+
switch runtime.GOARCH {
38+
case "amd64":
39+
return filepath.Join(testerDir, "grep_linux_amd64")
40+
case "arm64":
41+
return filepath.Join(testerDir, "grep_linux_arm64")
42+
}
43+
}
44+
t.Fatalf("Unsupported OS: %s", runtime.GOOS)
45+
return ""
46+
}
47+
48+
// runGrep runs the grep executable with given arguments and returns its output and error if any
49+
func runGrep(t *testing.T, stdinContent string, args ...string) (string, string, int, error) {
50+
executable := getGrepExecutable(t)
51+
52+
t.Helper()
53+
prettyPrintCommand(args)
54+
cmd := exec.Command(executable, args...)
55+
56+
if stdinContent != "" {
57+
cmd.Stdin = strings.NewReader(stdinContent)
58+
}
59+
60+
var stdout, stderr bytes.Buffer
61+
cmd.Stdout = &stdout
62+
cmd.Stderr = &stderr
63+
64+
err := cmd.Run()
65+
66+
exitCode := 0
67+
if err != nil {
68+
var exitError *exec.ExitError
69+
if errors.As(err, &exitError) {
70+
exitCode = exitError.ExitCode()
71+
}
72+
}
73+
74+
return stdout.String(), stderr.String(), exitCode, err // Return err as well
75+
}
76+
77+
func prettyPrintCommand(args []string) {
78+
// Basic pretty printing, similar to cat/wc tests
79+
displayArgs := make([]string, len(args))
80+
copy(displayArgs, args)
81+
// Potentially shorten paths or quote arguments if needed here
82+
83+
out := fmt.Sprintf("=== RUN: > grep %s", strings.Join(displayArgs, " "))
84+
fmt.Println(out)
85+
}
86+
87+
// TestGrepStdin tests grep functionality reading from standard input.
88+
func TestGrepStdin(t *testing.T) {
89+
tests := []struct {
90+
name string
91+
pattern string
92+
input string
93+
expectedOut string
94+
expectedErr string
95+
expectedExit int
96+
}{
97+
{
98+
name: "Simple match",
99+
pattern: "hello",
100+
input: "hello world\nthis is a test\nhello again",
101+
expectedOut: "hello world\nhello again\n",
102+
expectedErr: "",
103+
expectedExit: 0,
104+
},
105+
{
106+
name: "No match",
107+
pattern: "goodbye",
108+
input: "hello world\nthis is a test",
109+
expectedOut: "",
110+
expectedErr: "",
111+
expectedExit: 1,
112+
},
113+
{
114+
name: "Empty input",
115+
pattern: "test",
116+
input: "",
117+
expectedOut: "",
118+
expectedErr: "",
119+
expectedExit: 1,
120+
},
121+
{
122+
name: "Match on empty string pattern",
123+
pattern: "", // Our simple strings.Contain matches everything
124+
input: "line1\nline2",
125+
expectedOut: "line1\nline2\n",
126+
expectedErr: "",
127+
expectedExit: 0,
128+
// Note: Real grep might error or behave differently with empty pattern
129+
},
130+
{
131+
name: "Usage error - no pattern",
132+
pattern: "", // Args will be empty
133+
input: "test",
134+
expectedOut: "",
135+
expectedErr: "Usage: grep PATTERN\n",
136+
expectedExit: 2,
137+
},
138+
}
139+
140+
for _, tt := range tests {
141+
t.Run(tt.name, func(t *testing.T) {
142+
var args []string
143+
144+
if *useSystemGrep && tt.name == "Usage error - no pattern" {
145+
return // Skip this test for System grep
146+
}
147+
if tt.name != "Usage error - no pattern" {
148+
args = append(args, tt.pattern)
149+
} // else args remains empty
150+
151+
stdout, stderr, exitCode, runErr := runGrep(t, tt.input, args...)
152+
153+
// Check exit code first, as stderr/stdout might be irrelevant if exit code is wrong
154+
if exitCode != tt.expectedExit {
155+
// Include stdout/stderr in the error message for better debugging
156+
t.Errorf("Expected exit code %d, got %d. Stderr: %q, Stdout: %q, RunErr: %v",
157+
tt.expectedExit, exitCode, stderr, stdout, runErr)
158+
}
159+
160+
// Now check stdout and stderr
161+
if stdout != tt.expectedOut {
162+
t.Errorf("Expected stdout %q, got %q", tt.expectedOut, stdout)
163+
}
164+
165+
if stderr != tt.expectedErr {
166+
t.Errorf("Expected stderr %q, got %q", tt.expectedErr, stderr)
167+
}
168+
169+
// Handle expected usage error specifically
170+
if tt.expectedExit == 2 && runErr == nil {
171+
t.Errorf("Expected a non-nil error for usage error case, but got nil")
172+
} else if tt.expectedExit != 2 && runErr != nil {
173+
// If we didn't expect an error exit, but got one, check if it was *exec.ExitError
174+
var exitError *exec.ExitError
175+
if !errors.As(runErr, &exitError) {
176+
// It was some other error during execution
177+
t.Errorf("Command execution failed unexpectedly: %v", runErr)
178+
}
179+
// If it *was* an ExitError, we already checked the exit code above.
180+
}
181+
})
182+
}
183+
}

internal/custom_executable/head/head.go

+29-8
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"bufio"
5+
"errors"
56
"fmt"
67
"io"
78
"os"
@@ -237,7 +238,7 @@ func processBytes(reader io.Reader, byteCount int) error {
237238
// Print first N bytes
238239
buffer := make([]byte, byteCount)
239240
n, err := io.ReadFull(reader, buffer)
240-
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
241+
if err != nil && err != io.EOF && !errors.Is(err, io.ErrUnexpectedEOF) {
241242
return err
242243
}
243244
os.Stdout.Write(buffer[:n])
@@ -264,20 +265,40 @@ func processLines(reader io.Reader, lineCount int) error {
264265
return nil
265266
}
266267

267-
scanner := bufio.NewScanner(reader)
268-
269268
if lineCount > 0 {
270269
// Print first N lines
271270
linesRead := 0
272-
for scanner.Scan() && linesRead < lineCount {
273-
fmt.Println(scanner.Text())
274-
linesRead++
271+
// Use bufio.Reader to preserve original line endings
272+
bufReader := bufio.NewReader(reader)
273+
for linesRead < lineCount {
274+
lineBytes, err := bufReader.ReadBytes('\n')
275+
if len(lineBytes) > 0 {
276+
// Write the bytes read, including the newline if present
277+
_, writeErr := os.Stdout.Write(lineBytes)
278+
if writeErr != nil {
279+
return writeErr
280+
}
281+
// Only increment linesRead if we actually wrote something that ended with a newline
282+
// or if it was the last line of the file without a newline
283+
if err == nil || (err == io.EOF && len(lineBytes) > 0) {
284+
linesRead++
285+
}
286+
}
287+
if err != nil {
288+
if err == io.EOF {
289+
break // Reached end of file
290+
}
291+
return err // Return other errors
292+
}
275293
}
276-
return scanner.Err()
294+
return nil // Success
277295
}
278296

279297
// For negative line count (all but last N lines)
280-
// We need to keep a rolling buffer of the last N lines
298+
// Scanner logic is likely okay here as we print lines *before* the last N
299+
// Keep existing scanner logic for negative count
300+
scanner := bufio.NewScanner(reader)
301+
281302
absCount := -lineCount
282303
var lines []string
283304
for scanner.Scan() {

internal/custom_executable/tail/tail_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,7 @@ func TestTailFollow(t *testing.T) {
586586
output.Write(buf[:n])
587587
// Send partial output or wait until done? Let's send full when done.
588588
}
589-
if err == io.EOF || err == io.ErrClosedPipe {
589+
if err == io.EOF || errors.Is(err, io.ErrClosedPipe) {
590590
break
591591
}
592592
if err != nil {
@@ -606,7 +606,7 @@ func TestTailFollow(t *testing.T) {
606606
if n > 0 {
607607
output.Write(buf[:n])
608608
}
609-
if err == io.EOF || err == io.ErrClosedPipe {
609+
if err == io.EOF || errors.Is(err, io.ErrClosedPipe) {
610610
break
611611
}
612612
if err != nil {

0 commit comments

Comments
 (0)