compiletest: Add an experimental new executor to replace libtest · rust-lang/rust@e3d6813 (original) (raw)

1

``

`-

//! This module encapsulates all of the code that interacts directly with

`

2

``

`-

//! libtest, to execute the collected tests.

`

3

``

`-

//!

`

4

``

`-

//! This will hopefully make it easier to migrate away from libtest someday.

`

``

1

`+

//! This module contains a reimplementation of the subset of libtest

`

``

2

`+

//! functionality needed by compiletest.

`

5

3

``

6

4

`use std::borrow::Cow;

`

7

``

`-

use std::sync::Arc;

`

``

5

`+

use std::collections::HashMap;

`

``

6

`+

use std::hash::{BuildHasherDefault, DefaultHasher};

`

``

7

`+

use std::num::NonZero;

`

``

8

`+

use std::sync::{Arc, Mutex, mpsc};

`

``

9

`+

use std::{env, hint, io, mem, panic, thread};

`

8

10

``

9

11

`use crate::common::{Config, TestPaths};

`

10

12

``

``

13

`+

mod deadline;

`

``

14

`+

mod json;

`

11

15

`pub(crate) mod libtest;

`

12

16

``

``

17

`+

pub(crate) fn run_tests(config: &Config, tests: Vec) -> bool {

`

``

18

`+

let tests_len = tests.len();

`

``

19

`+

let filtered = filter_tests(config, tests);

`

``

20

`+

// Iterator yielding tests that haven't been started yet.

`

``

21

`+

let mut fresh_tests = (0..).map(TestId).zip(&filtered);

`

``

22

+

``

23

`+

let concurrency = get_concurrency();

`

``

24

`+

assert!(concurrency > 0);

`

``

25

`+

let concurrent_capacity = concurrency.min(filtered.len());

`

``

26

+

``

27

`+

let mut listener = json::Listener::new();

`

``

28

`+

let mut running_tests = HashMap::with_capacity_and_hasher(

`

``

29

`+

concurrent_capacity,

`

``

30

`+

BuildHasherDefault::::new(),

`

``

31

`+

);

`

``

32

`+

let mut deadline_queue = deadline::DeadlineQueue::with_capacity(concurrent_capacity);

`

``

33

+

``

34

`+

let num_filtered_out = tests_len - filtered.len();

`

``

35

`+

listener.suite_started(filtered.len(), num_filtered_out);

`

``

36

+

``

37

`+

// Channel used by test threads to report the test outcome when done.

`

``

38

`+

let (completion_tx, completion_rx) = mpsc::channel::();

`

``

39

+

``

40

`+

// Unlike libtest, we don't have a separate code path for concurrency=1.

`

``

41

`+

// In that case, the tests will effectively be run serially anyway.

`

``

42

`+

loop {

`

``

43

`+

// Spawn new test threads, up to the concurrency limit.

`

``

44

`+

// FIXME(let_chains): Use a let-chain here when stable in bootstrap.

`

``

45

`+

'spawn: while running_tests.len() < concurrency {

`

``

46

`+

let Some((id, test)) = fresh_tests.next() else { break 'spawn };

`

``

47

`+

listener.test_started(test);

`

``

48

`+

deadline_queue.push(id, test);

`

``

49

`+

let join_handle = spawn_test_thread(id, test, completion_tx.clone());

`

``

50

`+

running_tests.insert(id, RunningTest { test, join_handle });

`

``

51

`+

}

`

``

52

+

``

53

`+

// If all running tests have finished, and there weren't any unstarted

`

``

54

`+

// tests to spawn, then we're done.

`

``

55

`+

if running_tests.is_empty() {

`

``

56

`+

break;

`

``

57

`+

}

`

``

58

+

``

59

`+

let completion = deadline_queue

`

``

60

`+

.read_channel_while_checking_deadlines(&completion_rx, |_id, test| {

`

``

61

`+

listener.test_timed_out(test);

`

``

62

`+

})

`

``

63

`+

.expect("receive channel should never be closed early");

`

``

64

+

``

65

`+

let RunningTest { test, join_handle } = running_tests.remove(&completion.id).unwrap();

`

``

66

`+

if let Some(join_handle) = join_handle {

`

``

67

`+

join_handle.join().unwrap_or_else(|_| {

`

``

68

`` +

panic!("thread for {} panicked after reporting completion", test.desc.name)

``

``

69

`+

});

`

``

70

`+

}

`

``

71

+

``

72

`+

listener.test_finished(test, &completion);

`

``

73

+

``

74

`+

if completion.outcome.is_failed() && config.fail_fast {

`

``

75

`+

// Prevent any other in-flight threads from panicking when they

`

``

76

`+

// write to the completion channel.

`

``

77

`+

mem::forget(completion_rx);

`

``

78

`+

break;

`

``

79

`+

}

`

``

80

`+

}

`

``

81

+

``

82

`+

let suite_passed = listener.suite_finished();

`

``

83

`+

suite_passed

`

``

84

`+

}

`

``

85

+

``

86

`+

/// Spawns a thread to run a single test, and returns the thread's join handle.

`

``

87

`+

///

`

``

88

`` +

/// Returns None if the test was ignored, so no thread was spawned.

``

``

89

`+

fn spawn_test_thread(

`

``

90

`+

id: TestId,

`

``

91

`+

test: &CollectedTest,

`

``

92

`+

completion_tx: mpsc::Sender,

`

``

93

`+

) -> Option<thread::JoinHandle<()>> {

`

``

94

`+

if test.desc.ignore && !test.config.run_ignored {

`

``

95

`+

completion_tx

`

``

96

`+

.send(TestCompletion { id, outcome: TestOutcome::Ignored, stdout: None })

`

``

97

`+

.unwrap();

`

``

98

`+

return None;

`

``

99

`+

}

`

``

100

+

``

101

`+

let runnable_test = RunnableTest::new(test);

`

``

102

`+

let should_panic = test.desc.should_panic;

`

``

103

`+

let run_test = move || run_test_inner(id, should_panic, runnable_test, completion_tx);

`

``

104

+

``

105

`+

let thread_builder = thread::Builder::new().name(test.desc.name.clone());

`

``

106

`+

let join_handle = thread_builder.spawn(run_test).unwrap();

`

``

107

`+

Some(join_handle)

`

``

108

`+

}

`

``

109

+

``

110

`+

/// Runs a single test, within the dedicated thread spawned by the caller.

`

``

111

`+

fn run_test_inner(

`

``

112

`+

id: TestId,

`

``

113

`+

should_panic: ShouldPanic,

`

``

114

`+

runnable_test: RunnableTest,

`

``

115

`+

completion_sender: mpsc::Sender,

`

``

116

`+

) {

`

``

117

`+

let is_capture = !runnable_test.config.nocapture;

`

``

118

`+

let capture_buf = is_capture.then(|| Arc::new(Mutex::new(vec![])));

`

``

119

+

``

120

`+

if let Some(capture_buf) = &capture_buf {

`

``

121

`+

io::set_output_capture(Some(Arc::clone(capture_buf)));

`

``

122

`+

}

`

``

123

+

``

124

`+

let panic_payload = panic::catch_unwind(move || runnable_test.run()).err();

`

``

125

+

``

126

`+

if is_capture {

`

``

127

`+

io::set_output_capture(None);

`

``

128

`+

}

`

``

129

+

``

130

`+

let outcome = match (should_panic, panic_payload) {

`

``

131

`+

(ShouldPanic::No, None) | (ShouldPanic::Yes, Some(_)) => TestOutcome::Succeeded,

`

``

132

`+

(ShouldPanic::No, Some(_)) => TestOutcome::Failed { message: None },

`

``

133

`+

(ShouldPanic::Yes, None) => {

`

``

134

`+

TestOutcome::Failed { message: Some("test did not panic as expected") }

`

``

135

`+

}

`

``

136

`+

};

`

``

137

`+

let stdout = capture_buf.map(|mutex| mutex.lock().unwrap_or_else(|e| e.into_inner()).to_vec());

`

``

138

+

``

139

`+

completion_sender.send(TestCompletion { id, outcome, stdout }).unwrap();

`

``

140

`+

}

`

``

141

+

``

142

`+

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]

`

``

143

`+

struct TestId(usize);

`

``

144

+

``

145

`+

struct RunnableTest {

`

``

146

`+

config: Arc,

`

``

147

`+

testpaths: TestPaths,

`

``

148

`+

revision: Option,

`

``

149

`+

}

`

``

150

+

``

151

`+

impl RunnableTest {

`

``

152

`+

fn new(test: &CollectedTest) -> Self {

`

``

153

`+

let config = Arc::clone(&test.config);

`

``

154

`+

let testpaths = test.testpaths.clone();

`

``

155

`+

let revision = test.revision.clone();

`

``

156

`+

Self { config, testpaths, revision }

`

``

157

`+

}

`

``

158

+

``

159

`+

fn run(&self) {

`

``

160

`+

__rust_begin_short_backtrace(|| {

`

``

161

`+

crate::runtest::run(

`

``

162

`+

Arc::clone(&self.config),

`

``

163

`+

&self.testpaths,

`

``

164

`+

self.revision.as_deref(),

`

``

165

`+

);

`

``

166

`+

});

`

``

167

`+

}

`

``

168

`+

}

`

``

169

+

``

170

`` +

/// Fixed frame used to clean the backtrace with RUST_BACKTRACE=1.

``

``

171

`+

#[inline(never)]

`

``

172

`+

fn __rust_begin_short_backtrace<T, F: FnOnce() -> T>(f: F) -> T {

`

``

173

`+

let result = f();

`

``

174

+

``

175

`+

// prevent this frame from being tail-call optimised away

`

``

176

`+

hint::black_box(result)

`

``

177

`+

}

`

``

178

+

``

179

`+

struct RunningTest<'a> {

`

``

180

`+

test: &'a CollectedTest,

`

``

181

`+

join_handle: Option<thread::JoinHandle<()>>,

`

``

182

`+

}

`

``

183

+

``

184

`+

/// Test completion message sent by individual test threads when their test

`

``

185

`+

/// finishes (successfully or unsuccessfully).

`

``

186

`+

struct TestCompletion {

`

``

187

`+

id: TestId,

`

``

188

`+

outcome: TestOutcome,

`

``

189

`+

stdout: Option<Vec>,

`

``

190

`+

}

`

``

191

+

``

192

`+

#[derive(Clone, Debug, PartialEq, Eq)]

`

``

193

`+

enum TestOutcome {

`

``

194

`+

Succeeded,

`

``

195

`+

Failed { message: Option<&'static str> },

`

``

196

`+

Ignored,

`

``

197

`+

}

`

``

198

+

``

199

`+

impl TestOutcome {

`

``

200

`+

fn is_failed(&self) -> bool {

`

``

201

`+

matches!(self, Self::Failed { .. })

`

``

202

`+

}

`

``

203

`+

}

`

``

204

+

``

205

`+

/// Applies command-line arguments for filtering/skipping tests by name.

`

``

206

`+

///

`

``

207

`` +

/// Adapted from filter_tests in libtest.

``

``

208

`+

///

`

``

209

`+

/// FIXME(#139660): After the libtest dependency is removed, redesign the whole

`

``

210

`+

/// filtering system to do a better job of understanding and filtering paths,

`

``

211

`+

/// instead of being tied to libtest's substring/exact matching behaviour.

`

``

212

`+

fn filter_tests(opts: &Config, tests: Vec) -> Vec {

`

``

213

`+

let mut filtered = tests;

`

``

214

+

``

215

`+

let matches_filter = |test: &CollectedTest, filter_str: &str| {

`

``

216

`+

let test_name = &test.desc.name;

`

``

217

`+

if opts.filter_exact { test_name == filter_str } else { test_name.contains(filter_str) }

`

``

218

`+

};

`

``

219

+

``

220

`+

// Remove tests that don't match the test filter

`

``

221

`+

if !opts.filters.is_empty() {

`

``

222

`+

filtered.retain(|test| opts.filters.iter().any(|filter| matches_filter(test, filter)));

`

``

223

`+

}

`

``

224

+

``

225

`+

// Skip tests that match any of the skip filters

`

``

226

`+

if !opts.skip.is_empty() {

`

``

227

`+

filtered.retain(|test| !opts.skip.iter().any(|sf| matches_filter(test, sf)));

`

``

228

`+

}

`

``

229

+

``

230

`+

filtered

`

``

231

`+

}

`

``

232

+

``

233

`+

/// Determines the number of tests to run concurrently.

`

``

234

`+

///

`

``

235

`` +

/// Copied from get_concurrency in libtest.

``

``

236

`+

///

`

``

237

`+

/// FIXME(#139660): After the libtest dependency is removed, consider making

`

``

238

`+

/// bootstrap specify the number of threads on the command-line, instead of

`

``

239

`` +

/// propagating the RUST_TEST_THREADS environment variable.

``

``

240

`+

fn get_concurrency() -> usize {

`

``

241

`+

if let Ok(value) = env::var("RUST_TEST_THREADS") {

`

``

242

`+

match value.parse::<NonZero>().ok() {

`

``

243

`+

Some(n) => n.get(),

`

``

244

`` +

_ => panic!("RUST_TEST_THREADS is {value}, should be a positive integer."),

``

``

245

`+

}

`

``

246

`+

} else {

`

``

247

`+

thread::available_parallelism().map(|n| n.get()).unwrap_or(1)

`

``

248

`+

}

`

``

249

`+

}

`

``

250

+

13

251

`` /// Information needed to create a test::TestDescAndFn.

``

14

252

`pub(crate) struct CollectedTest {

`

15

253

`pub(crate) desc: CollectedTestDesc,

`