max / makenotwork
2 files changed,
+51 insertions,
-3 deletions
| @@ -37,13 +37,26 @@ pub async fn prune_old_records( | |||
| 37 | 37 | .execute(pool) | |
| 38 | 38 | .await?; | |
| 39 | 39 | ||
| 40 | + | // test_details has ON DELETE CASCADE on run_id and the pool runs with | |
| 41 | + | // foreign_keys=ON, so deleting the runs below already removes their details. | |
| 42 | + | // Count them first: the orphan sweep afterwards can only ever report rows the | |
| 43 | + | // cascade missed, which is why PruneResult.test_details always read ~0. | |
| 44 | + | let cascaded_details = sqlx::query_as::<_, (i64,)>( | |
| 45 | + | "SELECT COUNT(*) FROM test_details | |
| 46 | + | WHERE run_id IN (SELECT id FROM test_runs WHERE started_at < ?)", | |
| 47 | + | ) | |
| 48 | + | .bind(&cutoff_str) | |
| 49 | + | .fetch_one(pool) | |
| 50 | + | .await?; | |
| 51 | + | ||
| 40 | 52 | let test_result = sqlx::query("DELETE FROM test_runs WHERE started_at < ?") | |
| 41 | 53 | .bind(&cutoff_str) | |
| 42 | 54 | .execute(pool) | |
| 43 | 55 | .await?; | |
| 44 | 56 | ||
| 45 | - | // Prune orphaned test_details (run was deleted above). | |
| 46 | - | let test_details_result = sqlx::query( | |
| 57 | + | // Safety net for details orphaned some other way (a database written while | |
| 58 | + | // foreign_keys was off). Normally zero. | |
| 59 | + | let orphan_result = sqlx::query( | |
| 47 | 60 | "DELETE FROM test_details WHERE run_id NOT IN (SELECT id FROM test_runs)", | |
| 48 | 61 | ) | |
| 49 | 62 | .execute(pool) | |
| @@ -92,7 +105,7 @@ pub async fn prune_old_records( | |||
| 92 | 105 | Ok(PruneResult { | |
| 93 | 106 | health: health_result.rows_affected(), | |
| 94 | 107 | tests: test_result.rows_affected(), | |
| 95 | - | test_details: test_details_result.rows_affected(), | |
| 108 | + | test_details: cascaded_details.0 as u64 + orphan_result.rows_affected(), | |
| 96 | 109 | heartbeats: peer_hb_result.rows_affected(), | |
| 97 | 110 | alerts: alerts_result.rows_affected(), | |
| 98 | 111 | tls: tls_result.rows_affected(), |
| @@ -1892,6 +1892,41 @@ async fn prune_with_days_one_keeps_today() { | |||
| 1892 | 1892 | assert_eq!(remaining[0].response_time_ms, 100); | |
| 1893 | 1893 | } | |
| 1894 | 1894 | ||
| 1895 | + | #[tokio::test] | |
| 1896 | + | async fn prune_counts_cascade_deleted_test_details() { | |
| 1897 | + | let pool = db::connect_in_memory().await.unwrap(); | |
| 1898 | + | ||
| 1899 | + | let run = TestRun { | |
| 1900 | + | id: None, | |
| 1901 | + | target: "mnw".to_string(), | |
| 1902 | + | started_at: (chrono::Utc::now() - chrono::Duration::days(10)).to_rfc3339(), | |
| 1903 | + | finished_at: None, | |
| 1904 | + | duration_secs: Some(120), | |
| 1905 | + | exit_code: Some(0), | |
| 1906 | + | passed: true, | |
| 1907 | + | summary: TestSummary { | |
| 1908 | + | steps: vec![], | |
| 1909 | + | total_passed: Some(3), | |
| 1910 | + | total_failed: Some(0), | |
| 1911 | + | details: vec![ | |
| 1912 | + | TestDetail { test_name: "foo::bar".to_string(), passed: true }, | |
| 1913 | + | TestDetail { test_name: "foo::baz".to_string(), passed: true }, | |
| 1914 | + | TestDetail { test_name: "foo::qux".to_string(), passed: true }, | |
| 1915 | + | ], | |
| 1916 | + | }, | |
| 1917 | + | raw_output: String::new(), | |
| 1918 | + | filter: None, | |
| 1919 | + | }; | |
| 1920 | + | let run_id = db::insert_test_run(&pool, &run).await.unwrap(); | |
| 1921 | + | db::insert_test_details(&pool, run_id, &run.summary.details).await.unwrap(); | |
| 1922 | + | ||
| 1923 | + | // The ON DELETE CASCADE removes the details along with the run; the count | |
| 1924 | + | // must still reflect them rather than reading 0 off an empty orphan sweep. | |
| 1925 | + | let result = db::prune_old_records(&pool, 7).await.unwrap(); | |
| 1926 | + | assert_eq!(result.tests, 1); | |
| 1927 | + | assert_eq!(result.test_details, 3); | |
| 1928 | + | } | |
| 1929 | + | ||
| 1895 | 1930 | // --- SSH timeout_secs config test --- | |
| 1896 | 1931 | ||
| 1897 | 1932 | #[test] |