mirror of
https://github.com/apple/container.git
synced 2026-10-14 10:09:10 +00:00
Epoll: Unit tests (#688)
Now that we can run this project on linux, lets add some unit tests for the surfaces that don't.. This additionally changes delete to throw in most cases. I don't really see how masking these errors is ideal.
This commit is contained in:
1 parent
0f4c8cf592
commit
fdd66283ce
2 files changed
+301
-8
No files matched your search
@@ -141,9 +141,7 @@ public final class Epoll: Sendable {
|
||||
epoll_ctl(self.epollFD, EPOLL_CTL_DEL, fd, ptr) as Int32
|
||||
}
|
||||
if result != 0 {
|
||||
if !acceptableDeletionErrno() {
|
||||
throw POSIXError.fromErrno()
|
||||
}
|
||||
throw POSIXError.fromErrno()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,11 +184,6 @@ public final class Epoll: Sendable {
|
||||
precondition(n == MemoryLayout<UInt64>.size, "eventfd write failed: \(POSIXError.fromErrno())")
|
||||
}
|
||||
|
||||
// The errno's here are acceptable and can happen if the caller
|
||||
// closed the underlying fd before calling delete().
|
||||
private func acceptableDeletionErrno() -> Bool {
|
||||
errno == ENOENT || errno == EBADF || errno == EPERM
|
||||
}
|
||||
}
|
||||
|
||||
#endif // os(Linux)
|
||||
@@ -0,0 +1,300 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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)
|
||||
Reference in new issue
Block a user