@@ -265,7 +265,7 @@ public struct CoverageOptions: ParsableArguments {
265265 . customLong( " show-code-coverage-path " ) ,
266266 . customLong( " show-coverage-path " ) ,
267267 ] ,
268- help: " Print the path of the exported code coverage JSON file . " ,
268+ help: " Print the path of the exported code coverage files . " ,
269269 )
270270 var shouldPrintPath : Bool = false
271271
@@ -399,11 +399,40 @@ package struct CoverageFormatOutput: Encodable {
399399 package init ( ) {
400400 self . _underlying = [ CoverageFormat : AbsolutePath] ( )
401401 }
402-
402+
403403 package init ( data: [ CoverageFormat : AbsolutePath ] ) {
404404 self . _underlying = data
405405 }
406406
407+ // Custom encoding to ensure the dictionary is encoded as a JSON object, not an array
408+ public func encode( to encoder: Encoder ) throws {
409+ // Use keyed container to encode each format and its path
410+ // This will create proper JSON objects and proper plain text "key: value" format
411+ var container = encoder. container ( keyedBy: DynamicCodingKey . self)
412+
413+ // Sort entries for consistent output
414+ let sortedEntries = _underlying. sorted { $0. key. rawValue < $1. key. rawValue }
415+
416+ for (format, path) in sortedEntries {
417+ let key = DynamicCodingKey ( stringValue: format. rawValue) !
418+ try container. encode ( path. pathString, forKey: key)
419+ }
420+ }
421+
422+ // Dynamic coding keys for the formats
423+ private struct DynamicCodingKey : CodingKey {
424+ var stringValue : String
425+ var intValue : Int ? { nil }
426+
427+ init ? ( stringValue: String ) {
428+ self . stringValue = stringValue
429+ }
430+
431+ init ? ( intValue: Int ) {
432+ return nil
433+ }
434+ }
435+
407436 /// Adds a key/value pair to the underlying dictionary.
408437 /// - Parameters:
409438 /// - format: The coverage format key
@@ -420,7 +449,7 @@ package struct CoverageFormatOutput: Encodable {
420449 package subscript( format: CoverageFormat ) -> AbsolutePath ? {
421450 return _underlying [ format]
422451 }
423-
452+
424453 /// Gets the path for a format, throwing an error if it doesn't exist.
425454 /// - Parameter format: The coverage format
426455 /// - Returns: The absolute path for the format
@@ -431,48 +460,17 @@ package struct CoverageFormatOutput: Encodable {
431460 }
432461 return path
433462 }
434-
463+
435464 /// Returns all formats currently stored
436465 package var formats : [ CoverageFormat ] {
437466 return Array ( _underlying. keys) . sorted ( )
438467 }
439-
468+
440469 /// Iterate over format/path pairs
441470 package func forEach( _ body: ( CoverageFormat , AbsolutePath ) throws -> Void ) rethrows {
442471 try _underlying. forEach ( body)
443472 }
444-
445- /// Encodes the coverage format output as JSON string
446- /// - Returns: JSON string representation of the format/path mapping
447- /// - Throws: `StringError` if JSON encoding fails
448- package func encodeAsJSON( ) throws -> String {
449- let sortedData = _underlying. sorted { $0. key. rawValue < $1. key. rawValue }
450- let jsonObject : [ String : String ] = Dictionary ( uniqueKeysWithValues: sortedData. map { ( $0. key. rawValue, $0. value. pathString) } )
451-
452- do {
453- let jsonData = try JSONSerialization . data ( withJSONObject: jsonObject, options: [ . prettyPrinted, . sortedKeys] )
454- guard let jsonString = String ( data: jsonData, encoding: . utf8) else {
455- throw StringError ( " Failed to convert JSON data to string " )
456- }
457- return jsonString
458- } catch {
459- throw StringError ( " Failed to encode coverage format output as JSON: \( error) " )
460- }
461- }
462-
463- /// Encodes the coverage format output as plain text
464- /// - Returns: Text string with format/path pairs, one per line
465- package func encodeAsText( ) -> String {
466- let sortedFormats = _underlying. keys. sorted ( )
467- return sortedFormats. map { format in
468- let value = _underlying [ format] !. pathString
469- if _underlying. count == 1 {
470- return value
471- } else {
472- return " \( format. rawValue. uppercased ( ) ) : \( value) "
473- }
474- } . joined ( separator: " \n " )
475- }
473+
476474}
477475
478476struct CodeCoverageConfiguration {
@@ -1122,29 +1120,29 @@ extension SwiftTestCommand {
11221120 let config = try await self . getCodeCoverageConfiguration ( swiftCommandState, format: format)
11231121 coverageData [ format] = config. outputDir
11241122 }
1125-
1126- let coverageOutput = CoverageFormatOutput ( data: coverageData)
1127-
1128- switch printMode {
1129- case . json:
1130- let jsonOutput = try coverageOutput. encodeAsJSON ( )
1131- print ( jsonOutput)
1132- case . text:
1133- let textOutput = coverageOutput. encodeAsText ( )
1134- print ( textOutput)
1135- }
11361123
1137- print ( " ----------------------- " )
11381124 let data : Data
11391125 switch printMode {
11401126 case . json:
1127+ let coverageOutput = CoverageFormatOutput ( data: coverageData)
11411128 let encoder = JSONEncoder . makeWithDefaults ( )
11421129 encoder. keyEncodingStrategy = . convertToSnakeCase
11431130 data = try encoder. encode ( coverageOutput)
11441131 case . text:
1145- var encoder = PlainTextEncoder ( )
1146- encoder. formattingOptions = [ . prettyPrinted]
1147- data = try encoder. encode ( coverageOutput)
1132+ // When there's only one format, don't show the key prefix
1133+ if formats. count == 1 , let singlePath = coverageData. values. first {
1134+ swiftCommandState. observabilityScope. emit (
1135+ warning: """
1136+ The contents of this output are subject to change in the future. Use `--print-coverage-path-mode json` if the output is required in a script.
1137+ """ ,
1138+ )
1139+ data = Data ( " \( singlePath. pathString) " . utf8)
1140+ } else {
1141+ let coverageOutput = CoverageFormatOutput ( data: coverageData)
1142+ var encoder = PlainTextEncoder ( )
1143+ encoder. formattingOptions = [ . prettyPrinted]
1144+ data = try encoder. encode ( coverageOutput)
1145+ }
11481146 }
11491147 print ( String ( decoding: data, as: UTF8 . self) )
11501148 }
0 commit comments