testing: introduce util function to synchronize concurrent commands on files stable
authorRaphaël Gomès <rgomes@octobus.net>
Tue, 28 Feb 2023 00:01:41 +0100
branchstable
changeset 50214 8e0d823ef182
parent 50213 f5e4248e5bce
child 50215 ae61851e6fe2
testing: introduce util function to synchronize concurrent commands on files This is an extension of mechanisms that the tests have been using for a while. To be able to also control the execution in Rust, we introduce utility to perform such `wait_on_file` logic based on some configuration value. This will be used in the tests introduced in the next changesets.
mercurial/testing/__init__.py
rust/hg-core/src/utils.rs
rust/hg-core/src/utils/debug.rs
--- a/mercurial/testing/__init__.py	Tue Feb 28 00:04:32 2023 +0100
+++ b/mercurial/testing/__init__.py	Tue Feb 28 00:01:41 2023 +0100
@@ -9,6 +9,21 @@
 environ = getattr(os, 'environ')
 
 
+def wait_on_cfg(ui, cfg, timeout=10):
+    """synchronize on the `cfg` config path
+
+    Use this to synchronize commands during race tests.
+    """
+    full_config = b'sync.' + cfg
+    wait_config = full_config + b'-timeout'
+    sync_path = ui.config(b'devel', full_config)
+    if sync_path is not None:
+        timeout = ui.config(b'devel', wait_config)
+        ready_path = sync_path + b'.waiting'
+        write_file(ready_path)
+        wait_file(sync_path, timeout=timeout)
+
+
 def _timeout_factor():
     """return the current modification to timeout"""
     default = int(environ.get('HGTEST_TIMEOUT_DEFAULT', 360))
--- a/rust/hg-core/src/utils.rs	Tue Feb 28 00:04:32 2023 +0100
+++ b/rust/hg-core/src/utils.rs	Tue Feb 28 00:01:41 2023 +0100
@@ -15,6 +15,7 @@
 use std::fmt;
 use std::{io::Write, ops::Deref};
 
+pub mod debug;
 pub mod files;
 pub mod hg_path;
 pub mod path_auditor;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rust/hg-core/src/utils/debug.rs	Tue Feb 28 00:01:41 2023 +0100
@@ -0,0 +1,81 @@
+//! Utils for debugging hg-core
+
+use crate::config::Config;
+
+/// Write the file path given by the config option `devel.<config_option>` with
+/// the suffix `.waiting`, then wait for the file path given by the
+/// config option `devel.<config_option>` to appear on disk
+/// up to `devel.<config_option>-timeout` seconds.
+/// Note that the timeout may be higher because we scale it if global
+/// `run-tests` timeouts are raised to prevent flakiness on slower hardware.
+///
+/// Useful for testing race conditions.
+pub fn debug_wait_for_file(
+    config: &Config,
+    config_option: &str,
+) -> Result<(), String> {
+    let path_opt = format!("sync.{config_option}");
+    let file_path = match config.get_str(b"devel", path_opt.as_bytes()).ok() {
+        Some(Some(file_path)) => file_path,
+        _ => return Ok(()),
+    };
+
+    // TODO make it so `configitems` is shared between Rust and Python so that
+    // defaults work out of the box, etc.
+    let default_timeout = 2;
+    let timeout_opt = format!("sync.{config_option}-timeout");
+    let timeout_seconds =
+        match config.get_u32(b"devel", timeout_opt.as_bytes()) {
+            Ok(Some(timeout)) => timeout,
+            Err(e) => {
+                log::debug!("{e}");
+                default_timeout
+            }
+            _ => default_timeout,
+        };
+    let timeout_seconds = timeout_seconds as u64;
+
+    log::debug!(
+        "Config option `{config_option}` found, \
+             waiting for file `{file_path}` to be created"
+    );
+    std::fs::File::create(format!("{file_path}.waiting")).ok();
+    // If the test timeout have been extended, scale the timer relative
+    // to the normal timing.
+    let global_default_timeout: u64 = std::env::var("HGTEST_TIMEOUT_DEFAULT")
+        .map(|t| t.parse())
+        .unwrap_or(Ok(0))
+        .unwrap();
+    let global_timeout_override: u64 = std::env::var("HGTEST_TIMEOUT")
+        .map(|t| t.parse())
+        .unwrap_or(Ok(0))
+        .unwrap();
+    let timeout_seconds = if global_default_timeout < global_timeout_override {
+        timeout_seconds * global_timeout_override / global_default_timeout
+    } else {
+        timeout_seconds
+    };
+    let timeout = std::time::Duration::from_secs(timeout_seconds);
+
+    let start = std::time::Instant::now();
+    let path = std::path::Path::new(file_path);
+    let mut found = false;
+    while start.elapsed() < timeout {
+        if path.exists() {
+            log::debug!("File `{file_path}` was created");
+            found = true;
+            break;
+        } else {
+            std::thread::sleep(std::time::Duration::from_millis(10));
+        }
+    }
+    if !found {
+        let msg = format!(
+            "File `{file_path}` set by `{config_option}` was not found \
+            within the allocated {timeout_seconds} seconds timeout"
+        );
+        Err(msg)
+    } else {
+        Ok(())
+    }
+}