• Explore
  • About Us
  • Log In
  • Get Started
  • Explore
  • About Us
  • Log In
  • Get Started

Traffic Light Controller

Scenario

In a busy city intersection, traffic lights are used to manage the flow of vehicles coming from different directions. Each road has its own traffic light, and the lights must coordinate to ensure safety while allowing vehicles to flow efficiently. For example, if the traffic light for the North-South road is green, the traffic light for the East-West road must be red, and vice versa. This ensures that vehicles do not collide at the intersection.

Your task is to simulate this traffic light system using Go's concurrency features. Each traffic light will run independently as a Goroutine, but a central controller will ensure that their states remain synchronized.

Requirements

  1. Simulate Two Traffic Lights:

    • One for the North-South road.
    • One for the East-West road.
  2. State Transition Logic:

    • Each traffic light cycles through the following states:
      • Green: Vehicles are allowed to go.
      • Yellow: Vehicles are warned to stop (transition state).
      • Red: Vehicles must stop (other light gets priority).
    • State durations:
      • Green: 10 seconds.
      • Yellow: 3 seconds.
      • Red: 10 seconds (while the other light is Green or Yellow).
  3. Coordination:

    • Only one traffic light can be Green or Yellow at any time.
    • When one light is Green or Yellow, the other light must be Red.
  4. Concurrency:

    • Each traffic light must run as an independent Goroutine.
    • A central controller Goroutine manages transitions and enforces synchronization between the two lights.
  5. Output Requirements:

    • Log the state of both traffic lights in real time.
    • East-West and North-South can be logged in any order, however the states of lights should be correct. Only one can be Red and another one should be Green or Yellow.
    • Example log:
         [00:00] North-South: Green
         [00:00] East-West: Red
         [00:10] East-West: Red
         [00:10] North-South: Yellow
         [00:13] North-South: Red
         [00:13] East-West: Green
         [00:23] North-South: Red
         [00:23] East-West: Yellow
         [00:26] East-West: Red
         [00:26] North-South: Green
         [00:36] East-West: Red
         [00:36] North-South: Yellow
         [00:39] East-West: Green
         [00:39] North-South: Red
         [00:49] East-West: Yellow
         [00:49] North-South: Red
         [00:52] East-West: Red
         [00:52] North-South: Green
         [01:02] East-West: Red
         [01:02] North-South: Yellow
         [01:05] East-West: Green
         [01:05] North-South: Red
         [01:15] East-West: Yellow
         [01:15] North-South: Red
      
  6. Stop Condition:

    • The simulation should run for a fixed number of cycles (e.g., 3 full cycles of Green-Yellow-Red for both lights).

Program Structure

The project structure should look like this:

.
├── main.go
├── main_test.go

Your program should have the following structure and implement these functions:

main.go

package main

import (
	"fmt"
	"time"
)

type LightState string

const (
	Green  LightState = "Green"
	Yellow LightState = "Yellow"
	Red    LightState = "Red"
)

type TrafficLight struct {
	Name  string          // Name of traffic light
	Ch    chan LightState // Channel to receive light state changes
	State LightState      // Current light state
}

// NewTrafficLight creates a new TrafficLight instance
func NewTrafficLight(name string) *TrafficLight {
	return &TrafficLight{
		Name:  name,
		Ch:    make(chan LightState),
		State: Red,
	}
}

// Run starts the traffic light and listen to the channel for state changes. It also send logs to the logCh channel.
func (tl *TrafficLight) Run(logCh chan string, startTime time.Time) {

}

// controller is a function that controls the traffic light states and changes them in defined intervals.
func controller(tl1, tl2 *TrafficLight, cycles int) {

}

func main() {
	lightNS := NewTrafficLight("North-South")
	lightEW := NewTrafficLight("East-West")

	logCh := make(chan string)

	// Close channels to stop goroutines
	defer close(lightNS.Ch)
	defer close(lightEW.Ch)
	defer close(logCh)

	startTime := time.Now()

	// Start traffic light goroutines
	go lightNS.Run(logCh, startTime)
	go lightEW.Run(logCh, startTime)

	// Start a logger goroutine
	go func() {
		for log := range logCh {
			fmt.Println(log)
		}
	}()

	// Start controller
	controller(lightNS, lightEW, 3)
}

main_test.go

package main

import (
	"strings"
	"testing"
	"time"
)

func TestNewTrafficLight(t *testing.T) {
	light := NewTrafficLight("Test Light")
	if light.Name != "Test Light" {
		t.Errorf("Expected name 'Test Light', got '%s'", light.Name)
	}
	if light.State != Red {
		t.Errorf("Expected initial state to be 'Red', got '%s'", light.State)
	}
	if light.Ch == nil {
		t.Errorf("Expected channel to be initialized, got nil")
	}
}

func TestTrafficLightRun(t *testing.T) {
	light := NewTrafficLight("Test Light")
	logCh := make(chan string, 1) // Buffered channel for testing
	defer close(logCh)

	startTime := time.Now()
	go light.Run(logCh, startTime)

	// Simulate state change
	light.Ch <- Green

	// Validate log output
	select {
	case log := <-logCh:
		if log == "" {
			t.Errorf("Expected log message, got empty string")
		}
		if light.State != Green {
			t.Errorf("Expected light state 'Green', got '%s'", light.State)
		}
	case <-time.After(1 * time.Second):
		t.Errorf("Timeout waiting for log message")
	}

	// Clean up
	close(light.Ch)
}
func TestController(t *testing.T) {
	lightNS := NewTrafficLight("North-South")
	lightEW := NewTrafficLight("East-West")

	logCh := make(chan string, 50) // Buffered channel to handle logs
	defer close(logCh)

	// Start traffic light goroutines
	go lightNS.Run(logCh, time.Now())
	go lightEW.Run(logCh, time.Now())

	// Run the controller in a separate goroutine
	go func() {
		controller(lightNS, lightEW, 3) // 3 cycles
		close(lightNS.Ch)
		close(lightEW.Ch)
	}()

	// Expected sequence for each cycle
	type lightState struct {
		NS LightState
		EW LightState
	}
	expectedSequence := []lightState{
		{NS: Green, EW: Red},  // Phase 1
		{NS: Yellow, EW: Red}, // Phase 2
		{NS: Red, EW: Green},  // Phase 3
		{NS: Red, EW: Yellow}, // Phase 4
	}

	// Variables to track the current phase and cycle
	currentPhase := 0
	cycleCount := 0
	stateMap := map[string]LightState{
		"NS": Red,
		"EW": Red,
	}
	timeout := time.After(2 * time.Minute) // Increased timeout for 3 cycles

	for cycleCount < 3 {
		select {
		case log := <-logCh:
			t.Logf("Received log: %s", log) // Debugging log

			// Update state map based on the log
			if strings.Contains(log, "North-South") {
				if strings.Contains(log, "Green") {
					stateMap["NS"] = Green
				} else if strings.Contains(log, "Yellow") {
					stateMap["NS"] = Yellow
				} else if strings.Contains(log, "Red") {
					stateMap["NS"] = Red
				}
			} else if strings.Contains(log, "East-West") {
				if strings.Contains(log, "Green") {
					stateMap["EW"] = Green
				} else if strings.Contains(log, "Yellow") {
					stateMap["EW"] = Yellow
				} else if strings.Contains(log, "Red") {
					stateMap["EW"] = Red
				}
			}

			// Check mutual exclusivity and phase completion
			expected := expectedSequence[currentPhase]
			if stateMap["NS"] == expected.NS && stateMap["EW"] == expected.EW {
				// Validate mutual exclusivity
				if stateMap["NS"] != Red && stateMap["EW"] != Red {
					t.Errorf("Invalid state: North-South = %s, East-West = %s. Both cannot be non-red at the same time.", stateMap["NS"], stateMap["EW"])
				}

				// Move to the next phase
				currentPhase++
				if currentPhase == len(expectedSequence) {
					currentPhase = 0
					cycleCount++
					t.Logf("Completed cycle %d", cycleCount) // Debugging log for cycles
				}
			}

		case <-timeout:
			t.Errorf("Test timed out before completing all cycles. Current phase: %d, Current cycle: %d", currentPhase, cycleCount)
			return
		}
	}

	if cycleCount != 3 {
		t.Errorf("Expected 3 cycles but completed %d cycles.", cycleCount)
	}
}

func TestTrafficLightConcurrency(t *testing.T) {
	light := NewTrafficLight("Test Light")
	logCh := make(chan string, 10) // Buffered for concurrency testing
	defer close(logCh)

	startTime := time.Now()
	go light.Run(logCh, startTime)

	// Simulate concurrent state changes
	go func() { light.Ch <- Green }()
	go func() { light.Ch <- Yellow }()
	go func() { light.Ch <- Red }()

	// Collect and validate logs
	for i := 0; i < 3; i++ {
		select {
		case log := <-logCh:
			if log == "" {
				t.Errorf("Expected log message, got empty string")
			}
		case <-time.After(1 * time.Second):
			t.Errorf("Timeout waiting for log message")
		}
	}

	// Clean up
	close(light.Ch)
}

func containsState(log, state string) bool {
	return strings.Contains(log, state)
}

To run test cases, execute:

go test -v -timeout=2m

Acceptance Criteria

  1. The program correctly simulates two traffic lights cycling through the states: Green, Yellow, and Red.
  2. Only one traffic light is Green or Yellow at a time.
  3. The output logs the state of both lights in real time.
  4. The simulation runs for a fixed 3 number of cycles.
  5. The solution uses goroutines and channels effectively for concurrency and synchronization.
  6. Test cases in main_test.go validate both traffic light transitions and overall functionality.

Hints

  1. Use a struct to represent a traffic light with fields for its name (e.g., "North-South"), current state, and duration.
  2. Use time.Sleep() to simulate the duration of each state (e.g., Green for 10 seconds).
  3. Use channels to signal state transitions between traffic lights.
  4. Use a loop to simulate multiple cycles of the traffic light states.

    Golang (Go) for Production Systems

    Unlock All Exercises

  • Getting Started
    • Important - Please Read
  • Error Handling
    • Finance Transaction Error
    • Resilient Retry Mechanism
    • Graceful Panic Recovery
    • Error Wrapping and Unwrapping
    • Aggregated Validation Errors
    • Transaction Rollback on Failure
  • Interfaces
    • Design a Payment Gateway Interface
    • Pluggable Logging System
    • Configurable Notification System
    • Dynamic Data Serializer
    • Dynamic Plugin System
  • Concurrency
    • File Word Counter
    • Traffic Lights Controller
    • Parking Lot Manager
    • Real-time Auction System
    • Safe Bank Account Balance Update
    • Build a Buffered Logger
    • Build a TTL Cache
    • Build an Elevator Control System
    • Debug and Fix Race Conditions - Part 1
    • Debug and Fix Race Conditions - Part 2
    • Debug and Fix Race Conditions - Part 3
    • Dynamic Feature Flag System
    • Real-Time Multiplayer Matchmaking
    • Lazy Database Connection
    • Distributed Task Deduplication
    • High Performance Request Counter
  • Core Networking
    • Simple TCP Echo Server
    • TCP Number Guessing Game
    • Looking Up Domain Information
    • UDP Time Broadcast Service
    • UDP Ping-Pong Client and Server
    • Building a Simple TCP Chat Server and Client
  • Winding Up
    • Final Notes