Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:25:30 +08:00

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)