@@ -30,9 +30,15 @@ use nextest_runner::{
3030 partition:: PartitionerBuilder ,
3131 platform:: { BuildPlatforms , HostPlatform , PlatformLibdir , TargetPlatform } ,
3232 redact:: Redactor ,
33- reporter:: { structured, FinalStatusLevel , StatusLevel , TestOutputDisplay , TestReporterBuilder } ,
33+ reporter:: {
34+ heuristic_extract_description, highlight_end, structured, DescriptionKind ,
35+ FinalStatusLevel , StatusLevel , TestOutputDisplay , TestReporterBuilder ,
36+ } ,
3437 reuse_build:: { archive_to_file, ArchiveReporter , PathMapper , ReuseBuildInfo } ,
35- runner:: { configure_handle_inheritance, FinalRunStats , RunStatsFailureKind , TestRunnerBuilder } ,
38+ runner:: {
39+ configure_handle_inheritance, ExecutionResult , FinalRunStats , RunStatsFailureKind ,
40+ TestRunnerBuilder ,
41+ } ,
3642 show_config:: { ShowNextestVersion , ShowTestGroupSettings , ShowTestGroups , ShowTestGroupsMode } ,
3743 signal:: SignalHandlerKind ,
3844 target_runner:: { PlatformRunner , TargetRunner } ,
@@ -42,10 +48,12 @@ use nextest_runner::{
4248} ;
4349use once_cell:: sync:: OnceCell ;
4450use owo_colors:: { OwoColorize , Stream , Style } ;
51+ use quick_junit:: XmlString ;
4552use semver:: Version ;
4653use std:: {
4754 collections:: BTreeSet ,
4855 env:: VarError ,
56+ fmt,
4957 io:: { Cursor , Write } ,
5058 sync:: Arc ,
5159} ;
@@ -170,6 +178,7 @@ impl AppOpts {
170178 output_writer,
171179 ) ,
172180 Command :: Self_ { command } => command. exec ( self . common . output ) ,
181+ Command :: Debug { command } => command. exec ( self . common . output ) ,
173182 }
174183 }
175184}
@@ -378,6 +387,15 @@ enum Command {
378387 #[ clap( subcommand) ]
379388 command : SelfCommand ,
380389 } ,
390+ /// Debug commands
391+ ///
392+ /// The commands in this section are for nextest's own developers and those integrating with it
393+ /// to debug issues. They are not part of the public API and may change at any time.
394+ #[ clap( hide = true ) ]
395+ Debug {
396+ #[ clap( subcommand) ]
397+ command : DebugCommand ,
398+ } ,
381399}
382400
383401#[ derive( Debug , Args ) ]
@@ -1971,6 +1989,172 @@ impl SelfCommand {
19711989 }
19721990}
19731991
1992+ #[ derive( Debug , Subcommand ) ]
1993+ enum DebugCommand {
1994+ /// Show the data that nextest would extract from standard output or standard error.
1995+ ///
1996+ /// Text extraction is a heuristic process driven by a bunch of regexes and other similar logic.
1997+ /// This command shows what nextest would extract from a given input.
1998+ Extract {
1999+ /// The path to the standard output produced by the test process.
2000+ #[ arg( long, required_unless_present_any = [ "stderr" , "combined" ] ) ]
2001+ stdout : Option < Utf8PathBuf > ,
2002+
2003+ /// The path to the standard error produced by the test process.
2004+ #[ arg( long, required_unless_present_any = [ "stdout" , "combined" ] ) ]
2005+ stderr : Option < Utf8PathBuf > ,
2006+
2007+ /// The combined output produced by the test process.
2008+ #[ arg( long, conflicts_with_all = [ "stdout" , "stderr" ] ) ]
2009+ combined : Option < Utf8PathBuf > ,
2010+
2011+ /// The kind of output to produce.
2012+ #[ arg( value_enum) ]
2013+ output_format : ExtractOutputFormat ,
2014+ } ,
2015+ }
2016+
2017+ impl DebugCommand {
2018+ fn exec ( self , output : OutputOpts ) -> Result < i32 > {
2019+ let _ = output. init ( ) ;
2020+
2021+ match self {
2022+ DebugCommand :: Extract {
2023+ stdout,
2024+ stderr,
2025+ combined,
2026+ output_format,
2027+ } => {
2028+ // Either stdout + stderr or combined must be present.
2029+ if let Some ( combined) = combined {
2030+ let combined = std:: fs:: read ( & combined) . map_err ( |err| {
2031+ ExpectedError :: DebugExtractReadError {
2032+ kind : "combined" ,
2033+ path : combined,
2034+ err,
2035+ }
2036+ } ) ?;
2037+
2038+ let description_kind = extract_description ( & combined, & combined) ;
2039+ display_description_kind ( description_kind, output_format) ?;
2040+ } else {
2041+ let stdout = stdout
2042+ . map ( |path| {
2043+ std:: fs:: read ( & path) . map_err ( |err| {
2044+ ExpectedError :: DebugExtractReadError {
2045+ kind : "stdout" ,
2046+ path,
2047+ err,
2048+ }
2049+ } )
2050+ } )
2051+ . transpose ( ) ?
2052+ . unwrap_or_default ( ) ;
2053+ let stderr = stderr
2054+ . map ( |path| {
2055+ std:: fs:: read ( & path) . map_err ( |err| {
2056+ ExpectedError :: DebugExtractReadError {
2057+ kind : "stderr" ,
2058+ path,
2059+ err,
2060+ }
2061+ } )
2062+ } )
2063+ . transpose ( ) ?
2064+ . unwrap_or_default ( ) ;
2065+
2066+ let description_kind = extract_description ( & stdout, & stderr) ;
2067+ display_description_kind ( description_kind, output_format) ?;
2068+ }
2069+ }
2070+ }
2071+
2072+ Ok ( 0 )
2073+ }
2074+ }
2075+
2076+ fn extract_description < ' a > ( stdout : & ' a [ u8 ] , stderr : & ' a [ u8 ] ) -> Option < DescriptionKind < ' a > > {
2077+ // The execution result is a generic one.
2078+ heuristic_extract_description (
2079+ ExecutionResult :: Fail {
2080+ abort_status : None ,
2081+ leaked : false ,
2082+ } ,
2083+ stdout,
2084+ stderr,
2085+ )
2086+ }
2087+
2088+ fn display_description_kind (
2089+ kind : Option < DescriptionKind < ' _ > > ,
2090+ output_format : ExtractOutputFormat ,
2091+ ) -> Result < ( ) > {
2092+ match output_format {
2093+ ExtractOutputFormat :: Raw => {
2094+ if let Some ( kind) = kind {
2095+ if let Some ( out) = kind. combined_subslice ( ) {
2096+ return std:: io:: stdout ( ) . write_all ( out. slice ) . map_err ( |err| {
2097+ ExpectedError :: DebugExtractWriteError {
2098+ format : output_format,
2099+ err,
2100+ }
2101+ } ) ;
2102+ }
2103+ }
2104+ }
2105+ ExtractOutputFormat :: JunitDescription => {
2106+ if let Some ( kind) = kind {
2107+ println ! (
2108+ "{}" ,
2109+ XmlString :: new( kind. display_human( ) . to_string( ) ) . as_str( )
2110+ ) ;
2111+ }
2112+ }
2113+ ExtractOutputFormat :: Highlight => {
2114+ if let Some ( kind) = kind {
2115+ if let Some ( out) = kind. combined_subslice ( ) {
2116+ let end = highlight_end ( out. slice ) ;
2117+ return std:: io:: stdout ( )
2118+ . write_all ( & out. slice [ ..end] )
2119+ . map_err ( |err| ExpectedError :: DebugExtractWriteError {
2120+ format : output_format,
2121+ err,
2122+ } ) ;
2123+ }
2124+ }
2125+ }
2126+ }
2127+
2128+ eprintln ! ( "(no description found)" ) ;
2129+ Ok ( ( ) )
2130+ }
2131+
2132+ /// Output format for `nextest debug extract`.
2133+ #[ derive( Clone , Copy , Debug , ValueEnum ) ]
2134+ pub enum ExtractOutputFormat {
2135+ /// Show the raw text extracted.
2136+ Raw ,
2137+
2138+ /// Show what would be put in the description field of JUnit reports.
2139+ ///
2140+ /// This is similar to `Raw`, but is valid Unicode, and strips out ANSI escape codes and other
2141+ /// invalid XML characters.
2142+ JunitDescription ,
2143+
2144+ /// Show what would be highlighted in nextest's output.
2145+ Highlight ,
2146+ }
2147+
2148+ impl fmt:: Display for ExtractOutputFormat {
2149+ fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
2150+ match self {
2151+ Self :: Raw => write ! ( f, "raw" ) ,
2152+ Self :: JunitDescription => write ! ( f, "junit-description" ) ,
2153+ Self :: Highlight => write ! ( f, "highlight" ) ,
2154+ }
2155+ }
2156+ }
2157+
19742158fn acquire_graph_data (
19752159 manifest_path : Option < & Utf8Path > ,
19762160 target_dir : Option < & Utf8Path > ,
0 commit comments