Commit c49dd9f

mo khan <mo@mokhan.ca>
2025-07-31 17:07:50
test: add unit tests
1 parent 6881bd7
pkg/procfile/testdata/env/variables.procfile
@@ -0,0 +1,3 @@
+web: ./server -port $PORT
+worker: ./worker -env $NODE_ENV
+database: psql -h $DB_HOST -U $DB_USER
\ No newline at end of file
pkg/procfile/testdata/invalid/empty-command.procfile
@@ -0,0 +1,3 @@
+web: 
+worker: echo 'this is valid'
+empty:
\ No newline at end of file
pkg/procfile/testdata/invalid/no-colon.procfile
@@ -0,0 +1,3 @@
+web echo 'missing colon'
+worker: echo 'this one is valid'
+invalid line without colon
\ No newline at end of file
pkg/procfile/testdata/valid/basic.procfile
@@ -0,0 +1,1 @@
+web: echo 'Hello World'
\ No newline at end of file
pkg/procfile/testdata/valid/multiple.procfile
@@ -0,0 +1,3 @@
+web: ./server -port 8080
+worker: ./worker -queue tasks
+redis: redis-server --port 6379
\ No newline at end of file
pkg/procfile/testdata/valid/with-comments.procfile
@@ -0,0 +1,7 @@
+# This is a comment
+web: echo 'Web server'
+
+# Another comment
+worker: echo 'Background worker'
+
+# Empty lines and comments should be ignored
\ No newline at end of file
pkg/procfile/parse.go
@@ -0,0 +1,44 @@
+package procfile
+
+import (
+	"bufio"
+	"io"
+	"os"
+	"strings"
+)
+
+func ParseFile(path string) ([]*Proc, error) {
+	file, err := os.Open(path)
+	if err != nil {
+		return nil, err
+	}
+	defer file.Close()
+
+	return Parse(file), nil
+}
+
+func Parse(file io.Reader) []*Proc {
+	var processes []*Proc
+	scanner := bufio.NewScanner(file)
+
+	for scanner.Scan() {
+		line := strings.TrimSpace(scanner.Text())
+		if line == "" || strings.HasPrefix(line, "#") {
+			continue
+		}
+
+		parts := strings.SplitN(line, ":", 2)
+		if len(parts) != 2 {
+			continue
+		}
+
+		args := strings.Fields(os.ExpandEnv(strings.TrimSpace(parts[1])))
+		if len(args) == 0 {
+			continue
+		}
+
+		processes = append(processes, New(parts[0], args))
+	}
+
+	return processes
+}
pkg/procfile/parse_test.go
@@ -0,0 +1,163 @@
+package procfile
+
+import (
+	"os"
+	"strings"
+	"testing"
+)
+
+func TestParse(t *testing.T) {
+	t.Run("TestParseBasic", func(t *testing.T) {
+		input := "web: echo 'Hello World'\n"
+		procs := Parse(strings.NewReader(input))
+
+		if len(procs) != 1 {
+			t.Errorf("Expected 1 process, got %d", len(procs))
+		}
+
+		proc := procs[0]
+		if proc.name != "web" {
+			t.Errorf("Expected name 'web', got '%s'", proc.name)
+		}
+
+		expectedArgs := []string{"echo", "'Hello", "World'"}
+		args := proc.args
+		if len(args) != len(expectedArgs) {
+			t.Errorf("Expected %d args, got %d", len(expectedArgs), len(args))
+		}
+
+		for i, expected := range expectedArgs {
+			if args[i] != expected {
+				t.Errorf("Expected arg[%d] '%s', got '%s'", i, expected, args[i])
+			}
+		}
+	})
+
+	t.Run("TestParseMultiple", func(t *testing.T) {
+		input := `web: ./server -port 8080
+worker: ./worker -queue tasks
+redis: redis-server --port 6379`
+
+		procs := Parse(strings.NewReader(input))
+
+		if len(procs) != 3 {
+			t.Errorf("Expected 3 processes, got %d", len(procs))
+		}
+
+		expectedNames := []string{"web", "worker", "redis"}
+		for i, expectedName := range expectedNames {
+			if procs[i].name != expectedName {
+				t.Errorf("Expected process[%d] name '%s', got '%s'", i, expectedName, procs[i].name)
+			}
+		}
+	})
+
+	t.Run("TestParseCommentsAndEmptyLines", func(t *testing.T) {
+		input := `# This is a comment
+web: echo 'Web server'
+
+# Another comment
+worker: echo 'Background worker'
+
+# Empty lines should be ignored`
+
+		procs := Parse(strings.NewReader(input))
+
+		if len(procs) != 2 {
+			t.Errorf("Expected 2 processes, got %d", len(procs))
+		}
+
+		if procs[0].name != "web" || procs[1].name != "worker" {
+			t.Errorf("Expected processes 'web' and 'worker', got '%s' and '%s'",
+				procs[0].name, procs[1].name)
+		}
+	})
+
+	t.Run("TestParseInvalidLines", func(t *testing.T) {
+		input := `web echo 'missing colon'
+worker: echo 'this one is valid'
+invalid line without colon`
+
+		procs := Parse(strings.NewReader(input))
+
+		if len(procs) != 1 {
+			t.Errorf("Expected 1 process, got %d", len(procs))
+		}
+
+		if procs[0].name != "worker" {
+			t.Errorf("Expected process name 'worker', got '%s'", procs[0].name)
+		}
+	})
+
+	t.Run("TestParseEmptyCommands", func(t *testing.T) {
+		input := `web: 
+worker: echo 'this is valid'
+empty:`
+
+		procs := Parse(strings.NewReader(input))
+
+		if len(procs) != 1 {
+			t.Errorf("Expected 1 process, got %d", len(procs))
+		}
+
+		if procs[0].name != "worker" {
+			t.Errorf("Expected process name 'worker', got '%s'", procs[0].name)
+		}
+	})
+
+	t.Run("TestParseEnvironmentVariables", func(t *testing.T) {
+		os.Setenv("TEST_PORT", "3000")
+		os.Setenv("TEST_ENV", "development")
+		defer func() {
+			os.Unsetenv("TEST_PORT")
+			os.Unsetenv("TEST_ENV")
+		}()
+
+		input := `web: ./server -port $TEST_PORT
+worker: ./worker -env $TEST_ENV`
+
+		procs := Parse(strings.NewReader(input))
+
+		if len(procs) != 2 {
+			t.Errorf("Expected 2 processes, got %d", len(procs))
+		}
+
+		// Check that environment variables were expanded
+		webArgs := procs[0].args
+		if len(webArgs) < 3 || webArgs[2] != "3000" {
+			t.Errorf("Expected web port '3000', got args: %v", webArgs)
+		}
+
+		workerArgs := procs[1].args
+		if len(workerArgs) < 3 || workerArgs[2] != "development" {
+			t.Errorf("Expected worker env 'development', got args: %v", workerArgs)
+		}
+	})
+}
+
+func TestParseFile(t *testing.T) {
+	tests := []struct {
+		file     string
+		expected int
+	}{
+		{"testdata/valid/basic.procfile", 1},
+		{"testdata/valid/multiple.procfile", 3},
+		{"testdata/valid/with-comments.procfile", 2},
+		{"testdata/invalid/no-colon.procfile", 1},      // Only valid lines parsed
+		{"testdata/invalid/empty-command.procfile", 1}, // Only valid lines parsed
+		{"testdata/env/variables.procfile", 3},         // Environment variable expansion
+	}
+
+	for _, test := range tests {
+		t.Run(test.file, func(t *testing.T) {
+			procs, err := ParseFile(test.file)
+			if err != nil {
+				t.Fatalf("ParseFile failed: %v", err)
+			}
+
+			if len(procs) != test.expected {
+				t.Errorf("Expected %d processes, got %d", test.expected, len(procs))
+			}
+		})
+	}
+}
pkg/procfile/proc.go
@@ -1,11 +1,8 @@
 package procfile
 
 import (
-	"bufio"
-	"io"
 	"os"
 	"os/exec"
-	"strings"
 	"syscall"
 )
 
@@ -21,42 +18,6 @@ func New(name string, args []string) *Proc {
 	}
 }
 
-func ParseFile(path string) ([]*Proc, error) {
-	file, err := os.Open(path)
-	if err != nil {
-		return nil, err
-	}
-	defer file.Close()
-
-	return Parse(file), nil
-}
-
-func Parse(file io.Reader) []*Proc {
-	var processes []*Proc
-	scanner := bufio.NewScanner(file)
-
-	for scanner.Scan() {
-		line := strings.TrimSpace(scanner.Text())
-		if line == "" || strings.HasPrefix(line, "#") {
-			continue
-		}
-
-		parts := strings.SplitN(line, ":", 2)
-		if len(parts) != 2 {
-			continue
-		}
-
-		args := strings.Fields(os.ExpandEnv(strings.TrimSpace(parts[1])))
-		if len(args) == 0 {
-			continue
-		}
-
-		processes = append(processes, New(parts[0], args))
-	}
-
-	return processes
-}
-
 func (p *Proc) NewCommand() *exec.Cmd {
 	cmd := exec.Command(p.args[0], p.args[1:]...)
 	cmd.Stdout = os.Stdout
pkg/procfile/proc_test.go
@@ -0,0 +1,38 @@
+package procfile
+
+import (
+	"strings"
+	"testing"
+)
+
+func TestNewCommand(t *testing.T) {
+	proc := New("test", []string{"echo", "hello"})
+	cmd := proc.NewCommand()
+
+	if !strings.HasSuffix(cmd.Path, "echo") {
+		t.Errorf("Expected command path to end with 'echo', got '%s'", cmd.Path)
+	}
+
+	expectedArgs := []string{"echo", "hello"}
+	if len(cmd.Args) != len(expectedArgs) {
+		t.Errorf("Expected %d args, got %d", len(expectedArgs), len(cmd.Args))
+	}
+
+	for i, expected := range expectedArgs {
+		if cmd.Args[i] != expected {
+			t.Errorf("Expected arg[%d] '%s', got '%s'", i, expected, cmd.Args[i])
+		}
+	}
+
+	if cmd.Stdout == nil {
+		t.Error("Expected Stdout to be set")
+	}
+
+	if cmd.Stderr == nil {
+		t.Error("Expected Stderr to be set")
+	}
+
+	if cmd.SysProcAttr == nil || !cmd.SysProcAttr.Setpgid {
+		t.Error("Expected process group to be configured")
+	}
+}
Makefile
@@ -6,5 +6,11 @@ clean:
 build:
 	@go build -o ./bin/minit main.go
 
-test: build
+test-unit:
 	@go test ./...
+
+test-integration: build
+	@./bin/minit -h
+
+test: test-unit test-integration
+