Skip to content

Commit 4837bb9

Browse files
authored
DUX-5115 Add startup failure error log test (#449)
#444 got auto-merged a bit early, oops! I can confirm that if you comment out the change in #444 then a bunch of stuff fails: ``` Summary [ 83.049s] 239 tests run: 227 passed (1 leaky), 12 failed, 0 skipped FAIL [ 36.712s] ghciwatch::error_log error_log_startup_failure_9102 FAIL [ 24.564s] ghciwatch::error_log error_log_startup_failure_9122 FAIL [ 22.095s] ghciwatch::error_log error_log_startup_failure_967 FAIL [ 33.764s] ghciwatch::error_log error_log_startup_failure_984 FAIL [ 22.941s] ghciwatch::shutdown handles_repeated_startup_failures_9102 FAIL [ 24.139s] ghciwatch::shutdown handles_repeated_startup_failures_9122 FAIL [ 25.440s] ghciwatch::shutdown handles_repeated_startup_failures_967 FAIL [ 27.936s] ghciwatch::shutdown handles_repeated_startup_failures_984 FAIL [ 27.663s] ghciwatch::shutdown handles_repeated_startup_failures_before_restart_ghci_hook_9102 FAIL [ 25.208s] ghciwatch::shutdown handles_repeated_startup_failures_before_restart_ghci_hook_9122 FAIL [ 22.660s] ghciwatch::shutdown handles_repeated_startup_failures_before_restart_ghci_hook_967 FAIL [ 31.145s] ghciwatch::shutdown handles_repeated_startup_failures_before_restart_ghci_hook_984 ```
1 parent d641be1 commit 4837bb9

2 files changed

Lines changed: 80 additions & 16 deletions

File tree

src/ghci/manager.rs

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -81,20 +81,7 @@ pub async fn run_ghci(
8181
startup_result = ghci.initialize(&mut log, [LifecycleEvent::Startup(hooks::When::After)]) => {
8282
// Only reachable if ghci starts successfully (startup_result = Ok) or if
8383
// initialization fails for a non-EOF reason (e.g., a lifecycle hook error).
84-
match startup_result {
85-
Ok(()) => {},
86-
Err(err) => {
87-
let is_broken_pipe = err.chain().any(|e| {
88-
e.downcast_ref::<std::io::Error>()
89-
.is_some_and(|io| io.kind() == std::io::ErrorKind::BrokenPipe)
90-
});
91-
if is_broken_pipe {
92-
tracing::debug!("ghci stdin closed during startup (broken pipe): {err}");
93-
} else {
94-
return Err(err);
95-
}
96-
}
97-
}
84+
handle_broken_pipe(startup_result)?;
9885
}
9986
};
10087
let startup_exit: Option<ExitStatus> = exited_receiver.try_recv().ok();
@@ -500,7 +487,7 @@ impl RestartStrategy<'_> {
500487
}
501488

502489
async fn restart(&mut self) -> eyre::Result<()> {
503-
match self {
490+
handle_broken_pipe(match self {
504491
Self::Startup(ghci) => ghci
505492
.startup_restart()
506493
.await
@@ -511,7 +498,7 @@ impl RestartStrategy<'_> {
511498
.startup_restart()
512499
.await
513500
.wrap_err("Failed to restart ghci after unexpected exit"),
514-
}
501+
})
515502
}
516503
}
517504

@@ -575,3 +562,21 @@ async fn wait_and_restart(
575562
}
576563
}
577564
}
565+
566+
fn handle_broken_pipe(result: eyre::Result<()>) -> eyre::Result<()> {
567+
match result {
568+
Ok(()) => Ok(()),
569+
Err(err) => {
570+
let is_broken_pipe = err.chain().any(|e| {
571+
e.downcast_ref::<std::io::Error>()
572+
.is_some_and(|io| io.kind() == std::io::ErrorKind::BrokenPipe)
573+
});
574+
if is_broken_pipe {
575+
tracing::debug!("ignoring broken pipe error: {err}");
576+
Ok(())
577+
} else {
578+
Err(err)
579+
}
580+
}
581+
}
582+
}

tests/error_log.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use indoc::indoc;
33

44
use test_harness::test;
55
use test_harness::BaseMatcher;
6+
use test_harness::Fs;
67
use test_harness::GhcVersion;
78
use test_harness::GhciWatchBuilder;
89

@@ -213,3 +214,61 @@ async fn can_adjust_error_log_paths() {
213214

214215
expected.assert_eq(&error_contents);
215216
}
217+
218+
#[test]
219+
async fn error_log_startup_failure() {
220+
let error_path = "ghcid.txt";
221+
let mut session = GhciWatchBuilder::new("tests/data/with-dep")
222+
.with_args(["--errors", error_path])
223+
.before_start(move |path| {
224+
// A version of SimpleDep.hs with an unclosed string literal — cabal will refuse to build it.
225+
async move {
226+
Fs::new()
227+
.replace(
228+
path.join("simple-dep/src/SimpleDep.hs"),
229+
"\"depFunc\"",
230+
"\"depFunc",
231+
)
232+
.await
233+
}
234+
})
235+
.start()
236+
.await
237+
.expect("ghciwatch starts");
238+
let error_path = session.path(error_path);
239+
240+
// First startup fails because simple-dep won't compile.
241+
// NB: This message means that ghciwatch didn't crash!
242+
session
243+
.wait_for_startup_log("ghci exited during startup")
244+
.await
245+
.expect("ghciwatch detects first startup failure");
246+
247+
let error_contents = session
248+
.fs()
249+
.read(&error_path)
250+
.await
251+
.expect("ghciwatch writes ghcid.txt");
252+
253+
// We don't have access to the package's directory here so we can't fix these paths!
254+
// These _should_ be like `simple-dep/src/SimpleDep.hs` but GHC doesn't emit them relative to
255+
// the invocation so users are just Fucked.
256+
let expected = match session.ghc_version() {
257+
GhcVersion::Ghc96 | GhcVersion::Ghc98 | GhcVersion::Ghc910 => expect![[r#"
258+
src/SimpleDep.hs:4:28: error: [GHC-21231]
259+
lexical error in string/character literal at character '\n'
260+
|
261+
4 | depFunc = putStrLn "depFunc
262+
| ^
263+
"#]],
264+
GhcVersion::Ghc912 => expect![[r#"
265+
src/SimpleDep.hs:4:20: error: [GHC-21231]
266+
lexical error at character '\n'
267+
|
268+
4 | depFunc = putStrLn "depFunc
269+
| ^^^^^^^^
270+
"#]],
271+
};
272+
273+
expected.assert_eq(&error_contents);
274+
}

0 commit comments

Comments
 (0)