Skip to content

Commit 528c7ef

Browse files
authored
Merge pull request #1966 from ahoppen/ahoppen/retry-file-write
Retry writing file contents in tests on Windows
2 parents bd55793 + fba2b56 commit 528c7ef

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)