2
2
// License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
5
- use std:: sync:: Arc ;
5
+ use std:: sync:: { Arc , Mutex } ;
6
6
use std:: time:: { Duration , Instant } ;
7
7
8
8
use phd_tests:: phd_testcase:: { Framework , TestCase , TestOutcome } ;
@@ -37,21 +37,9 @@ pub struct ExecutionStats {
37
37
pub failed_test_cases : Vec < & ' static TestCase > ,
38
38
}
39
39
40
- #[ derive( Debug , PartialEq , Eq , PartialOrd , Ord ) ]
41
- enum Status {
42
- Ran ( TestOutcome ) ,
43
- NotRun ,
44
- }
45
-
46
- struct Execution {
47
- tc : & ' static TestCase ,
48
- status : Status ,
49
- }
50
-
51
40
/// Executes a set of tests using the supplied test context.
52
41
pub async fn run_tests_with_ctx (
53
- ctx : & Arc < Framework > ,
54
- mut fixtures : TestFixtures ,
42
+ ctx : & mut Vec < ( Arc < Framework > , TestFixtures ) > ,
55
43
run_opts : & RunOptions ,
56
44
) -> ExecutionStats {
57
45
let mut executions = Vec :: new ( ) ;
@@ -60,10 +48,10 @@ pub async fn run_tests_with_ctx(
60
48
& run_opts. include_filter ,
61
49
& run_opts. exclude_filter ,
62
50
) {
63
- executions. push ( Execution { tc , status : Status :: NotRun } ) ;
51
+ executions. push ( tc ) ;
64
52
}
65
53
66
- let mut stats = ExecutionStats {
54
+ let stats = ExecutionStats {
67
55
tests_passed : 0 ,
68
56
tests_failed : 0 ,
69
57
tests_skipped : 0 ,
@@ -77,90 +65,128 @@ pub async fn run_tests_with_ctx(
77
65
return stats;
78
66
}
79
67
80
- fixtures. execution_setup ( ) . unwrap ( ) ;
81
- let sigint_rx = set_sigint_handler ( ) ;
82
- info ! ( "Running {} test(s)" , executions. len( ) ) ;
83
- let start_time = Instant :: now ( ) ;
84
- for execution in & mut executions {
85
- if * sigint_rx. borrow ( ) {
86
- info ! ( "Test run interrupted by SIGINT" ) ;
87
- break ;
88
- }
68
+ let stats = Arc :: new ( Mutex :: new ( stats) ) ;
89
69
90
- info ! ( "Starting test {}" , execution. tc. fully_qualified_name( ) ) ;
70
+ async fn run_tests (
71
+ execution_rx : crossbeam_channel:: Receiver < & ' static TestCase > ,
72
+ test_ctx : Arc < Framework > ,
73
+ mut fixtures : TestFixtures ,
74
+ stats : Arc < Mutex < ExecutionStats > > ,
75
+ sigint_rx : watch:: Receiver < bool > ,
76
+ ) -> Result < ( ) , ( ) > {
77
+ fixtures. execution_setup ( ) . unwrap ( ) ;
91
78
92
- // Failure to run a setup fixture is fatal to the rest of the run, but
93
- // it's still possible to report results, so return gracefully instead
94
- // of panicking.
95
- if let Err ( e) = fixtures. test_setup ( ) {
96
- error ! ( "Error running test setup fixture: {}" , e) ;
97
- break ;
98
- }
79
+ loop {
80
+ // Check for SIGINT only at the top of the loop because while
81
+ // waiting for a new testcase is theoretically a blocking
82
+ // operation, it won't be in a meaningful way for our use. The
83
+ // recv() will return immediately because either there are more
84
+ // testcases to run or the sender is closed. The only long
85
+ // blocking operation to check against in this loop is the test
86
+ // run itself.
87
+ if * sigint_rx. borrow ( ) {
88
+ info ! ( "Test run interrupted by SIGINT" ) ;
89
+ break ;
90
+ }
99
91
100
- stats. tests_not_run -= 1 ;
101
- let test_ctx = ctx. clone ( ) ;
102
- let tc = execution. tc ;
103
- let mut sigint_rx_task = sigint_rx. clone ( ) ;
104
- let test_outcome = tokio:: spawn ( async move {
105
- tokio:: select! {
106
- // Ensure interrupt signals are always handled instead of
107
- // continuing to run the test.
108
- biased;
109
- result = sigint_rx_task. changed( ) => {
110
- assert!(
111
- result. is_ok( ) ,
112
- "SIGINT channel shouldn't drop while tests are running"
113
- ) ;
114
-
115
- TestOutcome :: Failed (
116
- Some ( "test interrupted by SIGINT" . to_string( ) )
117
- )
92
+ let tc = match execution_rx. recv ( ) {
93
+ Ok ( tc) => tc,
94
+ Err ( _) => {
95
+ // RecvError means the channel is closed, so we're all
96
+ // done.
97
+ break ;
118
98
}
119
- outcome = tc. run( test_ctx. as_ref( ) ) => outcome
99
+ } ;
100
+
101
+ info ! ( "Starting test {}" , tc. fully_qualified_name( ) ) ;
102
+
103
+ // Failure to run a setup fixture is fatal to the rest of the
104
+ // run, but it's still possible to report results, so return
105
+ // gracefully instead of panicking.
106
+ if let Err ( e) = fixtures. test_setup ( ) {
107
+ error ! ( "Error running test setup fixture: {}" , e) ;
108
+ // TODO: set this on stats too
109
+ break ;
120
110
}
121
- } )
122
- . await
123
- . unwrap_or_else ( |_| {
124
- TestOutcome :: Failed ( Some (
125
- "test task panicked, see test logs" . to_string ( ) ,
126
- ) )
127
- } ) ;
128
-
129
- info ! (
130
- "test {} ... {}{}" ,
131
- execution. tc. fully_qualified_name( ) ,
132
- match test_outcome {
133
- TestOutcome :: Passed => "ok" ,
134
- TestOutcome :: Failed ( _) => "FAILED: " ,
135
- TestOutcome :: Skipped ( _) => "skipped: " ,
136
- } ,
137
- match & test_outcome {
138
- TestOutcome :: Failed ( Some ( s) )
139
- | TestOutcome :: Skipped ( Some ( s) ) => s,
140
- TestOutcome :: Failed ( None ) | TestOutcome :: Skipped ( None ) =>
141
- "[no message]" ,
142
- _ => "" ,
111
+
112
+ {
113
+ let mut stats = stats. lock ( ) . unwrap ( ) ;
114
+ stats. tests_not_run -= 1 ;
143
115
}
144
- ) ;
145
116
146
- match test_outcome {
147
- TestOutcome :: Passed => stats. tests_passed += 1 ,
148
- TestOutcome :: Failed ( _) => {
149
- stats. tests_failed += 1 ;
150
- stats. failed_test_cases . push ( execution. tc ) ;
117
+ let test_outcome = tc. run ( test_ctx. as_ref ( ) ) . await ;
118
+
119
+ info ! (
120
+ "test {} ... {}{}" ,
121
+ tc. fully_qualified_name( ) ,
122
+ match test_outcome {
123
+ TestOutcome :: Passed => "ok" ,
124
+ TestOutcome :: Failed ( _) => "FAILED: " ,
125
+ TestOutcome :: Skipped ( _) => "skipped: " ,
126
+ } ,
127
+ match & test_outcome {
128
+ TestOutcome :: Failed ( Some ( s) )
129
+ | TestOutcome :: Skipped ( Some ( s) ) => s,
130
+ TestOutcome :: Failed ( None ) | TestOutcome :: Skipped ( None ) =>
131
+ "[no message]" ,
132
+ _ => "" ,
133
+ }
134
+ ) ;
135
+
136
+ {
137
+ let mut stats = stats. lock ( ) . unwrap ( ) ;
138
+ match test_outcome {
139
+ TestOutcome :: Passed => stats. tests_passed += 1 ,
140
+ TestOutcome :: Failed ( _) => {
141
+ stats. tests_failed += 1 ;
142
+ stats. failed_test_cases . push ( tc) ;
143
+ }
144
+ TestOutcome :: Skipped ( _) => stats. tests_skipped += 1 ,
145
+ }
151
146
}
152
- TestOutcome :: Skipped ( _) => stats. tests_skipped += 1 ,
153
- }
154
147
155
- execution. status = Status :: Ran ( test_outcome) ;
156
- if let Err ( e) = fixtures. test_cleanup ( ) . await {
157
- error ! ( "Error running cleanup fixture: {}" , e) ;
158
- break ;
148
+ if let Err ( e) = fixtures. test_cleanup ( ) . await {
149
+ error ! ( "Error running cleanup fixture: {}" , e) ;
150
+ // TODO: set this on stats
151
+ break ;
152
+ }
159
153
}
154
+
155
+ fixtures. execution_cleanup ( ) . unwrap ( ) ;
156
+
157
+ Ok ( ( ) )
160
158
}
161
- stats. duration = start_time. elapsed ( ) ;
162
159
163
- fixtures. execution_cleanup ( ) . unwrap ( ) ;
160
+ let sigint_rx = set_sigint_handler ( ) ;
161
+ info ! ( "Running {} test(s)" , executions. len( ) ) ;
162
+ let start_time = Instant :: now ( ) ;
163
+
164
+ let ( execution_tx, execution_rx) =
165
+ crossbeam_channel:: unbounded :: < & ' static TestCase > ( ) ;
166
+
167
+ let mut test_runners = tokio:: task:: JoinSet :: new ( ) ;
168
+
169
+ for ( ctx, fixtures) in ctx. drain ( ..) {
170
+ test_runners. spawn ( run_tests (
171
+ execution_rx. clone ( ) ,
172
+ ctx,
173
+ fixtures,
174
+ Arc :: clone ( & stats) ,
175
+ sigint_rx. clone ( ) ,
176
+ ) ) ;
177
+ }
178
+
179
+ for execution in & mut executions {
180
+ execution_tx. send ( execution) . expect ( "ok" ) ;
181
+ }
182
+ std:: mem:: drop ( execution_tx) ;
183
+
184
+ let _ = test_runners. join_all ( ) . await ;
185
+
186
+ let mut stats =
187
+ Mutex :: into_inner ( Arc :: into_inner ( stats) . expect ( "only one ref" ) )
188
+ . expect ( "lock not panicked" ) ;
189
+ stats. duration = start_time. elapsed ( ) ;
164
190
165
191
stats
166
192
}
0 commit comments