Skip to content

Commit fba2b56

Browse files
committed
Retry writing file contents in tests on Windows
Sometimes file writes fail on Windows because another process (like sourcekitd or clangd) still has exclusive access to the file but releases it soon after. Retry to save the file if this happens. This matches what a user would do.
1 parent 883a17c commit fba2b56

11 files changed

+64
-30
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SKLogging
14+
15+
#if compiler(>=6)
16+
package import Foundation
17+
#else
18+
import Foundation
19+
#endif
20+
21+
extension String {
22+
/// Write this string to the given URL using UTF-8 encoding.
23+
///
24+
/// Sometimes file writes fail on Windows because another process (like sourcekitd or clangd) still has exclusive
25+
/// access to the file but releases it soon after. Retry to save the file if this happens. This matches what a user
26+
/// would do.
27+
package func writeWithRetry(to url: URL) async throws {
28+
#if os(Windows)
29+
try await repeatUntilExpectedResult(timeout: .seconds(10), sleepInterval: .milliseconds(200)) {
30+
do {
31+
try self.write(to: url, atomically: true, encoding: .utf8)
32+
return true
33+
} catch {
34+
logger.error("Writing file contents to \(url) failed, will retry: \(error.forLogging)")
35+
return false
36+
}
37+
}
38+
#else
39+
try self.write(to: url, atomically: true, encoding: .utf8)
40+
#endif
41+
}
42+
}

Tests/SourceKitLSPTests/BackgroundIndexingTests.swift

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -509,15 +509,7 @@ final class BackgroundIndexingTests: XCTestCase {
509509

510510
// clangd might have Header.h open, which prevents us from updating it. Keep retrying until we get a successful
511511
// write. This matches what a user would do.
512-
try await repeatUntilExpectedResult {
513-
do {
514-
try headerNewMarkedContents.write(to: try XCTUnwrap(uri.fileURL), atomically: true, encoding: .utf8)
515-
return true
516-
} catch {
517-
logger.error("Writing new Header.h failed, will retry: \(error.forLogging)")
518-
return false
519-
}
520-
}
512+
try await headerNewMarkedContents.writeWithRetry(to: try XCTUnwrap(uri.fileURL))
521513

522514
project.testClient.send(DidChangeWatchedFilesNotification(changes: [FileEvent(uri: uri, type: .changed)]))
523515
try await project.testClient.send(PollIndexRequest())
@@ -1254,10 +1246,10 @@ final class BackgroundIndexingTests: XCTestCase {
12541246

12551247
// Just committing a new version of the dependency shouldn't change anything because we didn't update the package
12561248
// dependencies.
1257-
try """
1249+
try await """
12581250
/// Do something v1.1.0
12591251
public func doSomething() {}
1260-
""".write(to: dependencySwiftURL, atomically: true, encoding: .utf8)
1252+
""".writeWithRetry(to: dependencySwiftURL)
12611253
try await dependencyProject.tag(changedFiles: [dependencySwiftURL], version: "1.1.0")
12621254

12631255
let hoverAfterNewVersionCommit = try await project.testClient.send(
@@ -1403,7 +1395,7 @@ final class BackgroundIndexingTests: XCTestCase {
14031395
return value
14041396
}
14051397
"""
1406-
try newLibAContents.write(to: XCTUnwrap(uri.fileURL), atomically: true, encoding: .utf8)
1398+
try await newLibAContents.writeWithRetry(to: XCTUnwrap(uri.fileURL))
14071399
project.testClient.send(
14081400
DidOpenTextDocumentNotification(
14091401
textDocument: TextDocumentItem(uri: uri, language: .swift, version: 0, text: newLibAContents)
@@ -1590,7 +1582,7 @@ final class BackgroundIndexingTests: XCTestCase {
15901582
}
15911583
"""
15921584
)
1593-
try newAContents.write(to: XCTUnwrap(libAUri.fileURL), atomically: true, encoding: .utf8)
1585+
try await newAContents.writeWithRetry(to: XCTUnwrap(libAUri.fileURL))
15941586

15951587
project.testClient.send(
15961588
DidChangeWatchedFilesNotification(changes: [FileEvent(uri: libAUri, type: .changed)])
@@ -1645,11 +1637,11 @@ final class BackgroundIndexingTests: XCTestCase {
16451637
XCTAssertEqual(completionBeforeEdit.items.map(\.label), ["self"])
16461638

16471639
let libAUri = try project.uri(for: "LibA.swift")
1648-
try """
1640+
try await """
16491641
public struct LibA {
16501642
public func test() {}
16511643
}
1652-
""".write(to: XCTUnwrap(libAUri.fileURL), atomically: true, encoding: .utf8)
1644+
""".writeWithRetry(to: XCTUnwrap(libAUri.fileURL))
16531645

16541646
project.testClient.send(
16551647
DidChangeWatchedFilesNotification(changes: [FileEvent(uri: libAUri, type: .changed)])
@@ -1708,7 +1700,7 @@ final class BackgroundIndexingTests: XCTestCase {
17081700
)
17091701

17101702
let libAUri = try project.uri(for: "LibA.swift")
1711-
try "public let myVar: Int".write(to: try XCTUnwrap(libAUri.fileURL), atomically: true, encoding: .utf8)
1703+
try await "public let myVar: Int".writeWithRetry(to: try XCTUnwrap(libAUri.fileURL))
17121704
project.testClient.send(DidChangeWatchedFilesNotification(changes: [FileEvent(uri: libAUri, type: .changed)]))
17131705

17141706
try await repeatUntilExpectedResult {

Tests/SourceKitLSPTests/CompilationDatabaseTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ final class CompilationDatabaseTests: XCTestCase {
5757
// Remove -DFOO from the compile commands.
5858

5959
let compileFlagsUri = try project.uri(for: FixedCompilationDatabaseBuildSystem.dbName)
60-
try "".write(to: compileFlagsUri.fileURL!, atomically: false, encoding: .utf8)
60+
try await "".writeWithRetry(to: XCTUnwrap(compileFlagsUri.fileURL))
6161

6262
project.testClient.send(
6363
DidChangeWatchedFilesNotification(changes: [

Tests/SourceKitLSPTests/DefinitionTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ class DefinitionTests: XCTestCase {
354354
let (updatedAPositions, updatedACode) = DocumentPositions.extract(from: "func 2️⃣sayHello() {}")
355355

356356
let aUri = try project.uri(for: "FileA.swift")
357-
try updatedACode.write(to: try XCTUnwrap(aUri.fileURL), atomically: true, encoding: .utf8)
357+
try await updatedACode.writeWithRetry(to: XCTUnwrap(aUri.fileURL))
358358
project.testClient.send(
359359
DidChangeWatchedFilesNotification(changes: [FileEvent(uri: aUri, type: .changed)])
360360
)

Tests/SourceKitLSPTests/DependencyTrackingTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ final class DependencyTrackingTests: XCTestCase {
9898

9999
// Write an empty header file first since clangd doesn't handle missing header
100100
// files without a recently upstreamed extension.
101-
try "".write(to: generatedHeaderURL, atomically: true, encoding: .utf8)
101+
try await "".writeWithRetry(to: generatedHeaderURL)
102102
let (mainUri, _) = try project.openDocument("main.c")
103103

104104
let openDiags = try await project.testClient.nextDiagnosticsNotification()
@@ -108,7 +108,7 @@ final class DependencyTrackingTests: XCTestCase {
108108

109109
// Update the header file to have the proper contents for our code to build.
110110
let contents = "int libX(int value);"
111-
try contents.write(to: generatedHeaderURL, atomically: true, encoding: .utf8)
111+
try await contents.writeWithRetry(to: generatedHeaderURL)
112112

113113
let workspace = try await unwrap(project.testClient.server.workspaceForDocument(uri: mainUri))
114114
await workspace.filesDependenciesUpdated([mainUri])

Tests/SourceKitLSPTests/LocalClangTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,13 +332,13 @@ final class LocalClangTests: XCTestCase {
332332
XCTAssert(initialDiags.diagnostics.isEmpty)
333333

334334
// We rename Object to MyObject in the header.
335-
try """
335+
try await """
336336
struct MyObject {
337337
int field;
338338
};
339339
340340
struct MyObject * newObject();
341-
""".write(to: headerUri.fileURL!, atomically: false, encoding: .utf8)
341+
""".writeWithRetry(to: XCTUnwrap(headerUri.fileURL))
342342

343343
let clangdServer = await project.testClient.server.languageService(
344344
for: mainUri,

Tests/SourceKitLSPTests/MainFilesProviderTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ final class MainFilesProviderTests: XCTestCase {
192192
#include "\(try project.scratchDirectory.filePath)/Sources/shared.h"
193193
"""
194194
let fancyLibraryUri = try project.uri(for: "MyFancyLibrary.c")
195-
try newFancyLibraryContents.write(to: try XCTUnwrap(fancyLibraryUri.fileURL), atomically: false, encoding: .utf8)
195+
try await newFancyLibraryContents.writeWithRetry(to: XCTUnwrap(fancyLibraryUri.fileURL))
196196
project.testClient.send(
197197
DidChangeWatchedFilesNotification(changes: [FileEvent(uri: fancyLibraryUri, type: .changed)])
198198
)

Tests/SourceKitLSPTests/PublishDiagnosticsTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ final class PublishDiagnosticsTests: XCTestCase {
148148

149149
let updatedACode = "func sayHello() {}"
150150
let aUri = try project.uri(for: "FileA.swift")
151-
try updatedACode.write(to: try XCTUnwrap(aUri.fileURL), atomically: true, encoding: .utf8)
151+
try await updatedACode.writeWithRetry(to: XCTUnwrap(aUri.fileURL))
152152
project.testClient.send(
153153
DidChangeWatchedFilesNotification(changes: [FileEvent(uri: aUri, type: .changed)])
154154
)

Tests/SourceKitLSPTests/PullDiagnosticsTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ final class PullDiagnosticsTests: XCTestCase {
198198

199199
let updatedACode = "func sayHello() {}"
200200
let aUri = try project.uri(for: "FileA.swift")
201-
try updatedACode.write(to: try XCTUnwrap(aUri.fileURL), atomically: true, encoding: .utf8)
201+
try await updatedACode.writeWithRetry(to: XCTUnwrap(aUri.fileURL))
202202
project.testClient.send(
203203
DidChangeWatchedFilesNotification(changes: [FileEvent(uri: aUri, type: .changed)])
204204
)

Tests/SourceKitLSPTests/SwiftPMIntegrationTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ final class SwiftPMIntegrationTests: XCTestCase {
121121
l.2️⃣foo()
122122
}
123123
"""
124-
try extractMarkers(newFileContents).textWithoutMarkers.write(to: newFileUrl, atomically: false, encoding: .utf8)
124+
try await extractMarkers(newFileContents).textWithoutMarkers.writeWithRetry(to: newFileUrl)
125125

126126
// Check that we don't get cross-file code completion before we send a `DidChangeWatchedFilesNotification` to make
127127
// sure we didn't include the file in the initial retrieval of build settings.

Tests/SourceKitLSPTests/WorkspaceTestDiscoveryTests.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ final class WorkspaceTestDiscoveryTests: XCTestCase {
172172
}6️⃣
173173
"""
174174
)
175-
try newFileContents.write(to: try XCTUnwrap(myTestsUri.fileURL), atomically: true, encoding: .utf8)
175+
try await newFileContents.writeWithRetry(to: XCTUnwrap(myTestsUri.fileURL))
176176
project.testClient.send(DidChangeWatchedFilesNotification(changes: [FileEvent(uri: myTestsUri, type: .changed)]))
177177

178178
let testsAfterDocumentChanged = try await project.testClient.send(WorkspaceTestsRequest())
@@ -672,7 +672,7 @@ final class WorkspaceTestDiscoveryTests: XCTestCase {
672672
.deletingLastPathComponent()
673673
.appendingPathComponent("MyNewTests.swift")
674674
let uri = DocumentURI(url)
675-
try fileContents.write(to: url, atomically: true, encoding: .utf8)
675+
try await fileContents.writeWithRetry(to: url)
676676
project.testClient.send(
677677
DidChangeWatchedFilesNotification(changes: [FileEvent(uri: uri, type: .created)])
678678
)
@@ -867,7 +867,7 @@ final class WorkspaceTestDiscoveryTests: XCTestCase {
867867
let testsAfterFileRemove = try await project.testClient.send(WorkspaceTestsRequest())
868868
XCTAssertEqual(testsAfterFileRemove, [])
869869

870-
try extractMarkers(markedFileContents).textWithoutMarkers.write(to: myTestsUrl, atomically: true, encoding: .utf8)
870+
try await extractMarkers(markedFileContents).textWithoutMarkers.writeWithRetry(to: myTestsUrl)
871871
project.testClient.send(DidChangeWatchedFilesNotification(changes: [FileEvent(uri: myTestsUri, type: .created)]))
872872

873873
let testsAfterFileReAdded = try await project.testClient.send(WorkspaceTestsRequest())
@@ -935,7 +935,7 @@ final class WorkspaceTestDiscoveryTests: XCTestCase {
935935

936936
let uri = try XCTUnwrap(project.fileURI.fileURL)
937937

938-
try (originalContents + addedTest).write(to: uri, atomically: true, encoding: .utf8)
938+
try await (originalContents + addedTest).writeWithRetry(to: uri)
939939

940940
project.testClient.send(
941941
DidChangeWatchedFilesNotification(changes: [FileEvent(uri: project.fileURI, type: .changed)])

0 commit comments

Comments
 (0)