@@ -63,6 +63,130 @@ class PreviewActionIntegrationTests: XCTestCase {
6363 return ( sourceURL: sourceURL, outputURL: outputURL, templateURL: templateURL)
6464 }
6565
66+ func testWatchRecoversAfterConversionErrors( ) async throws {
67+ #if os(macOS)
68+ let ( sourceURL, outputURL, templateURL) = try createPreviewSetup ( source: createMinimalDocsBundle ( ) )
69+ defer {
70+ try ? FileManager . default. removeItem ( at: sourceURL)
71+ try ? FileManager . default. removeItem ( at: outputURL)
72+ try ? FileManager . default. removeItem ( at: templateURL)
73+ }
74+
75+ // A FileHandle to read action's output.
76+ let pipeURL = try createTemporaryDirectory ( ) . appendingPathComponent ( " pipe " )
77+ try Data ( ) . write ( to: pipeURL)
78+ let fileHandle = try FileHandle ( forUpdating: pipeURL)
79+ defer { fileHandle. closeFile ( ) }
80+
81+ let convertActionTempDirectory = try createTemporaryDirectory ( )
82+ let createConvertAction = {
83+ try ConvertAction (
84+ documentationBundleURL: sourceURL,
85+ outOfProcessResolver: nil ,
86+ analyze: false ,
87+ targetDirectory: outputURL,
88+ htmlTemplateDirectory: templateURL,
89+ emitDigest: false ,
90+ currentPlatforms: nil ,
91+ fileManager: FileManager . default,
92+ temporaryDirectory: convertActionTempDirectory)
93+ }
94+
95+ let preview = try PreviewAction (
96+ port: 8080 , // We ignore this value when we set the `bindServerToSocketPath` property below.
97+ createConvertAction: createConvertAction
98+ )
99+ defer {
100+ try ? preview. stop ( )
101+ }
102+
103+ preview. bindServerToSocketPath = try createTemporaryTestSocketPath ( )
104+
105+ let logStorage = LogHandle . LogStorage ( )
106+
107+ // The technology output file URL
108+ let convertedOverviewURL = outputURL
109+ . appendingPathComponent ( " data " )
110+ . appendingPathComponent ( " tutorials " )
111+ . appendingPathComponent ( " Overview.json " )
112+
113+ // Start watching the source and get the initial (successful) state.
114+ do {
115+ let didStartServerExpectation = asyncLogExpectation ( log: logStorage, description: " Did start the preview server " , expectedText: " ======= " )
116+
117+ // Start the preview and keep it running for the asserts that follow inside this test.
118+ Task {
119+ var logHandle = LogHandle . memory ( logStorage)
120+ let result = try await preview. perform ( logHandle: & logHandle)
121+
122+ guard !result. problems. containsErrors else {
123+ throw ErrorsEncountered ( )
124+ }
125+ }
126+
127+ // This should only take 1.5 seconds (1 second for the directory monitor debounce and 0.5 seconds for the expectation poll interval)
128+ await fulfillment ( of: [ didStartServerExpectation] , timeout: 20.0 )
129+
130+ // Check the log output to confirm that expected informational text is printed
131+ let logOutput = logStorage. text
132+
133+ let expectedLogIntroductoryOutput = """
134+ Input: \( sourceURL. path)
135+ Template: \( templateURL. path)
136+ """
137+ XCTAssertTrue ( logOutput. hasPrefix ( expectedLogIntroductoryOutput) , """
138+ Missing expected input and template information in log/print output
139+ """ )
140+
141+ if let previewInfoStart = logOutput. range ( of: " ===== \n " ) ? . upperBound,
142+ let previewInfoEnd = logOutput [ previewInfoStart... ] . range ( of: " \n ===== " ) ? . lowerBound {
143+ XCTAssertEqual ( logOutput [ previewInfoStart..< previewInfoEnd] , """
144+ Starting Local Preview Server
145+ \t Address: http://localhost:8080/documentation/mykit
146+ \t http://localhost:8080/tutorials/overview
147+ """ )
148+ } else {
149+ XCTFail ( " Missing preview information in log/print output " )
150+ }
151+
152+ XCTAssertTrue ( FileManager . default. fileExists ( atPath: convertedOverviewURL. path, isDirectory: nil ) )
153+ }
154+
155+ // Verify conversion result.
156+ let overview = try JSONDecoder ( ) . decode ( RenderNode . self, from: Data ( contentsOf: convertedOverviewURL) )
157+ let introSection = try XCTUnwrap ( overview. sections. first ( where: { $0. kind == . hero } ) as? IntroRenderSection )
158+ XCTAssertEqual ( introSection. title, " Technology X " )
159+
160+ let invalidJSONSymbolGraphURL = sourceURL. appendingPathComponent ( " invalid-incomplete-data.symbols.json " )
161+
162+ // Start watching the source and detect failed conversion.
163+ do {
164+ let didFailRebuiltExpectation = asyncLogExpectation ( log: logStorage, description: " Did notice changed input and failed rebuild " , expectedText: " Compilation failed " )
165+
166+ // this is invalid JSON and will result in an error
167+ try " { " . write ( to: invalidJSONSymbolGraphURL, atomically: true , encoding: . utf8)
168+
169+ // This should only take 1.5 seconds (1 second for the directory monitor debounce and 0.5 seconds for the expectation poll interval)
170+ await fulfillment ( of: [ didFailRebuiltExpectation] , timeout: 20.0 )
171+ }
172+
173+ // Start watching the source and detect recovery and successful conversion after a failure.
174+ do {
175+ let didSuccessfullyRebuiltExpectation = asyncLogExpectation ( log: logStorage, description: " Did notice changed input (again) and finished rebuild " , expectedText: " Done " )
176+
177+ try FileManager . default. removeItem ( at: invalidJSONSymbolGraphURL)
178+
179+ // This should only take 1.5 seconds (1 second for the directory monitor debounce and 0.5 seconds for the expectation poll interval)
180+ await fulfillment ( of: [ didSuccessfullyRebuiltExpectation] , timeout: 20.0 )
181+
182+ // Check conversion result.
183+ let overview = try JSONDecoder ( ) . decode ( RenderNode . self, from: Data ( contentsOf: convertedOverviewURL) )
184+ let introSection = try XCTUnwrap ( overview. sections. first ( where: { $0. kind == . hero } ) as? IntroRenderSection )
185+ XCTAssertEqual ( introSection. title, " Technology X " )
186+ }
187+ #endif
188+ }
189+
66190 func testThrowsHumanFriendlyErrorWhenCannotStartServerOnAGivenPort( ) async throws {
67191 // Binding an invalid address
68192 try await assert ( bindPort: - 1 , expectedErrorMessage: " Can't start the preview server on port -1 " )
0 commit comments