commit 7877b7262a73b649b6fb5df6b14346182e6d03bd Author: Obai Albek <89144251+ObaiAlbek@users.noreply.github.com> Date: Thu Sep 4 14:58:00 2025 +0200 push Project diff --git a/README.md b/README.md new file mode 100644 index 0000000..32d728b --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ + +# Flughafen-Simulation ✈️ + +Dieses Projekt ist eine **Flughafensimulation in Go**, die zentrale Prozesse eines Flughafens modelliert. + +## Inhalte +- **Check-in** von Passagieren +- **Gepäckabfertigung** +- **Sicherheitskontrolle** +- **Boarding am Gate** +- **Start- und Landebahn-Management** + +## Technische Umsetzung +- Implementiert in **Go** +- Nutzung von **Goroutines** und **Channels** zur parallelen Verarbeitung +- **Unit-Tests** zur Überprüfung der Kernfunktionen + +## Projektstruktur +- `main.go` → Einstiegspunkt der Simulation +- `checkin/` → Prozesse am Check-in-Schalter +- `baggageHandling/` → Gepäckabfertigung +- `securityCheck/` → Sicherheitskontrolle +- `runway/` → Start- und Landebahn-Logik +- `tests/` → Unit-Tests + diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8b63735 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I= +github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= diff --git a/main.go b/main.go new file mode 100644 index 0000000..ddd0e29 --- /dev/null +++ b/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "strings" + + "gitty.informatik.hs-mannheim.de/steger/pr3-ss2025/go/06-airport/airport" +) + +func main() { + ap := airport.DefaultAirport() + + ap.FlightSchedule.Print() + defer func() { + lostBaggage := ap.Bhs.CollectLostBaggage() + fmt.Println("The following baggage was lost today: ", lostBaggage) + }() + + ap.Start() + + for _, flight := range ap.FlightSchedule.AllFlights() { + ac := airport.NewAircraft(flight) + gate := ap.Gates[flight.GateNumber] + go ac.Start(gate, &ap.Runway) + } + + reader := bufio.NewReader(os.Stdin) + + fmt.Print("What/Who arrives at airport? (p for passenger, c for car, t for train, q to quit): ") + for { + input, _ := reader.ReadString('\n') + input = strings.TrimSpace(input) + + switch input { + case "q": + fmt.Println("Closing airport") + return + case "p": + p := ap.NewPassenger() + go p.Start(&ap) + case "c": + for i := 0; i < 4; i++ { + p := ap.NewPassenger() + go p.Start(&ap) + } + case "t": + for i := 0; i < 50; i++ { + p := ap.NewPassenger() + go p.Start(&ap) + } + default: + fmt.Println("Invalid command.") + } + } +} diff --git a/src/aircraft.go b/src/aircraft.go new file mode 100644 index 0000000..6088c59 --- /dev/null +++ b/src/aircraft.go @@ -0,0 +1,39 @@ +package airport + +import ( + "fmt" +) + +type Aircraft struct { + Flight Flight + Passengers []Passenger + Baggage []Baggage +} + +var _ BaggageProcessor = &Aircraft{} + +func NewAircraft(flight Flight) Aircraft { + return Aircraft{flight, []Passenger{}, []Baggage{}} +} + +func (ac *Aircraft) ProcessBaggage(fn FlightNumber, b Baggage) error { + if fn != ac.Flight.Id { + return fmt.Errorf("wrong flight %s", fn) + } + ac.Baggage = append(ac.Baggage, b) + fmt.Printf("loaded baggage %v onto flight %v\n", b, fn) + return nil +} + +func (ac *Aircraft) Process(p Passenger) error { + if p.BoardingPass.FlightNumber != ac.Flight.Id { + return fmt.Errorf("%s is unauthorized to enter aircraft", p.Name) + } + ac.Passengers = append(ac.Passengers, p) + return nil +} + +func (ac *Aircraft) Start(gate *Gate, rwy *Runway) { + gate.ProcessAircraft(ac) + rwy.ProcessAircraft(*ac) +} diff --git a/src/airport.go b/src/airport.go new file mode 100644 index 0000000..22d1ed4 --- /dev/null +++ b/src/airport.go @@ -0,0 +1,62 @@ +package airport + +import ( + "time" +) + +type Airport struct { + Checkin Checkin + Security SecurityCheck + Gates map[GateNumber]*Gate + Runway Runway + Bhs *BaggageHandlingSystem + FlightSchedule FlightSchedule +} + +func DefaultAirport() Airport { + + ap := Airport{ + Security: NewSecurityCheck(time.Duration(1 * time.Second)), + Gates: NewGates(10), + Runway: NewRunway(time.Duration(3 * time.Second)), + } + + fs := GenerateRandomFlightSchedule(ap.Gates, 10) + ap.FlightSchedule = fs + + baggageSinks := make(map[FlightNumber](BaggageProcessor)) + for _, g := range ap.Gates { + + var flightsForGate []Flight + for _, f := range fs.AllFlights() { + if f.GateNumber == g.id { + flightsForGate = append(flightsForGate, f) + } + baggageSinks[f.Id] = g + } + + g.SetFlights(flightsForGate) + ap.Gates[g.id] = g + } + + bhs := NewBaggageHandlingSystem(time.Duration(1*time.Second), baggageSinks) + ap.Bhs = &bhs + ap.Checkin = NewCheckin(time.Duration(1*time.Second), &fs, &bhs) + + return ap +} + +func (a *Airport) Start() { + go a.Checkin.Start() + go a.Security.Start() + go a.Bhs.Start() + go a.Runway.Start() + + for _, g := range a.Gates { + go g.Start() + } +} + +func (a *Airport) NewPassenger() Passenger { + return NewRandomPassenger(a.FlightSchedule.AllFlights()) +} diff --git a/src/baggageHandlingSystem.go b/src/baggageHandlingSystem.go new file mode 100644 index 0000000..54ba61f --- /dev/null +++ b/src/baggageHandlingSystem.go @@ -0,0 +1,61 @@ +package airport + +import ( + "fmt" + "time" +) + +type BaggageProcessor interface { + ProcessBaggage(FlightNumber, Baggage) error +} + +type BaggageHandlingSystem struct { + processingTime time.Duration + sinks map[FlightNumber]BaggageProcessor + lostBaggage chan LostBaggage +} + +type LostBaggage struct { + Baggage + FlightNumber FlightNumber + Err error +} + +var _ BaggageProcessor = &BaggageHandlingSystem{} + +func NewBaggageHandlingSystem(processingTime time.Duration, sinks map[FlightNumber]BaggageProcessor) BaggageHandlingSystem { + return BaggageHandlingSystem{ + processingTime: processingTime, + sinks: sinks, + lostBaggage: make(chan LostBaggage), + } +} + +func (bhs *BaggageHandlingSystem) ProcessBaggage(fn FlightNumber, b Baggage) error { + if sink, ok := bhs.sinks[fn]; ok { + go func() { + time.Sleep(bhs.processingTime) + if err := sink.ProcessBaggage(fn, b); err != nil { + bhs.lostBaggage <- LostBaggage{b, fn, err} + } + }() + } else { + return fmt.Errorf("invalid flight %v", fn) + } + return nil +} + +func (bhs *BaggageHandlingSystem) CollectLostBaggage() []LostBaggage { + lostBaggage := []LostBaggage{} + for { + select { + case lb := <-bhs.lostBaggage: + lostBaggage = append(lostBaggage, lb) + default: + return lostBaggage + } + } +} + +func (bhs *BaggageHandlingSystem) Start() { +} diff --git a/src/baggageHandlingSystem_test.go b/src/baggageHandlingSystem_test.go new file mode 100644 index 0000000..9c5ca14 --- /dev/null +++ b/src/baggageHandlingSystem_test.go @@ -0,0 +1,133 @@ +package airport_test + +import ( + "fmt" + "reflect" + "testing" + "time" + + "gitty.informatik.hs-mannheim.de/steger/pr3-ss2025/go/06-airport/airport" +) + +type BaggageSpyData struct { + fn airport.FlightNumber + b airport.Baggage + t time.Time +} + +type BaggageProcessorSpy struct { + Baggage []BaggageSpyData + Err error +} + +func (bps *BaggageProcessorSpy) ProcessBaggage(fn airport.FlightNumber, b airport.Baggage) error { + bps.Baggage = append(bps.Baggage, BaggageSpyData{fn, b, time.Now()}) + return bps.Err +} + +var _ airport.BaggageProcessor = &BaggageProcessorSpy{} + +func compareSink(t *testing.T, expected []airport.Baggage, expectedFlightNumber airport.FlightNumber, got BaggageProcessorSpy, start time.Time) { + for _, b := range got.Baggage { + if elapsed := b.t.Sub(start); elapsed < 1*time.Millisecond { + t.Errorf("processing time too short: got %v, want at least 1ms", elapsed) + } + + if b.fn != expectedFlightNumber { + t.Errorf("expected flight number: %v, got: %v", expectedFlightNumber, b.fn) + } + } + + if len(got.Baggage) != len(expected) { + t.Errorf("expected number of baggage: %d, got: %d", len(expected), len(got.Baggage)) + } + + deliveredBaggage := []airport.Baggage{} + for _, b := range got.Baggage { + deliveredBaggage = append(deliveredBaggage, b.b) + } + if !reflect.DeepEqual(deliveredBaggage, expected) { + t.Errorf("expected delivered baggage for %v: %v, got: %v", expectedFlightNumber, expected, deliveredBaggage) + } + +} + +func TestBaggageHandlingSystem(t *testing.T) { + + sink123 := BaggageProcessorSpy{nil, nil} + sink456 := BaggageProcessorSpy{nil, fmt.Errorf("aircraft already gone")} + sinks := map[airport.FlightNumber]airport.BaggageProcessor{ + airport.FlightNumber("LH123"): &sink123, + airport.FlightNumber("LH456"): &sink456, + } + sut := airport.NewBaggageHandlingSystem(1*time.Millisecond, sinks) + go sut.Start() + + testCases := []struct { + name string + baggage airport.Baggage + fn airport.FlightNumber + expectErr bool + expectDeliveredBaggage123 []airport.Baggage + expectDeliveredBaggage456 []airport.Baggage + expectLostBaggage []airport.LostBaggage + }{ + { + name: "Valid baggage", + baggage: airport.Baggage{[]string{"Socks", "Shirts"}}, + fn: "LH123", + expectErr: false, + expectDeliveredBaggage123: []airport.Baggage{{[]string{"Socks", "Shirts"}}}, + expectDeliveredBaggage456: []airport.Baggage{}, + expectLostBaggage: []airport.LostBaggage{}, + }, + { + name: "Aircraft already gone", + baggage: airport.Baggage{[]string{"Socks", "Shirts"}}, + fn: "LH456", + expectErr: false, + expectDeliveredBaggage123: []airport.Baggage{}, + expectDeliveredBaggage456: []airport.Baggage{{[]string{"Socks", "Shirts"}}}, + expectLostBaggage: []airport.LostBaggage{ + { + airport.Baggage{[]string{"Socks", "Shirts"}}, + "LH456", + fmt.Errorf("aircraft already gone"), + }, + }, + }, + { + name: "Unknown flight", + baggage: airport.Baggage{[]string{"Socks", "Shirts"}}, + fn: "XX999", + expectErr: true, + expectDeliveredBaggage123: []airport.Baggage{}, + expectDeliveredBaggage456: []airport.Baggage{}, + expectLostBaggage: []airport.LostBaggage{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sink123.Baggage = []BaggageSpyData{} + sink456.Baggage = []BaggageSpyData{} + start := time.Now() + + err := sut.ProcessBaggage(tc.fn, tc.baggage) + + time.Sleep(10 * time.Millisecond) //not really threadsafe because the 10 milliseconds have never been guaranteed. TODO: replace by sync mechanism + + if (err != nil) != tc.expectErr { + t.Errorf("expected error: %v, got: %v", tc.expectErr, err != nil) + } + + compareSink(t, tc.expectDeliveredBaggage123, "LH123", sink123, start) + compareSink(t, tc.expectDeliveredBaggage456, "LH456", sink456, start) + + lostBaggage := sut.CollectLostBaggage() + if !reflect.DeepEqual(lostBaggage, tc.expectLostBaggage) { + t.Errorf("expected lost baggage: %v, got: %v", tc.expectLostBaggage, lostBaggage) + } + }) + } +} diff --git a/src/checkin.go b/src/checkin.go new file mode 100644 index 0000000..c3abf3a --- /dev/null +++ b/src/checkin.go @@ -0,0 +1,72 @@ +package airport + +import ( + "errors" + "fmt" + "time" +) + +type Checkin struct { + processingTime time.Duration + queue chan passengerCheckinData + flightProvider FlightProvider + baggageProcessor BaggageProcessor +} + +type passengerCheckinData struct { + p *Passenger + err chan error +} + +func NewCheckin(processingTime time.Duration, fp FlightProvider, bp BaggageProcessor) Checkin { + return Checkin{ + processingTime: processingTime, + queue: make(chan passengerCheckinData), + flightProvider: fp, + baggageProcessor: bp, + } +} + +func (c Checkin) Process(p *Passenger) error { + err := make(chan error) + c.queue <- passengerCheckinData{p: p, err: err} + return <-err +} + +func (c Checkin) Start() { + defer fmt.Println("checkin terminated") + for element := range c.queue { + + time.Sleep(c.processingTime) + + //1. check ticket + if element.p.Name != element.p.Ticket.Name { + element.err <- fmt.Errorf("name %s on ticket does not match", element.p.Ticket.Name) + continue + } + + flight, err := c.flightProvider.GetFlight(element.p.Ticket.FlightNumber) + if err != nil { + element.err <- err + continue + } + + //2. issue boarding pass + element.p.BoardingPass = &BoardingPass{element.p.Ticket, flight.GateNumber} + + //3. check in bags + var baggageErr error + for _, bag := range element.p.Bags { + if err := c.baggageProcessor.ProcessBaggage(flight.Id, bag); err != nil { + baggageErr = errors.Join(baggageErr, err) + } + } + if baggageErr != nil { + element.err <- baggageErr + continue + } + element.p.Bags = []Baggage{} + + element.err <- nil + } +} diff --git a/src/checkin_test.go b/src/checkin_test.go new file mode 100644 index 0000000..e3328ea --- /dev/null +++ b/src/checkin_test.go @@ -0,0 +1,116 @@ +package airport_test + +import ( + "fmt" + "reflect" + "testing" + "time" + + "gitty.informatik.hs-mannheim.de/steger/pr3-ss2025/go/06-airport/airport" +) + +type FlightProviderStub struct { +} + +func (fps FlightProviderStub) GetFlight(fn airport.FlightNumber) (airport.Flight, error) { + if fn == "LH123" { + return airport.Flight{"LH123", "FRA", airport.GateNumber(1), time.Now(), time.Now()}, nil + } + return airport.Flight{}, fmt.Errorf("unknown flight %v", fn) +} + +var _ airport.FlightProvider = FlightProviderStub{} + +func TestCheckin(t *testing.T) { + + fp := FlightProviderStub{} + bps := BaggageProcessorSpy{} + sut := airport.NewCheckin(1*time.Millisecond, &fp, &bps) + go sut.Start() + + testCases := []struct { + name string + passenger airport.Passenger + expectErr bool + expectBaggage []airport.Baggage + expectBoardingpass *airport.BoardingPass + expectBPSBaggage []airport.Baggage + }{ + { + name: "Valid passenger with valid flight", + passenger: airport.Passenger{ + "Max Mustermann", + airport.Ticket{"Max Mustermann", "FRA", "LH123"}, + nil, + []airport.Baggage{{[]string{"Socks", "Shirts"}}}, + []string{}, + }, + expectErr: false, + expectBaggage: []airport.Baggage{}, + expectBoardingpass: &airport.BoardingPass{airport.Ticket{"Max Mustermann", "FRA", "LH123"}, airport.GateNumber(1)}, + expectBPSBaggage: []airport.Baggage{{[]string{"Socks", "Shirts"}}}, + }, + { + name: "Passenger with unknown flight", + passenger: airport.Passenger{ + "John Doe", + airport.Ticket{"John Doe", "MUC", "LH999"}, + nil, + []airport.Baggage{{[]string{"Socks", "Shirts"}}}, + []string{}, + }, + expectErr: true, + expectBaggage: []airport.Baggage{{[]string{"Socks", "Shirts"}}}, + expectBoardingpass: nil, + expectBPSBaggage: []airport.Baggage{}, + }, + { + name: "Passenger with wrong ticket", + passenger: airport.Passenger{ + "Max Mustermann", + airport.Ticket{"Someone else", "FRA", "LH123"}, + nil, + []airport.Baggage{{[]string{"Socks", "Shirts"}}}, + []string{}, + }, + expectErr: true, + expectBaggage: []airport.Baggage{{[]string{"Socks", "Shirts"}}}, + expectBoardingpass: nil, + expectBPSBaggage: []airport.Baggage{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + bps.Baggage = []BaggageSpyData{} + + start := time.Now() + err := sut.Process(&tc.passenger) + + if elapsed := time.Since(start); elapsed < 1*time.Millisecond { + t.Errorf("processing time too short: got %v, want at least 1ms", elapsed) + } + + if (err != nil) != tc.expectErr { + t.Errorf("expected error: %v, got: %v", tc.expectErr, err != nil) + } + + if !reflect.DeepEqual(tc.passenger.BoardingPass, tc.expectBoardingpass) { + t.Errorf("expected boarding pass: %v, got: %v", tc.expectBoardingpass, tc.passenger.BoardingPass) + } + + if !reflect.DeepEqual(tc.passenger.Bags, tc.expectBaggage) { + t.Errorf("expected baggage: %v, got: %v", tc.expectBaggage, tc.passenger.Bags) + } + + gotBPSBaggage := []airport.Baggage{} + for _, b := range bps.Baggage { + gotBPSBaggage = append(gotBPSBaggage, b.b) + } + + if !reflect.DeepEqual(gotBPSBaggage, tc.expectBPSBaggage) { + t.Errorf("expected baggage processor baggage: %v, got: %v", tc.expectBPSBaggage, gotBPSBaggage) + } + }) + } +} diff --git a/src/flightSchedule.go b/src/flightSchedule.go new file mode 100644 index 0000000..30cb6b1 --- /dev/null +++ b/src/flightSchedule.go @@ -0,0 +1,104 @@ +package airport + +import ( + "fmt" + "math/rand" + "strings" + "time" +) + +type FlightNumber string + +type Flight struct { + Id FlightNumber + Destination string + GateNumber GateNumber + BoardingTime time.Time + DepartureTime time.Time +} + +type FlightProvider interface { + GetFlight(FlightNumber) (Flight, error) +} + +type FlightSchedule struct { + flights map[FlightNumber]Flight +} + +var possibleDestinations = []string{ + "AMS", "ATH", "BCN", "BRU", "BUD", "CPH", "DUB", "DUS", "EDI", "FCO", + "FRA", "GVA", "HEL", "IST", "LIS", "LYS", "MAD", "MUC", "OSL", "PRG", + "RIX", "SOF", "STO", "TLL", "VIE", "WAW", "ZAG", "ZRH", "LUX", "MLA", +} + +func GenerateRandomFlightNumbers(count int) []FlightNumber { + + airlines := []string{"LH", "AF", "KL", "BA", "SK", "AZ", "IB", "TK", "AY", "OS"} + flightNumbers := make([]FlightNumber, count) + + for i := 0; i < count; i++ { + airline := airlines[rand.Intn(len(airlines))] + number := rand.Intn(9000) + 1000 // Random 4-digit number + flightNumbers[i] = FlightNumber(fmt.Sprintf("%s%04d", airline, number)) + } + + return flightNumbers +} + +func GenerateRandomFlightSchedule(gates map[GateNumber]*Gate, count int) FlightSchedule { + flightNumbers := GenerateRandomFlightNumbers(count) + flights := make(map[FlightNumber]Flight) + + gateNumbers := make([]GateNumber, 0, len(gates)) + for gn, _ := range gates { + gateNumbers = append(gateNumbers, gn) + } + + for _, flightNumber := range flightNumbers { + destination := possibleDestinations[rand.Intn(len(possibleDestinations))] + + gateNumber := gateNumbers[rand.Intn(len(gateNumbers))] + departureTime := time.Now().Add(time.Duration(rand.Intn(5*60)) * time.Second) + boardingTime := departureTime.Add(-30 * time.Second) + + flights[flightNumber] = Flight{ + Id: flightNumber, + Destination: destination, + GateNumber: gateNumber, + BoardingTime: boardingTime, + DepartureTime: departureTime, + } + } + + return FlightSchedule{flights: flights} +} + +func (fs *FlightSchedule) AllFlights() []Flight { + flights := make([]Flight, 0, len(fs.flights)) + for _, flight := range fs.flights { + flights = append(flights, flight) + } + return flights +} + +func (fs *FlightSchedule) GetFlight(flightNumber FlightNumber) (Flight, error) { + if flight, ok := fs.flights[flightNumber]; ok { + return flight, nil + } else { + return Flight{}, fmt.Errorf("invalid flight number %v", flightNumber) + } +} + +func (fs *FlightSchedule) Print() { + fmt.Printf("%-15s %-15s %-10s %-20s %-20s\n", "Flight Number", "Destination", "Gate", "Boarding Time", "Departure Time") + fmt.Println(strings.Repeat("-", 80)) + for _, flight := range fs.AllFlights() { + fmt.Printf("%-15s %-15s %-10d %-20s %-20s\n", + flight.Id, + flight.Destination, + flight.GateNumber, + flight.BoardingTime.Format("15:04:05"), + flight.DepartureTime.Format("15:04:05"), + ) + } +} diff --git a/src/gate.go b/src/gate.go new file mode 100644 index 0000000..733e174 --- /dev/null +++ b/src/gate.go @@ -0,0 +1,195 @@ +package airport + +import ( + "fmt" + "math/rand" + "sort" + "time" +) + +var distantFuture = time.Unix(1<<63-1, 0) // Max value for Unix timestamp + +type GateNumber int + +type aircraftBoardingData struct { + ac *Aircraft + boardingCompleted chan bool +} + +type passengerBoardingData struct { + passenger *Passenger + boardingError chan error +} + +type baggageLoadingData struct { + Baggage + fn FlightNumber + err chan error +} + +type Gate struct { + id GateNumber + walkingTimeFromSecurity time.Duration + taxiTimeToRunway time.Duration + + sortedFlights []Flight + + aircrafts chan aircraftBoardingData + passengers chan passengerBoardingData + baggage chan baggageLoadingData +} + +var _ BaggageProcessor = &Gate{} + +func NewGate(id GateNumber, walkingTimeFromSecurity time.Duration, taxiTimeToRunway time.Duration) Gate { + + return Gate{id, + walkingTimeFromSecurity, + taxiTimeToRunway, + []Flight{}, + make(chan aircraftBoardingData, 100), + make(chan passengerBoardingData), + make(chan baggageLoadingData), + } +} + +func NewGates(count int) map[GateNumber]*Gate { + gates := make(map[GateNumber]*Gate) + for i := range count { + walkingTime := time.Duration(5+rand.Intn(6)) * time.Second + taxiTime := time.Duration(5+rand.Intn(6)) * time.Second + + gate := NewGate(GateNumber(i+1), walkingTime, taxiTime) + gates[GateNumber(i+1)] = &gate + } + return gates +} + +func (g *Gate) SetFlights(flights []Flight) { + g.sortedFlights = make([]Flight, len(flights)) + copy(g.sortedFlights, flights) + sort.Slice(g.sortedFlights, func(i, j int) bool { + return g.sortedFlights[i].BoardingTime.Before(g.sortedFlights[j].BoardingTime) + }) +} + +// Blocks for the walking distance to the gate + the time until the passenger has boarded +func (g *Gate) Process(passenger Passenger) error { + time.Sleep(g.walkingTimeFromSecurity) + err := make(chan error) + g.passengers <- passengerBoardingData{&passenger, err} + return <-err +} + +// Blocks until departure (includes taxi time to runway) +func (g *Gate) ProcessAircraft(ac *Aircraft) { + done := make(chan bool) + g.aircrafts <- aircraftBoardingData{ac, done} + <-done + time.Sleep(g.taxiTimeToRunway) +} + +func (g *Gate) ProcessBaggage(fn FlightNumber, b Baggage) error { + err := make(chan error) + g.baggage <- baggageLoadingData{b, fn, err} + return <-err +} + +type FlightStatus int + +const ( + WAITING FlightStatus = iota + BOARDING + COMPLETED +) + +func (g Gate) Start() { + defer fmt.Println("gate terminated") + + type FlightData struct { + aircraftBoardingData + waitingPassengers []passengerBoardingData + waitingBaggage []baggageLoadingData + status FlightStatus + } + + data := make(map[FlightNumber]FlightData) + + currentFlight := 0 + for { + var deadline time.Time = distantFuture + var flight *Flight = nil + + if currentFlight < len(g.sortedFlights) { + flight = &g.sortedFlights[currentFlight] + switch data[flight.Id].status { + case WAITING: + deadline = flight.BoardingTime + case BOARDING: + deadline = flight.DepartureTime + } + } + + select { + case boardingData := <-g.aircrafts: + flightNumber := boardingData.ac.Flight.Id + fd := data[flightNumber] + fd.aircraftBoardingData = boardingData + data[flightNumber] = fd + + case passengerData := <-g.passengers: + flightData := data[passengerData.passenger.BoardingPass.FlightNumber] + switch flightData.status { + case WAITING: + flightData.waitingPassengers = append(flightData.waitingPassengers, passengerData) + case BOARDING: + passengerData.boardingError <- flightData.ac.Process(*passengerData.passenger) + case COMPLETED: + passengerData.boardingError <- fmt.Errorf("boarding not possible: flight %v already left", flightData.ac.Flight.Id) + } + data[passengerData.passenger.BoardingPass.FlightNumber] = flightData + + case baggageData := <-g.baggage: + flightData := data[baggageData.fn] + switch flightData.status { + case WAITING: + flightData.waitingBaggage = append(flightData.waitingBaggage, baggageData) + case BOARDING: + baggageData.err <- flightData.ac.ProcessBaggage(baggageData.fn, baggageData.Baggage) + case COMPLETED: + baggageData.err <- fmt.Errorf("bag cannot be loaded: flight %v already left", baggageData.fn) + } + data[baggageData.fn] = flightData + + //only applicable if there is a valid flight + case <-time.After(time.Until(deadline)): + if flight != nil { + flightData := data[flight.Id] + + switch flightData.status { + case WAITING: + fmt.Printf("flight %v is ready for boarding\n", flight.Id) + + for _, wp := range flightData.waitingPassengers { + wp.boardingError <- flightData.ac.Process(*wp.passenger) + } + flightData.waitingPassengers = nil + + for _, wb := range flightData.waitingBaggage { + wb.err <- flightData.ac.ProcessBaggage(wb.fn, wb.Baggage) + } + flightData.waitingBaggage = nil + + flightData.status = BOARDING + case BOARDING: + fmt.Printf("boarding completed for flight number %s\n", flight.Id) + flightData.status = COMPLETED + flightData.boardingCompleted <- true + currentFlight++ + } + data[flight.Id] = flightData + } + } + } + +} diff --git a/src/gate_test.go b/src/gate_test.go new file mode 100644 index 0000000..386b37c --- /dev/null +++ b/src/gate_test.go @@ -0,0 +1,112 @@ +package airport_test + +import ( + "reflect" + "sync" + "testing" + "time" + + "gitty.informatik.hs-mannheim.de/steger/pr3-ss2025/go/06-airport/airport" +) + +func TestGateSingleFlight(t *testing.T) { + + sut := airport.NewGate(airport.GateNumber(25), time.Duration(3*time.Millisecond), time.Duration(1*time.Millisecond)) + flight := airport.Flight{"LH123", "FRA", 25, time.Now().Add(5 * time.Millisecond), time.Now().Add(10 * time.Millisecond)} + sut.SetFlights([]airport.Flight{ + flight, + }) + go sut.Start() + + start := time.Now() + wg := sync.WaitGroup{} + + //the aircraft + wg.Add(1) + ac := airport.NewAircraft(flight) + departureTime := time.Now() + go func() { sut.ProcessAircraft(&ac); departureTime = time.Now(); wg.Done() }() + + //the baggage + wg.Add(1) + baggage := airport.Baggage{[]string{"socks", "shirt"}} + var bag1err error + go func() { bag1err = sut.ProcessBaggage("LH123", baggage); wg.Done() }() + + //a passenger arriving on time + wg.Add(1) + ticket1 := airport.Ticket{"John Doe", "FRA", "LH123"} + pax1 := airport.Passenger{"John Doe", ticket1, &airport.BoardingPass{ticket1, 25}, []airport.Baggage{}, []string{}} + pax1boardingTime := time.Now() + var pax1err error + go func() { pax1err = sut.Process(pax1); pax1boardingTime = time.Now(); wg.Done() }() + + //another passenger arriving too late + wg.Add(1) + ticket2 := airport.Ticket{"Jane Doe", "FRA", "LH123"} + pax2 := airport.Passenger{"Jane Doe", ticket2, &airport.BoardingPass{ticket2, 25}, []airport.Baggage{}, []string{}} + var pax2err error + go func() { time.Sleep(8 * time.Millisecond); pax2err = sut.Process(pax2); wg.Done() }() + + //another passenger arriving sometime during boarding + wg.Add(1) + ticket3 := airport.Ticket{"Alex Smith", "FRA", "LH123"} + pax3 := airport.Passenger{"Alex Smith", ticket3, &airport.BoardingPass{ticket3, 25}, []airport.Baggage{}, []string{}} + pax3boardingTime := time.Now() + var pax3err error + go func() { + time.Sleep(6 * time.Millisecond) + pax3err = sut.Process(pax3) + pax3boardingTime = time.Now() + wg.Done() + }() + + //------------------------FROM HERE: checking ---------------------------------- + wg.Wait() + + //--------------------------check aircraft------------------- + if elapsed := departureTime.Sub(start); elapsed < 11*time.Millisecond { + t.Errorf("departure time too short: got %v, want at least 11ms", elapsed) + } + + expectedBoardedPax := []airport.Passenger{pax1, pax3} + if !reflect.DeepEqual(expectedBoardedPax, ac.Passengers) { + t.Errorf("boarded passengers mismatch: got %v, want %v", ac.Passengers, expectedBoardedPax) + } + + expectedBaggageOnboard := []airport.Baggage{baggage} + if !reflect.DeepEqual(expectedBaggageOnboard, ac.Baggage) { + t.Errorf("baggage on board mismatch: got %v, want %v", ac.Baggage, expectedBaggageOnboard) + } + + //----------------------------check baggage-------------------- + + if bag1err != nil { + t.Errorf("baggage encountered an error during processing: %v", bag1err) + } + + //--------------------------check passengers------------------- + + //boarding can only happen between boarding (5ms from start) and departure (10ms from start) + if elapsed := pax1boardingTime.Sub(start); elapsed < 5*time.Millisecond || elapsed > 10*time.Millisecond { + t.Errorf("passenger 1 boarding time out of range: got %v, want between 5ms and 10ms", elapsed) + } + + if pax1err != nil { + t.Errorf("passenger 1 encountered an error during boarding: %v", pax1err) + } + + if pax2err == nil { + t.Errorf("passenger 2 did not encounter an error during boarding, but was expected to") + } + + //boarding can only happen after boarding (5ms from start) + if elapsed := pax3boardingTime.Sub(start); elapsed < 5*time.Millisecond { + t.Errorf("passenger 3 boarding time out of range: got %v, want more than 5ms", elapsed) + } + + if pax3err != nil { + t.Errorf("passenger 3 encountered an error during boarding: %v", pax3err) + } + +} diff --git a/src/passenger.go b/src/passenger.go new file mode 100644 index 0000000..c38f78a --- /dev/null +++ b/src/passenger.go @@ -0,0 +1,128 @@ +package airport + +import ( + "fmt" + "math/rand" + "time" + + "github.com/docker/docker/pkg/namesgenerator" +) + +type Ticket struct { + Name string + Destination string + FlightNumber FlightNumber +} + +type BoardingPass struct { + Ticket + Gate GateNumber +} + +type Baggage struct { + Items []string +} + +type Passenger struct { + Name string + Ticket Ticket + BoardingPass *BoardingPass + Bags []Baggage + CarryOnItems []string +} + +var PossibleItems = []string{ + "Laptop", "Tablet", "Smartphone", "Headphones", "Charger", "Book", "Magazine", "Notebook", "Pen", "Pencil", "Carabiner", + "Water Bottle", "Snacks", "Sandwich", "Camera", "Tripod", "Gun", "Clothes", "Shoes", "Toothbrush", "Toothpaste", + "Shampoo", "Conditioner", "Soap", "Deodorant", "Perfume", "Makeup Kit", "Hairbrush", "Comb", "Razor", "Shaving Cream", + "First Aid Kit", "Medicine", "Umbrella", "Hat", "Sunglasses", "Scarf", "Gloves", "Wallet", "Passport", "Camping Gear", + "Keys", "Watch", "Jewelry", "Flashlight", "Power Bank", "Knife", "USB Drive", "External Hard Drive", "Gaming Console", "Controller", + "Board Game", "Puzzle", "Toy", "Explosives", "Stuffed Animal", "Blanket", "Pillow", "Sleeping Bag", "Towel", "Swimsuit", + "Goggles", "Flip Flops", "Backpack", "Handbag", "Suitcase", "Briefcase", "Binoculars", "Map", "Guidebook", "Notebook", + "Sketchpad", "Paints", "Brushes", "Musical Instrument", "Sheet Music", "Sports Equipment", "Yoga Mat", "Jump Rope", "Resistance Bands", + "Tent", "Cooking Utensils", "Portable Stove", "Food Containers", "Water Filter", "Fishing Rod", "Knife Sharpener", "Multi-tool", "Rope", + "Compass", "GPS Device", "Safety Vest", "Helmet", "Protective Gear", "Fire Extinguisher", "Matches", "Lighter", "Candles", "Notebook", + "Clipboard", "Calculator", "Documents", "Folders", "Envelopes", +} + +func NewRandomPassenger(flights []Flight) Passenger { + + // Generate random ticket details + name := namesgenerator.GetRandomName(0) + flight := flights[rand.Intn(len(flights))] + + ticket := Ticket{ + Name: name, + Destination: flight.Destination, + FlightNumber: flight.Id, + } + + // Generate random baggage + numBags := rand.Intn(3) // Random number of bags (0 to 2) + bags := make([]Baggage, numBags) + for i := 0; i < numBags; i++ { + numItems := rand.Intn(5) + 1 // Random number of items (1 to 5) + items := make([]string, numItems) + for j := 0; j < numItems; j++ { + items[j] = PossibleItems[rand.Intn(len(PossibleItems))] + } + bags[i] = Baggage{ + Items: items, + } + } + + // Generate random carry-on items + numCarryOnItems := rand.Intn(3) // Random number of carry-on items (0 to 2) + carryOnItems := make([]string, numCarryOnItems) + for i := 0; i < numCarryOnItems; i++ { + carryOnItems[i] = PossibleItems[rand.Intn(len(PossibleItems))] + } + + return Passenger{ + Name: name, + Ticket: ticket, + BoardingPass: nil, + Bags: bags, + CarryOnItems: carryOnItems, + } +} + +func (p *Passenger) Start(ap *Airport) { + + fmt.Printf("%s has arrived at the airport for the flight number %s to %s in %.02f minutes with %d bags and carry-on items: %v\n", + p.Name, + p.Ticket.FlightNumber, + p.Ticket.Destination, + time.Until(ap.FlightSchedule.flights[p.Ticket.FlightNumber].DepartureTime).Minutes(), + len(p.Bags), + p.CarryOnItems) + + err := ap.Checkin.Process(p) + if err != nil { + fmt.Printf("\033[31m%s: %v\033[0m\n", p.Name, err) + return + } + fmt.Printf("%s completed the checkin procedure and is now heading to security\n", p.Name) + + err = ap.Security.Process(*p) + if err != nil { + fmt.Printf("\033[31m%s: %v\033[0m\n", p.Name, err) + return + } + + fmt.Printf("%s has successfully passed the security check and is now walking to the gate\n", p.Name) + + gate, ok := ap.Gates[p.BoardingPass.Gate] + if !ok { + fmt.Printf("\033[31m%s: invalid gate number %v\033[0m\n", p.Name, p.BoardingPass.Gate) + return + } + + err = gate.Process(*p) + if err != nil { + fmt.Printf("\033[31mError: %s: %v\033[0m\n", p.Name, err) + return + } + + fmt.Printf("\033[32m%s has successfully boarded flight %s to %s\033[0m\n", p.Name, p.Ticket.FlightNumber, p.Ticket.Destination) +} diff --git a/src/runway.go b/src/runway.go new file mode 100644 index 0000000..f96b6de --- /dev/null +++ b/src/runway.go @@ -0,0 +1,33 @@ +package airport + +import ( + "fmt" + "time" +) + +type Runway struct { + takeoffDuration time.Duration + queue chan Aircraft +} + +func NewRunway(takeoffDuration time.Duration) Runway { + return Runway{ + takeoffDuration: takeoffDuration, + queue: make(chan Aircraft), + } +} + +func (r *Runway) Start() { + defer fmt.Println("runway terminated") + + for ac := range r.queue { + fmt.Printf("Flight %v to %v is taking off\n", ac.Flight.Id, ac.Flight.Destination) + time.Sleep(r.takeoffDuration) + fmt.Printf("\033[32mFlight %v to %v has taken off with %v passengers and %v bags on board\033[0m\n", ac.Flight.Id, ac.Flight.Destination, int(len(ac.Passengers)), int(len(ac.Baggage))) + } +} + +func (r *Runway) ProcessAircraft(ac Aircraft) { + fmt.Printf("Flight %v is waiting for takeoff\n", ac.Flight.Id) + r.queue <- ac +} diff --git a/src/securityCheck.go b/src/securityCheck.go new file mode 100644 index 0000000..6989c02 --- /dev/null +++ b/src/securityCheck.go @@ -0,0 +1,65 @@ +package airport + +import ( + "fmt" + "time" +) + +var prohibitedItems = map[string]bool{"Knife": true, "Gun": true, "Explosives": true} + +type SecurityCheck struct { + processingTime time.Duration + queue chan securityCheckPassengerData +} + +type securityCheckPassengerData struct { + p Passenger + err chan error +} + +func NewSecurityCheck(processingTime time.Duration) SecurityCheck { + return SecurityCheck{ + processingTime: processingTime, + queue: make(chan securityCheckPassengerData), + } +} + +func (sc *SecurityCheck) Start() { + defer fmt.Println("security check terminated") + for element := range sc.queue { + + time.Sleep(sc.processingTime) + + //1. check boarding pass + if element.p.BoardingPass == nil { + element.err <- fmt.Errorf("no boarding pass. Go to checkin first") + continue + } + + if element.p.BoardingPass.Name != element.p.Name { + element.err <- fmt.Errorf("name %s on ticket does not match", element.p.BoardingPass.Name) + continue + } + + //2. check carry on items + detectedProhibitedItems := []string{} + for _, item := range element.p.CarryOnItems { + _, prohibited := prohibitedItems[item] + if prohibited { + detectedProhibitedItems = append(detectedProhibitedItems, item) + } + } + if len(detectedProhibitedItems) > 0 { + element.err <- fmt.Errorf("prohibited item detected: %v", detectedProhibitedItems) + continue + } + + element.err <- nil + } +} + +func (sc SecurityCheck) Process(p Passenger) error { + err := make(chan error) + sc.queue <- securityCheckPassengerData{p: p, err: err} + return <-err +} diff --git a/src/securityCheck_test.go b/src/securityCheck_test.go new file mode 100644 index 0000000..ad53770 --- /dev/null +++ b/src/securityCheck_test.go @@ -0,0 +1,81 @@ +package airport_test + +import ( + "testing" + "time" + + "gitty.informatik.hs-mannheim.de/steger/pr3-ss2025/go/06-airport/airport" +) + +func TestSecurityCheck(t *testing.T) { + + sut := airport.NewSecurityCheck(1 * time.Millisecond) + go sut.Start() + + testCases := []struct { + name string + passenger airport.Passenger + expectErr bool + }{ + { + name: "Valid passenger", + passenger: airport.Passenger{ + "Max Mustermann", + airport.Ticket{"Max Mustermann", "FRA", "LH123"}, + &airport.BoardingPass{airport.Ticket{"Max Mustermann", "FRA", "LH123"}, airport.GateNumber(1)}, + []airport.Baggage{}, + []string{"Laptop", "Hat"}, + }, + expectErr: false, + }, + { + name: "Passenger without boarding pass", + passenger: airport.Passenger{ + "Max Mustermann", + airport.Ticket{"Max Mustermann", "FRA", "LH123"}, + nil, + []airport.Baggage{}, + []string{"Laptop", "Hat"}, + }, + expectErr: true, + }, + { + name: "Passenger with wrong boarding pass", + passenger: airport.Passenger{ + "Max Mustermann", + airport.Ticket{"Max Mustermann", "FRA", "LH123"}, + &airport.BoardingPass{airport.Ticket{"Someone else", "FRA", "LH123"}, airport.GateNumber(1)}, + []airport.Baggage{}, + []string{"Laptop", "Hat"}, + }, + expectErr: true, + }, + { + name: "Passenger with prohibited item", + passenger: airport.Passenger{ + "Max Mustermann", + airport.Ticket{"Max Mustermann", "FRA", "LH123"}, + &airport.BoardingPass{airport.Ticket{"Max Mustermann", "FRA", "LH123"}, airport.GateNumber(1)}, + []airport.Baggage{}, + []string{"Laptop", "Hat", "Knife"}, + }, + expectErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + + start := time.Now() + err := sut.Process(tc.passenger) + + if elapsed := time.Since(start); elapsed < 1*time.Millisecond { + t.Errorf("processing time too short: got %v, want at least 1ms", elapsed) + } + + if (err != nil) != tc.expectErr { + t.Errorf("expected error: %v, got: %v", tc.expectErr, err != nil) + } + }) + } +}