Skip to content

Commit e486787

Browse files
authored
Remove dependencies on Foundation's file handling APIs. (#356)
1 parent 8705819 commit e486787

File tree

6 files changed

+193
-54
lines changed

6 files changed

+193
-54
lines changed

Sources/Testing/Support/FileHandle.swift

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,16 @@ struct FileHandle: ~Copyable, Sendable {
7171
self.init(unsafeCFILEHandle: fileHandle, closeWhenDone: true)
7272
}
7373

74+
/// Initialize an instance of this type to read from the given path.
75+
///
76+
/// - Parameters:
77+
/// - path: The path to read from.
78+
///
79+
/// - Throws: Any error preventing the stream from being opened.
80+
init(forReadingAtPath path: String) throws {
81+
try self.init(atPath: path, mode: "rb")
82+
}
83+
7484
/// Initialize an instance of this type to write to the given path.
7585
///
7686
/// - Parameters:
@@ -208,6 +218,62 @@ struct FileHandle: ~Copyable, Sendable {
208218
}
209219
}
210220

221+
// MARK: - Reading
222+
223+
extension FileHandle {
224+
/// Read to the end of the file handle.
225+
///
226+
/// - Returns: A copy of the contents of the file handle starting at the
227+
/// current offset and ending at the end of the file.
228+
///
229+
/// - Throws: Any error that occurred while reading the file.
230+
func readToEnd() throws -> [UInt8] {
231+
var result = [UInt8]()
232+
233+
// If possible, reserve enough space in the resulting buffer to contain
234+
// the contents of the file being read.
235+
var size: Int?
236+
#if SWT_TARGET_OS_APPLE || os(Linux)
237+
withUnsafePOSIXFileDescriptor { fd in
238+
var s = stat()
239+
if let fd, 0 == fstat(fd, &s) {
240+
size = Int(exactly: s.st_size)
241+
}
242+
}
243+
#elseif os(Windows)
244+
withUnsafeWindowsHANDLE { handle in
245+
var liSize = LARGE_INTEGER(QuadPart: 0)
246+
if let handle, GetFileSizeEx(handle, &liSize) {
247+
size = Int(exactly: liSize.QuadPart)
248+
}
249+
}
250+
#endif
251+
if let size, size > 0 {
252+
result.reserveCapacity(size)
253+
}
254+
255+
try withUnsafeCFILEHandle { file in
256+
try withUnsafeTemporaryAllocation(byteCount: 1024, alignment: 1) { buffer in
257+
while true {
258+
let countRead = fread(buffer.baseAddress, 1, buffer.count, file)
259+
if 0 != ferror(file) {
260+
throw CError(rawValue: swt_errno())
261+
}
262+
if countRead > 0 {
263+
let endIndex = buffer.index(buffer.startIndex, offsetBy: countRead)
264+
result.append(contentsOf: buffer[..<endIndex])
265+
}
266+
if 0 != feof(file) {
267+
break
268+
}
269+
}
270+
}
271+
}
272+
273+
return result
274+
}
275+
}
276+
211277
// MARK: - Writing
212278

213279
extension FileHandle {
@@ -357,4 +423,30 @@ extension FileHandle {
357423
#endif
358424
}
359425
}
426+
427+
// MARK: - General path utilities
428+
429+
/// Append a path component to a path.
430+
///
431+
/// - Parameters:
432+
/// - pathComponent: The path component to append.
433+
/// - path: The path to which `pathComponent` should be appended.
434+
///
435+
/// - Returns: The full path to `pathComponent`, or `nil` if the resulting
436+
/// string could not be created.
437+
func appendPathComponent(_ pathComponent: String, to path: String) -> String {
438+
#if os(Windows)
439+
path.withCString(encodedAs: UTF16.self) { path in
440+
pathComponent.withCString(encodedAs: UTF16.self) { pathComponent in
441+
withUnsafeTemporaryAllocation(of: wchar_t.self, capacity: (wcslen(path) + wcslen(pathComponent)) * 2 + 1) { buffer in
442+
_ = wcscpy_s(buffer.baseAddress, buffer.count, path)
443+
_ = PathCchAppendEx(buffer.baseAddress, buffer.count, pathComponent, ULONG(PATHCCH_ALLOW_LONG_PATHS.rawValue))
444+
return (String.decodeCString(buffer.baseAddress, as: UTF16.self)?.result)!
445+
}
446+
}
447+
}
448+
#else
449+
"\(path)/\(pathComponent)"
450+
#endif
451+
}
360452
#endif

Sources/Testing/Traits/Tags/Tag.Color+Loading.swift

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,22 @@
1010

1111
private import TestingInternals
1212

13-
#if canImport(Foundation)
14-
private import Foundation
15-
#endif
16-
1713
#if !SWT_NO_FILE_IO
1814
#if os(macOS) || (os(iOS) && targetEnvironment(macCatalyst)) || os(Linux)
1915
/// The path to the current user's home directory, if known.
2016
private var _homeDirectoryPath: String? {
21-
#if canImport(Foundation)
22-
NSHomeDirectory()
23-
#else
24-
if let homeVariable = Environment.variable(named: "HOME") {
17+
#if SWT_TARGET_OS_APPLE
18+
if let fixedHomeVariable = Environment.variable(named: "CFFIXED_USER_HOME") {
19+
return fixedHomeVariable
20+
}
21+
#endif
22+
return if let homeVariable = Environment.variable(named: "HOME") {
2523
homeVariable
2624
} else if let pwd = getpwuid(geteuid()) {
2725
String(validatingUTF8: pwd.pointee.pw_dir)
2826
} else {
2927
nil
3028
}
31-
#endif
3229
}
3330
#endif
3431

@@ -58,11 +55,11 @@ var swiftTestingDirectoryPath: String {
5855

5956
#if os(macOS) || (os(iOS) && targetEnvironment(macCatalyst)) || os(Linux)
6057
if let homeDirectoryPath = _homeDirectoryPath {
61-
return "\(homeDirectoryPath)/\(swiftTestingDirectoryName)"
58+
return appendPathComponent(swiftTestingDirectoryName, to: homeDirectoryPath)
6259
}
6360
#elseif os(Windows)
6461
if let appDataDirectoryPath = _appDataDirectoryPath {
65-
return "\(appDataDirectoryPath)\\\(swiftTestingDirectoryName)"
62+
return appendPathComponent(swiftTestingDirectoryName, to: appDataDirectoryPath)
6663
}
6764
#else
6865
#warning("Platform-specific implementation missing: swift-testing directory location unavailable")
@@ -88,9 +85,9 @@ var swiftTestingDirectoryPath: String {
8885
/// supported formats for tag colors in this dictionary, see <doc:AddingTags>.
8986
func loadTagColors(fromFileInDirectoryAtPath swiftTestingDirectoryPath: String = swiftTestingDirectoryPath) throws -> [Tag: Tag.Color] {
9087
// Find the path to the tag-colors.json file and try to load its contents.
91-
let tagColorsURL = URL(fileURLWithPath: swiftTestingDirectoryPath, isDirectory: true)
92-
.appendingPathComponent("tag-colors.json", isDirectory: false)
93-
let tagColorsData = try Data(contentsOf: tagColorsURL, options: [.mappedIfSafe])
88+
let tagColorsPath = appendPathComponent("tag-colors.json", to: swiftTestingDirectoryPath)
89+
let fileHandle = try FileHandle(forReadingAtPath: tagColorsPath)
90+
let tagColorsData = try fileHandle.readToEnd()
9491

9592
// By default, a dictionary with non-string keys is encoded to and decoded
9693
// from JSON as an array, so we decode the dictionary as if its keys are plain

Sources/TestingInternals/include/Includes.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@
7575
#include <pty.h>
7676
#endif
7777

78+
#if __has_include(<pwd.h>)
79+
#include <pwd.h>
80+
#endif
81+
7882
#if __has_include(<limits.h>)
7983
#include <limits.h>
8084
#endif

Tests/TestingTests/Support/FileHandleTests.swift

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -56,23 +56,27 @@ struct FileHandleTests {
5656

5757
@Test("Can write to a file")
5858
func canWrite() throws {
59-
// NOTE: we are not trying to test mkstemp() here. We are trying to test the
60-
// capacity of FileHandle to open a file for writing, and need a temporary
61-
// file to write to.
62-
#if os(Windows)
63-
let path = try String(unsafeUninitializedCapacity: 1024) { buffer in
64-
try #require(0 == tmpnam_s(buffer.baseAddress!, buffer.count))
65-
return strnlen(buffer.baseAddress!, buffer.count)
59+
try withTemporaryPath { path in
60+
let fileHandle = try FileHandle(forWritingAtPath: path)
61+
try fileHandle.write([0, 1, 2, 3, 4, 5])
62+
try fileHandle.write("Hello world!")
6663
}
67-
#else
68-
let path = "/tmp/can_write_to_file_\(UInt64.random(in: 0 ..< .max))"
69-
#endif
70-
defer {
71-
remove(path)
64+
}
65+
66+
@Test("Can read from a file")
67+
func canRead() throws {
68+
let bytes: [UInt8] = (0 ..< 8192).map { _ in
69+
UInt8.random(in: .min ... .max)
70+
}
71+
try withTemporaryPath { path in
72+
do {
73+
let fileHandle = try FileHandle(forWritingAtPath: path)
74+
try fileHandle.write(bytes)
75+
}
76+
let fileHandle = try FileHandle(forReadingAtPath: path)
77+
let bytes2 = try fileHandle.readToEnd()
78+
#expect(bytes == bytes2)
7279
}
73-
let fileHandle = try FileHandle(forWritingAtPath: path)
74-
try fileHandle.write([0, 1, 2, 3, 4, 5])
75-
try fileHandle.write("Hello world!")
7680
}
7781

7882
@Test("Cannot write bytes to a read-only file")
@@ -160,6 +164,45 @@ struct FileHandleTests {
160164

161165
// MARK: - Fixtures
162166

167+
func temporaryDirectory() throws -> String {
168+
#if SWT_TARGET_OS_APPLE
169+
try withUnsafeTemporaryAllocation(of: CChar.self, capacity: Int(PATH_MAX)) { buffer in
170+
if 0 != confstr(_CS_DARWIN_USER_TEMP_DIR, buffer.baseAddress, buffer.count) {
171+
return String(cString: buffer.baseAddress!)
172+
}
173+
return try #require(Environment.variable(named: "TMPDIR"))
174+
}
175+
#elseif os(Linux)
176+
"/tmp"
177+
#elseif os(Windows)
178+
try withUnsafeTemporaryAllocation(of: wchar_t.self, capacity: Int(MAX_PATH + 1)) { buffer in
179+
// NOTE: GetTempPath2W() was introduced in Windows 10 Build 20348.
180+
if 0 == GetTempPathW(DWORD(buffer.count), buffer.baseAddress) {
181+
throw Win32Error(rawValue: GetLastError())
182+
}
183+
return try #require(String.decodeCString(buffer.baseAddress, as: UTF16.self)?.result)
184+
}
185+
#endif
186+
}
187+
188+
func withTemporaryPath<R>(_ body: (_ path: String) throws -> R) throws -> R {
189+
// NOTE: we are not trying to test mkstemp() here. We are trying to test the
190+
// capacity of FileHandle to open a file for reading or writing and we need a
191+
// temporary file to write to.
192+
#if os(Windows)
193+
let path = try String(unsafeUninitializedCapacity: 1024) { buffer in
194+
try #require(0 == tmpnam_s(buffer.baseAddress!, buffer.count))
195+
return strnlen(buffer.baseAddress!, buffer.count)
196+
}
197+
#else
198+
let path = appendPathComponent("file_named_\(UInt64.random(in: 0 ..< .max))", to: try temporaryDirectory())
199+
#endif
200+
defer {
201+
_ = remove(path)
202+
}
203+
return try body(path)
204+
}
205+
163206
extension FileHandle {
164207
static func temporary() throws -> FileHandle {
165208
#if os(Windows)

Tests/TestingTests/SwiftPMTests.swift

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@
99
//
1010

1111
@testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing
12-
#if canImport(Foundation)
13-
import Foundation
14-
#endif
1512
private import TestingInternals
1613

1714
@Suite("Swift Package Manager Integration Tests")
@@ -130,26 +127,32 @@ struct SwiftPMTests {
130127
}
131128
}
132129

133-
#if canImport(Foundation)
134130
@Test("--xunit-output argument (writes to file)")
135131
func xunitOutputIsWrittenToFile() throws {
136132
// Test that a file is opened when requested. Testing of the actual output
137133
// occurs in ConsoleOutputRecorderTests.
138-
let temporaryFileURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: false)
134+
let tempDirPath = try temporaryDirectory()
135+
let temporaryFilePath = appendPathComponent("\(UInt64.random(in: 0 ..< .max))", to: tempDirPath)
139136
defer {
140-
try? FileManager.default.removeItem(at: temporaryFileURL)
137+
_ = remove(temporaryFilePath)
141138
}
142139
do {
143-
let configuration = try configurationForSwiftPMEntryPoint(withArguments: ["PATH", "--xunit-output", temporaryFileURL.path])
140+
let configuration = try configurationForSwiftPMEntryPoint(withArguments: ["PATH", "--xunit-output", temporaryFilePath])
144141
let eventContext = Event.Context()
145142
configuration.eventHandler(Event(.runStarted, testID: nil, testCaseID: nil), eventContext)
146143
configuration.eventHandler(Event(.runEnded, testID: nil, testCaseID: nil), eventContext)
147144
}
148-
#expect(try temporaryFileURL.checkResourceIsReachable())
145+
146+
let fileHandle = try FileHandle(forReadingAtPath: temporaryFilePath)
147+
let fileContents = try fileHandle.readToEnd()
148+
#expect(!fileContents.isEmpty)
149+
#expect(fileContents.contains(UInt8(ascii: "<")))
150+
#expect(fileContents.contains(UInt8(ascii: ">")))
149151
}
150152

151-
func decodeEventStream(fromFileAt url: URL) throws -> [EventAndContextSnapshot] {
152-
try Data(contentsOf: url, options: [.mappedIfSafe])
153+
#if canImport(Foundation)
154+
func decodeEventStream(fromFileAtPath path: String) throws -> [EventAndContextSnapshot] {
155+
try FileHandle(forReadingAtPath: path).readToEnd()
153156
.split(separator: 10) // "\n"
154157
.map { line in
155158
try line.withUnsafeBytes { line in
@@ -162,12 +165,13 @@ struct SwiftPMTests {
162165
func eventStreamOutput() async throws {
163166
// Test that events are successfully streamed to a file and can be read
164167
// back as snapshots.
165-
let temporaryFileURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: false)
168+
let tempDirPath = try temporaryDirectory()
169+
let temporaryFilePath = appendPathComponent("\(UInt64.random(in: 0 ..< .max))", to: tempDirPath)
166170
defer {
167-
try? FileManager.default.removeItem(at: temporaryFileURL)
171+
_ = remove(temporaryFilePath)
168172
}
169173
do {
170-
let configuration = try configurationForSwiftPMEntryPoint(withArguments: ["PATH", "--experimental-event-stream-output", temporaryFileURL.path])
174+
let configuration = try configurationForSwiftPMEntryPoint(withArguments: ["PATH", "--experimental-event-stream-output", temporaryFilePath])
171175
let eventContext = Event.Context()
172176
configuration.handleEvent(Event(.runStarted, testID: nil, testCaseID: nil), in: eventContext)
173177
do {
@@ -178,9 +182,8 @@ struct SwiftPMTests {
178182
}
179183
configuration.handleEvent(Event(.runEnded, testID: nil, testCaseID: nil), in: eventContext)
180184
}
181-
#expect(try temporaryFileURL.checkResourceIsReachable())
182185

183-
let decodedEvents = try decodeEventStream(fromFileAt: temporaryFileURL)
186+
let decodedEvents = try decodeEventStream(fromFileAtPath: temporaryFilePath)
184187
#expect(decodedEvents.count == 4)
185188
}
186189
#endif

Tests/TestingTests/Traits/TagListTests.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,7 @@
99
//
1010

1111
@testable @_spi(Experimental) @_spi(ForToolsIntegrationOnly) import Testing
12-
13-
#if canImport(Foundation)
14-
import Foundation
15-
#endif
12+
private import TestingInternals
1613

1714
@Suite("Tag/Tag List Tests", .tags(.traitRelated))
1815
struct TagListTests {
@@ -137,9 +134,9 @@ struct TagListTests {
137134
.tags("alpha", "beta", "gamma", "delta"), .tags(.namedConstant)
138135
)
139136
func tagColorsReadFromDisk() throws {
140-
let tempDirURL = FileManager.default.temporaryDirectory
141-
let jsonURL = tempDirURL.appendingPathComponent("tag-colors.json", isDirectory: false)
142-
let jsonContent = """
137+
let tempDirPath = try temporaryDirectory()
138+
let jsonPath = appendPathComponent("tag-colors.json", to: tempDirPath)
139+
var jsonContent = """
143140
{
144141
"alpha": "red",
145142
"beta": "#00CCFF",
@@ -154,12 +151,15 @@ struct TagListTests {
154151
"encode purple": "purple"
155152
}
156153
"""
157-
try jsonContent.write(to: jsonURL, atomically: true, encoding: .utf8)
154+
try jsonContent.withUTF8 { jsonContent in
155+
let fileHandle = try FileHandle(forWritingAtPath: jsonPath)
156+
try fileHandle.write(jsonContent)
157+
}
158158
defer {
159-
try? FileManager.default.removeItem(at: jsonURL)
159+
_ = remove(jsonPath)
160160
}
161161

162-
let tagColors = try Testing.loadTagColors(fromFileInDirectoryAtPath: tempDirURL.path)
162+
let tagColors = try Testing.loadTagColors(fromFileInDirectoryAtPath: tempDirPath)
163163
#expect(tagColors[Tag("alpha")] == .red)
164164
#expect(tagColors[Tag("beta")] == .rgb(0, 0xCC, 0xFF))
165165
#expect(tagColors[Tag("gamma")] == .rgb(0xAA, 0xBB, 0xCC))

0 commit comments

Comments
 (0)