11/*
22 This source file is part of the Swift.org open source project
33
4- Copyright (c) 2021-2024 Apple Inc. and the Swift project authors
4+ Copyright (c) 2021-2025 Apple Inc. and the Swift project authors
55 Licensed under Apache License v2.0 with Runtime Library Exception
66
77 See https://swift.org/LICENSE.txt for license information
@@ -30,7 +30,7 @@ class DocumentationCuratorTests: XCTestCase {
3030 func testCrawl( ) throws {
3131 let ( bundle, context) = try testBundleAndContext ( named: " LegacyBundle_DoNotUseInNewTests " )
3232
33- var crawler = DocumentationCurator . init ( in: context, bundle: bundle)
33+ var crawler = DocumentationCurator ( in: context, bundle: bundle)
3434 let mykit = try context. entity ( with: ResolvedTopicReference ( bundleID: " org.swift.docc.example " , path: " /documentation/MyKit " , sourceLanguage: . swift) )
3535
3636 var symbolsWithCustomCuration = [ ResolvedTopicReference] ( )
@@ -303,7 +303,7 @@ class DocumentationCuratorTests: XCTestCase {
303303 """ . write ( to: url. appendingPathComponent ( " Root.md " ) , atomically: true , encoding: . utf8)
304304 }
305305
306- let crawler = DocumentationCurator . init ( in: context, bundle: bundle)
306+ let crawler = DocumentationCurator ( in: context, bundle: bundle)
307307 XCTAssert ( context. problems. isEmpty, " Expected no problems. Found: \( context. problems. map ( \. diagnostic. summary) ) " )
308308
309309 guard let moduleNode = context. documentationCache [ " SourceLocations " ] ,
@@ -316,11 +316,109 @@ class DocumentationCuratorTests: XCTestCase {
316316
317317 XCTAssertEqual ( root. path, " /documentation/Root " )
318318 XCTAssertEqual ( crawler. problems. count, 0 )
319-
319+ }
320+
321+ func testCuratorDoesNotRelateNodesWhenArticleLinksContainExtraPathComponents( ) throws {
322+ let ( bundle, context) = try loadBundle ( catalog:
323+ Folder ( name: " CatalogName.docc " , content: [
324+ TextFile ( name: " Root.md " , utf8Content: """
325+ # Root
326+
327+ @Metadata {
328+ @TechnologyRoot
329+ }
330+
331+ Add an API Collection of indirection to more easily detect the failed curation.
332+
333+ ## Topics
334+ - <doc:API-Collection>
335+ """ ) ,
336+
337+ TextFile ( name: " API-Collection.md " , utf8Content: """
338+ # Some API Collection
339+
340+ Fail to curate all 4 articles because of extra incorrect path components.
341+
342+ ## Topics
343+
344+ ### No links will resolve in this section
345+
346+ - <doc:WrongModuleName/First>
347+ - <doc:documentation/WrongModuleName/Second>
348+ - <doc:documentation/CatalogName/ExtraPathComponent/Third>
349+ - <doc:CatalogName/ExtraPathComponent/Forth>
350+ """ ) ,
351+
352+ TextFile ( name: " First.md " , utf8Content: " # First " ) ,
353+ TextFile ( name: " Second.md " , utf8Content: " # Second " ) ,
354+ TextFile ( name: " Third.md " , utf8Content: " # Third " ) ,
355+ TextFile ( name: " Forth.md " , utf8Content: " # Forth " ) ,
356+ ] )
357+ )
358+ let ( linkResolutionProblems, otherProblems) = context. problems. categorize ( where: { $0. diagnostic. identifier == " org.swift.docc.unresolvedTopicReference " } )
359+ XCTAssert ( otherProblems. isEmpty, " Unexpected problems: \( otherProblems. map ( \. diagnostic. summary) . sorted ( ) ) " )
360+
361+ XCTAssertEqual (
362+ linkResolutionProblems. map ( \. diagnostic. source? . lastPathComponent) ,
363+ [ " API-Collection.md " , " API-Collection.md " , " API-Collection.md " , " API-Collection.md " ] ,
364+ " Every unresolved link is in the API collection "
365+ )
366+ XCTAssertEqual (
367+ linkResolutionProblems. map ( { $0. diagnostic. range? . lowerBound. line } ) , [ 9 , 10 , 11 , 12 ] ,
368+ " There should be one warning about an unresolved reference for each link in the API collection's top "
369+ )
370+
371+ let rootReference = try XCTUnwrap ( context. soleRootModuleReference)
372+
373+ for articleName in [ " First " , " Second " , " Third " , " Forth " ] {
374+ let reference = try XCTUnwrap ( context. documentationCache. allReferences. first ( where: { $0. lastPathComponent == articleName } ) )
375+ XCTAssertEqual (
376+ context. topicGraph. nodeWithReference ( reference) ? . shouldAutoCurateInCanonicalLocation, true ,
377+ " Article ' \( articleName) ' isn't (successfully) manually curated and should therefore automatically curate. "
378+ )
379+ XCTAssertEqual (
380+ context. topicGraph. reverseEdges [ reference] ? . map ( \. path) , [ rootReference. path] ,
381+ " Article ' \( articleName) ' should only have a reverse edge to the root page where it will be automatically curated. "
382+ )
383+ }
384+
385+ let apiCollectionReference = try XCTUnwrap ( context. documentationCache. allReferences. first ( where: { $0. lastPathComponent == " API-Collection " } ) )
386+ let apiCollectionSemantic = try XCTUnwrap ( try context. entity ( with: apiCollectionReference) . semantic as? Article )
387+ XCTAssertEqual ( apiCollectionSemantic. topics? . taskGroups. count, 1 , " The API Collection has one topic section " )
388+ let topicSection = try XCTUnwrap ( apiCollectionSemantic. topics? . taskGroups. first)
389+ XCTAssertEqual ( topicSection. links. map ( \. destination) , [
390+ // All these links are the same as they were authored which means that they didn't resolve.
391+ " doc:WrongModuleName/First " ,
392+ " doc:documentation/WrongModuleName/Second " ,
393+ " doc:documentation/CatalogName/ExtraPathComponent/Third " ,
394+ " doc:CatalogName/ExtraPathComponent/Forth " ,
395+ ] )
396+
397+ let rootPage = try context. entity ( with: rootReference)
398+ let renderer = DocumentationNodeConverter ( bundle: bundle, context: context)
399+ let renderNode = renderer. convert ( rootPage)
400+
401+ XCTAssertEqual ( renderNode. topicSections. map ( \. title) , [
402+ nil , // An unnamed topic section
403+ " Articles " , // The automatic topic section
404+ ] )
405+ XCTAssertEqual ( renderNode. topicSections. map { $0. identifiers. sorted ( ) } , [
406+ // The unnamed topic section curates the API collection
407+ [
408+ " doc://CatalogName/documentation/CatalogName/API-Collection "
409+ ] ,
410+ // The automatic "Articles" section curates all 4 articles
411+ [
412+ " doc://CatalogName/documentation/CatalogName/First " ,
413+ " doc://CatalogName/documentation/CatalogName/Forth " ,
414+ " doc://CatalogName/documentation/CatalogName/Second " ,
415+ " doc://CatalogName/documentation/CatalogName/Third " ,
416+ ] ,
417+ ] )
320418 }
321419
322420 func testModuleUnderAncestorOfTechnologyRoot( ) throws {
323- let ( _, bundle , context) = try testBundleAndContext ( copying: " SourceLocations " ) { url in
421+ let ( _, _ , context) = try testBundleAndContext ( copying: " SourceLocations " ) { url in
324422 try """
325423 # Root with ancestor curating a module
326424
@@ -347,7 +445,6 @@ class DocumentationCuratorTests: XCTestCase {
347445 """ . write ( to: url. appendingPathComponent ( " Ancestor.md " ) , atomically: true , encoding: . utf8)
348446 }
349447
350- let _ = DocumentationCurator . init ( in: context, bundle: bundle)
351448 XCTAssert ( context. problems. isEmpty, " Expected no problems. Found: \( context. problems. map ( \. diagnostic. summary) ) " )
352449
353450 guard let moduleNode = context. documentationCache [ " SourceLocations " ] ,
@@ -364,7 +461,7 @@ class DocumentationCuratorTests: XCTestCase {
364461 func testSymbolLinkResolving( ) throws {
365462 let ( bundle, context) = try testBundleAndContext ( named: " LegacyBundle_DoNotUseInNewTests " )
366463
367- let crawler = DocumentationCurator . init ( in: context, bundle: bundle)
464+ let crawler = DocumentationCurator ( in: context, bundle: bundle)
368465
369466 // Resolve top-level symbol in module parent
370467 do {
@@ -417,7 +514,7 @@ class DocumentationCuratorTests: XCTestCase {
417514 func testLinkResolving( ) throws {
418515 let ( sourceRoot, bundle, context) = try testBundleAndContext ( named: " LegacyBundle_DoNotUseInNewTests " )
419516
420- var crawler = DocumentationCurator . init ( in: context, bundle: bundle)
517+ var crawler = DocumentationCurator ( in: context, bundle: bundle)
421518
422519 // Resolve and curate an article in module root (absolute link)
423520 do {
@@ -510,7 +607,7 @@ class DocumentationCuratorTests: XCTestCase {
510607 """ . write ( to: root. appendingPathComponent ( " documentation " ) . appendingPathComponent ( " api-collection.md " ) , atomically: true , encoding: . utf8)
511608 }
512609
513- var crawler = DocumentationCurator . init ( in: context, bundle: bundle)
610+ var crawler = DocumentationCurator ( in: context, bundle: bundle)
514611 let reference = ResolvedTopicReference ( bundleID: " org.swift.docc.example " , path: " /documentation/SideKit " , sourceLanguage: . swift)
515612
516613 try crawler. crawlChildren ( of: reference, prepareForCuration: { _ in } ) { ( _, _) in }
0 commit comments