Commit c49dd9f
Changed files (11)
pkg
procfile
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
+