Skip to content

Retry writing file contents in tests on Windows #1966

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions Sources/SKTestSupport/String+writeWithRetry.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import SKLogging

#if compiler(>=6)
package import Foundation
#else
import Foundation
#endif

extension String {
/// Write this string to the given URL using UTF-8 encoding.
///
/// 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.
package func writeWithRetry(to url: URL) async throws {
#if os(Windows)
try await repeatUntilExpectedResult(timeout: .seconds(10), sleepInterval: .milliseconds(200)) {
do {
try self.write(to: url, atomically: true, encoding: .utf8)
return true
} catch {
logger.error("Writing file contents to \(url) failed, will retry: \(error.forLogging)")
return false
}
}
#else
try self.write(to: url, atomically: true, encoding: .utf8)
#endif
}
}
24 changes: 8 additions & 16 deletions Tests/SourceKitLSPTests/BackgroundIndexingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -509,15 +509,7 @@ final class BackgroundIndexingTests: XCTestCase {

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

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

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

let hoverAfterNewVersionCommit = try await project.testClient.send(
Expand Down Expand Up @@ -1403,7 +1395,7 @@ final class BackgroundIndexingTests: XCTestCase {
return value
}
"""
try newLibAContents.write(to: XCTUnwrap(uri.fileURL), atomically: true, encoding: .utf8)
try await newLibAContents.writeWithRetry(to: XCTUnwrap(uri.fileURL))
project.testClient.send(
DidOpenTextDocumentNotification(
textDocument: TextDocumentItem(uri: uri, language: .swift, version: 0, text: newLibAContents)
Expand Down Expand Up @@ -1590,7 +1582,7 @@ final class BackgroundIndexingTests: XCTestCase {
}
"""
)
try newAContents.write(to: XCTUnwrap(libAUri.fileURL), atomically: true, encoding: .utf8)
try await newAContents.writeWithRetry(to: XCTUnwrap(libAUri.fileURL))

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

let libAUri = try project.uri(for: "LibA.swift")
try """
try await """
public struct LibA {
public func test() {}
}
""".write(to: XCTUnwrap(libAUri.fileURL), atomically: true, encoding: .utf8)
""".writeWithRetry(to: XCTUnwrap(libAUri.fileURL))

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

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

try await repeatUntilExpectedResult {
Expand Down
2 changes: 1 addition & 1 deletion Tests/SourceKitLSPTests/CompilationDatabaseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ final class CompilationDatabaseTests: XCTestCase {
// Remove -DFOO from the compile commands.

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

project.testClient.send(
DidChangeWatchedFilesNotification(changes: [
Expand Down
2 changes: 1 addition & 1 deletion Tests/SourceKitLSPTests/DefinitionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ class DefinitionTests: XCTestCase {
let (updatedAPositions, updatedACode) = DocumentPositions.extract(from: "func 2️⃣sayHello() {}")

let aUri = try project.uri(for: "FileA.swift")
try updatedACode.write(to: try XCTUnwrap(aUri.fileURL), atomically: true, encoding: .utf8)
try await updatedACode.writeWithRetry(to: XCTUnwrap(aUri.fileURL))
project.testClient.send(
DidChangeWatchedFilesNotification(changes: [FileEvent(uri: aUri, type: .changed)])
)
Expand Down
4 changes: 2 additions & 2 deletions Tests/SourceKitLSPTests/DependencyTrackingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ final class DependencyTrackingTests: XCTestCase {

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

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

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

let workspace = try await unwrap(project.testClient.server.workspaceForDocument(uri: mainUri))
await workspace.filesDependenciesUpdated([mainUri])
Expand Down
4 changes: 2 additions & 2 deletions Tests/SourceKitLSPTests/LocalClangTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -332,13 +332,13 @@ final class LocalClangTests: XCTestCase {
XCTAssert(initialDiags.diagnostics.isEmpty)

// We rename Object to MyObject in the header.
try """
try await """
struct MyObject {
int field;
};

struct MyObject * newObject();
""".write(to: headerUri.fileURL!, atomically: false, encoding: .utf8)
""".writeWithRetry(to: XCTUnwrap(headerUri.fileURL))

let clangdServer = await project.testClient.server.languageService(
for: mainUri,
Expand Down
2 changes: 1 addition & 1 deletion Tests/SourceKitLSPTests/MainFilesProviderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ final class MainFilesProviderTests: XCTestCase {
#include "\(try project.scratchDirectory.filePath)/Sources/shared.h"
"""
let fancyLibraryUri = try project.uri(for: "MyFancyLibrary.c")
try newFancyLibraryContents.write(to: try XCTUnwrap(fancyLibraryUri.fileURL), atomically: false, encoding: .utf8)
try await newFancyLibraryContents.writeWithRetry(to: XCTUnwrap(fancyLibraryUri.fileURL))
project.testClient.send(
DidChangeWatchedFilesNotification(changes: [FileEvent(uri: fancyLibraryUri, type: .changed)])
)
Expand Down
2 changes: 1 addition & 1 deletion Tests/SourceKitLSPTests/PublishDiagnosticsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ final class PublishDiagnosticsTests: XCTestCase {

let updatedACode = "func sayHello() {}"
let aUri = try project.uri(for: "FileA.swift")
try updatedACode.write(to: try XCTUnwrap(aUri.fileURL), atomically: true, encoding: .utf8)
try await updatedACode.writeWithRetry(to: XCTUnwrap(aUri.fileURL))
project.testClient.send(
DidChangeWatchedFilesNotification(changes: [FileEvent(uri: aUri, type: .changed)])
)
Expand Down
2 changes: 1 addition & 1 deletion Tests/SourceKitLSPTests/PullDiagnosticsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ final class PullDiagnosticsTests: XCTestCase {

let updatedACode = "func sayHello() {}"
let aUri = try project.uri(for: "FileA.swift")
try updatedACode.write(to: try XCTUnwrap(aUri.fileURL), atomically: true, encoding: .utf8)
try await updatedACode.writeWithRetry(to: XCTUnwrap(aUri.fileURL))
project.testClient.send(
DidChangeWatchedFilesNotification(changes: [FileEvent(uri: aUri, type: .changed)])
)
Expand Down
2 changes: 1 addition & 1 deletion Tests/SourceKitLSPTests/SwiftPMIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ final class SwiftPMIntegrationTests: XCTestCase {
l.2️⃣foo()
}
"""
try extractMarkers(newFileContents).textWithoutMarkers.write(to: newFileUrl, atomically: false, encoding: .utf8)
try await extractMarkers(newFileContents).textWithoutMarkers.writeWithRetry(to: newFileUrl)

// Check that we don't get cross-file code completion before we send a `DidChangeWatchedFilesNotification` to make
// sure we didn't include the file in the initial retrieval of build settings.
Expand Down
8 changes: 4 additions & 4 deletions Tests/SourceKitLSPTests/WorkspaceTestDiscoveryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ final class WorkspaceTestDiscoveryTests: XCTestCase {
}6️⃣
"""
)
try newFileContents.write(to: try XCTUnwrap(myTestsUri.fileURL), atomically: true, encoding: .utf8)
try await newFileContents.writeWithRetry(to: XCTUnwrap(myTestsUri.fileURL))
project.testClient.send(DidChangeWatchedFilesNotification(changes: [FileEvent(uri: myTestsUri, type: .changed)]))

let testsAfterDocumentChanged = try await project.testClient.send(WorkspaceTestsRequest())
Expand Down Expand Up @@ -672,7 +672,7 @@ final class WorkspaceTestDiscoveryTests: XCTestCase {
.deletingLastPathComponent()
.appendingPathComponent("MyNewTests.swift")
let uri = DocumentURI(url)
try fileContents.write(to: url, atomically: true, encoding: .utf8)
try await fileContents.writeWithRetry(to: url)
project.testClient.send(
DidChangeWatchedFilesNotification(changes: [FileEvent(uri: uri, type: .created)])
)
Expand Down Expand Up @@ -867,7 +867,7 @@ final class WorkspaceTestDiscoveryTests: XCTestCase {
let testsAfterFileRemove = try await project.testClient.send(WorkspaceTestsRequest())
XCTAssertEqual(testsAfterFileRemove, [])

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

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

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

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

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