680845cb1c
Linux build / Determine Swift version (push) Waiting to run
Linux build / Linux compile check (push) Blocked by required conditions
Build containerization / Verify commit signatures (push) Has been skipped
Build containerization / containerization (push) Successful in 0s
301 lines
8.6 KiB
Swift
301 lines
8.6 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// Copyright © 2026 Apple Inc. and the Containerization project authors.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// https://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#if os(Linux)
|
|
|
|
import Foundation
|
|
import Testing
|
|
|
|
#if canImport(Musl)
|
|
import Musl
|
|
#elseif canImport(Glibc)
|
|
import Glibc
|
|
#endif
|
|
|
|
@testable import ContainerizationOS
|
|
|
|
@Suite("Epoll tests")
|
|
final class EpollTests {
|
|
|
|
@Suite("Mask option set")
|
|
struct MaskTests {
|
|
@Test
|
|
func inputAndOutputAreDistinct() {
|
|
let input = Epoll.Mask.input
|
|
let output = Epoll.Mask.output
|
|
#expect(input != output)
|
|
#expect(input.isDisjoint(with: output))
|
|
}
|
|
|
|
@Test
|
|
func readyToReadMatchesInput() {
|
|
let mask = Epoll.Mask.input
|
|
#expect(mask.readyToRead)
|
|
#expect(!mask.readyToWrite)
|
|
}
|
|
|
|
@Test
|
|
func readyToWriteMatchesOutput() {
|
|
let mask = Epoll.Mask.output
|
|
#expect(mask.readyToWrite)
|
|
#expect(!mask.readyToRead)
|
|
}
|
|
|
|
@Test
|
|
func combinedMask() {
|
|
let mask: Epoll.Mask = [.input, .output]
|
|
#expect(mask.readyToRead)
|
|
#expect(mask.readyToWrite)
|
|
}
|
|
|
|
@Test
|
|
func emptyMaskHasNoFlags() {
|
|
let mask = Epoll.Mask(rawValue: 0)
|
|
#expect(!mask.readyToRead)
|
|
#expect(!mask.readyToWrite)
|
|
#expect(!mask.isHangup)
|
|
#expect(!mask.isRemoteHangup)
|
|
}
|
|
}
|
|
|
|
private static func makePipe() throws -> (Int32, Int32) {
|
|
var fds: [Int32] = [0, 0]
|
|
guard pipe(&fds) == 0 else {
|
|
throw POSIXError.fromErrno()
|
|
}
|
|
return (fds[0], fds[1])
|
|
}
|
|
|
|
@Test
|
|
func addAndDeletePipeFD() throws {
|
|
let epoll = try Epoll()
|
|
let (readFD, writeFD) = try Self.makePipe()
|
|
defer {
|
|
close(readFD)
|
|
close(writeFD)
|
|
}
|
|
|
|
try epoll.add(readFD, mask: .input)
|
|
try epoll.delete(readFD)
|
|
}
|
|
|
|
@Test
|
|
func addInvalidFDThrows() throws {
|
|
let epoll = try Epoll()
|
|
#expect(throws: POSIXError.self) {
|
|
try epoll.add(-1, mask: .input)
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func deletingAlreadyClosedFDThrows() throws {
|
|
let epoll = try Epoll()
|
|
let (readFD, writeFD) = try Self.makePipe()
|
|
try epoll.add(readFD, mask: .input)
|
|
close(readFD)
|
|
close(writeFD)
|
|
#expect(throws: POSIXError.self) {
|
|
try epoll.delete(readFD)
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func doubleDeleteThrows() throws {
|
|
let epoll = try Epoll()
|
|
let (readFD, writeFD) = try Self.makePipe()
|
|
defer {
|
|
close(readFD)
|
|
close(writeFD)
|
|
}
|
|
|
|
try epoll.add(readFD, mask: .input)
|
|
try epoll.delete(readFD)
|
|
#expect(throws: POSIXError.self) {
|
|
try epoll.delete(readFD)
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func waitTimesOutWithNoEvents() throws {
|
|
let epoll = try Epoll()
|
|
let (readFD, writeFD) = try Self.makePipe()
|
|
defer {
|
|
close(readFD)
|
|
close(writeFD)
|
|
}
|
|
|
|
try epoll.add(readFD, mask: .input)
|
|
// Timeout of 0 means return immediately.
|
|
let events = epoll.wait(timeout: 0)
|
|
try #require(events != nil, "wait should not return nil without shutdown")
|
|
#expect(events!.isEmpty, "No data written, so no events expected")
|
|
}
|
|
|
|
@Test
|
|
func waitReturnsReadableEvent() throws {
|
|
let epoll = try Epoll()
|
|
let (readFD, writeFD) = try Self.makePipe()
|
|
defer {
|
|
close(readFD)
|
|
close(writeFD)
|
|
}
|
|
|
|
try epoll.add(readFD, mask: .input)
|
|
|
|
// Write some data to make the read end readable.
|
|
var byte: UInt8 = 42
|
|
let n = write(writeFD, &byte, 1)
|
|
try #require(n == 1, "write to pipe should succeed")
|
|
|
|
let events = epoll.wait(maxEvents: 4, timeout: 1000)
|
|
try #require(events != nil, "wait should not return nil without shutdown")
|
|
try #require(!events!.isEmpty, "Should have at least one event")
|
|
|
|
let event = events!.first { $0.fd == readFD }
|
|
try #require(event != nil, "Should have an event for the read fd")
|
|
#expect(event!.mask.readyToRead)
|
|
}
|
|
|
|
@Test
|
|
func waitReportsMultipleFDs() throws {
|
|
let epoll = try Epoll()
|
|
|
|
let (readFD1, writeFD1) = try Self.makePipe()
|
|
defer {
|
|
close(readFD1)
|
|
close(writeFD1)
|
|
}
|
|
let (readFD2, writeFD2) = try Self.makePipe()
|
|
defer {
|
|
close(readFD2)
|
|
close(writeFD2)
|
|
}
|
|
|
|
try epoll.add(readFD1, mask: .input)
|
|
try epoll.add(readFD2, mask: .input)
|
|
|
|
// Write to both pipes.
|
|
var byte: UInt8 = 1
|
|
_ = write(writeFD1, &byte, 1)
|
|
_ = write(writeFD2, &byte, 1)
|
|
|
|
let events = epoll.wait(maxEvents: 4, timeout: 1000)
|
|
try #require(events != nil)
|
|
#expect(events!.count == 2, "Should report events for both fds")
|
|
|
|
let fds = Set(events!.map { $0.fd })
|
|
#expect(fds.contains(readFD1))
|
|
#expect(fds.contains(readFD2))
|
|
}
|
|
|
|
@Test
|
|
func shutdownCausesWaitToReturnNil() throws {
|
|
let epoll = try Epoll()
|
|
let (readFD, writeFD) = try Self.makePipe()
|
|
defer {
|
|
close(readFD)
|
|
close(writeFD)
|
|
}
|
|
|
|
try epoll.add(readFD, mask: .input)
|
|
|
|
epoll.shutdown()
|
|
|
|
let events = epoll.wait(timeout: 0)
|
|
#expect(events == nil, "wait should return nil after shutdown")
|
|
}
|
|
|
|
@Test
|
|
func closingWriteEndSignalsHangup() throws {
|
|
let epoll = try Epoll()
|
|
let (readFD, writeFD) = try Self.makePipe()
|
|
defer { close(readFD) }
|
|
|
|
try epoll.add(readFD, mask: .input)
|
|
|
|
// Close the write end to trigger a hangup on the read end.
|
|
close(writeFD)
|
|
|
|
let events = epoll.wait(maxEvents: 4, timeout: 1000)
|
|
try #require(events != nil)
|
|
try #require(!events!.isEmpty, "Should have a hangup event")
|
|
|
|
let event = events!.first { $0.fd == readFD }
|
|
try #require(event != nil)
|
|
#expect(event!.mask.isHangup)
|
|
}
|
|
|
|
@Test
|
|
func deletedFDIsNotReported() throws {
|
|
let epoll = try Epoll()
|
|
let (readFD, writeFD) = try Self.makePipe()
|
|
defer {
|
|
close(readFD)
|
|
close(writeFD)
|
|
}
|
|
|
|
try epoll.add(readFD, mask: .input)
|
|
try epoll.delete(readFD)
|
|
|
|
// Write data, should not produce events since we deleted the fd.
|
|
var byte: UInt8 = 1
|
|
_ = write(writeFD, &byte, 1)
|
|
|
|
let events = epoll.wait(maxEvents: 4, timeout: 100)
|
|
try #require(events != nil)
|
|
#expect(events!.isEmpty, "Deleted fd should produce no events")
|
|
}
|
|
|
|
@Test
|
|
func edgeTriggeredRequiresDrainBeforeRenotify() throws {
|
|
let epoll = try Epoll()
|
|
let (readFD, writeFD) = try Self.makePipe()
|
|
defer {
|
|
close(readFD)
|
|
close(writeFD)
|
|
}
|
|
|
|
try epoll.add(readFD, mask: .input)
|
|
|
|
// Write data to make it readable.
|
|
var byte: UInt8 = 99
|
|
_ = write(writeFD, &byte, 1)
|
|
|
|
// First wait should return the event.
|
|
let events1 = epoll.wait(maxEvents: 4, timeout: 1000)
|
|
try #require(events1 != nil)
|
|
try #require(!events1!.isEmpty)
|
|
|
|
// Without reading the data, a second immediate wait should NOT
|
|
// re-trigger because of edge triggered semantics.
|
|
let events2 = epoll.wait(maxEvents: 4, timeout: 0)
|
|
try #require(events2 != nil)
|
|
#expect(events2!.isEmpty, "Edge triggered should not re-fire without new activity")
|
|
|
|
// Drain the pipe.
|
|
var buf = [UInt8](repeating: 0, count: 16)
|
|
_ = read(readFD, &buf, buf.count)
|
|
|
|
// Write new data. This is a new edge, so it should trigger again.
|
|
_ = write(writeFD, &byte, 1)
|
|
let events3 = epoll.wait(maxEvents: 4, timeout: 1000)
|
|
try #require(events3 != nil)
|
|
#expect(!events3!.isEmpty, "New write after drain should trigger event")
|
|
}
|
|
}
|
|
|
|
#endif // os(Linux)
|