Handle no-record cars and per-copy trash listing

Empty API responses (200 + empty body) mean "known car, no times
submitted yet" on the records site — typically newly released prize
cars. Previously each such car burned 3 retries with backoff and then
aborted the entire fetch; 27 cars in the current DB hit this.

- Add AppError::EmptyResponse { url } as a distinct, deterministic
  failure class; get_file fails immediately instead of retrying
- fetch_car downgrades EmptyResponse to a skip with an info log, so
  fetch completes; the next fetch picks the cars up automatically
  once records exist
- parse_car_times treats empty (or whitespace-only) timing files
  like missing files, eliminating "EOF while parsing a value at line
  1 column 0" parse errors from stale empty files

trash.csv Config column now lists only the copies to delete: for a
car whose one tune is a final build not best on any track (e.g.
owned 332 next to a best-on-213-tracks 323), only the dead tune is
shown instead of joining all owned configs. Categories 1 and 3 still
list all configs since the whole car goes.

Tests: empty response makes exactly 1 request and yields the
EmptyResponse variant; empty file parses Ok; partial-copy trash
shows only the deletable config.
This commit is contained in:
Jan-Peter von Hunnius 2026-08-22 07:24:22 +02:00
parent 293a8d7707
commit 7ac889cbff
6 changed files with 148 additions and 45 deletions

View file

@ -31,22 +31,23 @@ async fn test_get_file_success() {
}
#[tokio::test]
async fn test_get_file_empty_response_retries_and_fails() {
async fn test_get_file_empty_response_fails_immediately_without_retry() {
let mut server = Server::new_async().await;
// Return 200 with empty body — triggers retry, then fails
let m = server
.mock("GET", "/data.json")
.with_status(200)
.with_body("")
.expect(4) // 1 initial + 3 retries
.create();
// 200 with empty body means "no data available yet" — permanent, so exactly
// one request must be made (no retries) and a distinguishable error returned.
let m = server.mock("GET", "/data.json").with_status(200).with_body("").expect(1).create();
let dir = tempfile::tempdir().unwrap();
let out_path = dir.path().join("output.txt");
let url = format!("{}/data.json", server.url());
let result = get_file(&url, out_path.to_str().unwrap()).await;
assert!(result.is_err(), "Empty response should fail after retries");
let err = result.expect_err("Empty response should fail immediately");
assert!(
matches!(err, top_drives::AppError::EmptyResponse { .. }),
"Empty response should yield AppError::EmptyResponse, got: {err}"
);
assert!(!out_path.exists(), "No file should be written for an empty response");
m.assert();
}