diff --git a/.github/workflows/system_tests.yaml b/.github/workflows/system_tests.yaml index 606b67b8..43c77e75 100644 --- a/.github/workflows/system_tests.yaml +++ b/.github/workflows/system_tests.yaml @@ -5,55 +5,7 @@ on: workflow_call: {} jobs: - windows: - runs-on: windows-latest - steps: - - uses: actions/checkout@v4 - - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - target: x86_64-pc-windows-gnu - # By default, setup-rust-toolchain sets "-D warnings". As a side effect, the settings in - # .cargo/config.toml are ignored: - # https://doc.rust-lang.org/cargo/reference/config.html#buildrustflags - # "There are four mutually exclusive sources of extra flags" - rustflags: "" - - uses: actions/setup-python@v5 - with: - python-version: '3.12' - cache: 'pip' - - run: pip install -r tests/minimal_suite/requirements.txt - - uses: actions/download-artifact@v4 - with: - name: rcc - path: C:\ - - # MSVC uses vctip.exe for telemetry. vctip.exe is started as a child of termination.exe. This - # can cause CI failures, if "vctip.exe" does not terminate before `get_children` is called. - # It is unclear why MSVC is running, despite target=x86_64-pc-windows-gnu. The following - # command is intended turn off telemetry via vctip.exe. - - shell: pwsh - run: Get-ChildItem -Filter vctip.exe -Recurse "C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\VC\\Tools\\MSVC" | rm - - run: cargo test --target=x86_64-pc-windows-gnu --test test_plan_run --test test_agent_plugin -- --ignored - - run: cargo run --example termination --target=x86_64-pc-windows-gnu - - run: cargo run --example termination --target=x86_64-pc-windows-gnu -- C:\windows64\rcc.exe - - - run: mkdir C:\managed_robots - - run: tar --create -z --directory tests\minimal_suite\ --file C:\managed_robots\minimal_suite.tar.gz * - - run: net user "test_user" "uCjV*NRE#XH2a" /add - - run: cargo test --target=x86_64-pc-windows-gnu --test test_scheduler -- --nocapture --ignored - env: - TEST_DIR: C:\test_scheduler - RCC_BINARY_PATH: C:\windows64\rcc.exe - MANAGED_ROBOT_ARCHIVE_PATH: C:\managed_robots\minimal_suite.tar.gz - N_SECONDS_RUN_MAX: 300 - TEST_USER: test_user - - uses: actions/upload-artifact@v4 - if: success() || failure() - with: - name: system_test_debug_information_windows - path: C:\test_scheduler - - linux: + test_import_hololib: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -65,11 +17,6 @@ jobs: # https://doc.rust-lang.org/cargo/reference/config.html#buildrustflags # "There are four mutually exclusive sources of extra flags" rustflags: "" - - uses: actions/setup-python@v5 - with: - python-version: '3.12' - cache: 'pip' - - run: pip install -r tests/minimal_suite/requirements.txt - uses: actions/download-artifact@v4 with: name: rcc @@ -77,21 +24,14 @@ jobs: # file permissions are not retained during upload: # https://github.com/actions/upload-artifact?tab=readme-ov-file#permission-loss - run: chmod +x /tmp/linux64/rcc - - - run: cargo test --target=x86_64-unknown-linux-gnu --test test_plan_run --test test_agent_plugin -- --ignored - - run: cargo run --example termination --target=x86_64-unknown-linux-gnu - - run: cargo run --example termination --target=x86_64-unknown-linux-gnu -- /tmp/linux64/rcc - - - run: mkdir /tmp/managed_robots - - run: tar --create --gzip --directory tests/minimal_suite/ --file /tmp/managed_robots/minimal_suite.tar.gz . - - run: cargo test --target=x86_64-unknown-linux-gnu --test test_scheduler -- --nocapture --ignored + - run: echo "CARGO_MANIFEST_DIR=$(pwd)" >> $GITHUB_ENV + - run: cargo test --target=x86_64-unknown-linux-gnu --test test_ht_import_scheduler -- --nocapture --ignored env: TEST_DIR: /tmp/test_scheduler RCC_BINARY_PATH: /tmp/linux64/rcc - MANAGED_ROBOT_ARCHIVE_PATH: /tmp/managed_robots/minimal_suite.tar.gz - N_SECONDS_RUN_MAX: 300 - - uses: actions/upload-artifact@v4 - if: success() || failure() - with: - name: system_test_debug_information_linux - path: /tmp/test_scheduler + N_SECONDS_RUN_MAX: 120 + - uses: actions/upload-artifact@v4 + if: success() || failure() + with: + name: system_test_debug_information_linux + path: /tmp/test_scheduler diff --git a/src/bin/scheduler/main.rs b/src/bin/scheduler/main.rs index 0849621f..426a9e5f 100644 --- a/src/bin/scheduler/main.rs +++ b/src/bin/scheduler/main.rs @@ -32,6 +32,7 @@ fn main() -> AnyhowResult<()> { } fn run() -> Result<(), Terminate> { + println!("{:?}", std::env::args()); let args = cli::Args::parse(); logging::init(args.log_specification(), args.log_path).context("Logging setup failed.")?; info!("Program started and logging set up"); diff --git a/tests/test_ht_import_scheduler.rs b/tests/test_ht_import_scheduler.rs new file mode 100644 index 00000000..1f6e8270 --- /dev/null +++ b/tests/test_ht_import_scheduler.rs @@ -0,0 +1,254 @@ +#![cfg(unix)] +pub mod rcc; +use anyhow::{bail, Result as AnyhowResult}; +use assert_cmd::cargo::cargo_bin; +use camino::{Utf8Path, Utf8PathBuf}; +use robotmk::config::{ + Config, EnvironmentConfig, ExecutionConfig, PlanConfig, PlanMetadata, RCCConfig, + RCCEnvironmentConfig, RCCProfileConfig, RetryStrategy, RobotConfig, SequentialPlanGroup, + SessionConfig, Source, WorkingDirectoryCleanupConfig, +}; +use robotmk::section::Host; +use serde_json::to_string; +use std::env::var; +use std::fs::{create_dir_all, remove_file, write}; +use std::path::Path; +use std::time::Duration; +use tokio::{ + process::Command, + select, + time::{sleep, timeout}, +}; +use walkdir::WalkDir; + +#[tokio::test] +#[ignore] +async fn test_ht_import_scheduler() -> AnyhowResult<()> { + let test_dir = Utf8PathBuf::from(var("TEST_DIR")?); + let rcc_binary = Utf8PathBuf::from(var("RCC_BINARY_PATH")?); + let suite_dir = Utf8PathBuf::from(var("CARGO_MANIFEST_DIR")?) + .join("tests") + .join("minimal_suite"); + create_dir_all(&test_dir)?; + + let mut rcc_task_script = Command::new(rcc_binary.clone()); + rcc_task_script + .arg("task") + .arg("script") + .arg("--robot") + .arg(suite_dir.join("robot.yaml")) + .arg("--") + .arg("true"); + rcc_task_script.status().await?; + + let mut rcc_ht_export = Command::new(rcc_binary.clone()); + rcc_ht_export + .arg("holotree") + .arg("export") + .arg("--robot") + .arg(suite_dir.join("robot.yaml")) + .arg("--zipfile") + .arg(test_dir.join("hololib.zip")); + rcc_ht_export.status().await?; + + let mut rcc_cleanup = Command::new(rcc_binary.clone()); + rcc_cleanup.arg("configuration").arg("cleanup").arg("--all"); + rcc_cleanup.status().await?; + + let config = create_config( + &test_dir, + &suite_dir, + RCCConfig { + binary_path: rcc_binary, + profile_config: RCCProfileConfig::Default, + }, + ); + + run_scheduler( + &test_dir, + &config, + var("N_SECONDS_RUN_MAX")?.parse::()?, + ) + .await?; + + assert_working_directory(&config.working_directory).await?; + assert_results_directory(&config.results_directory); + Ok(()) +} + +fn create_config(test_dir: &Utf8Path, suite_dir: &Utf8Path, rcc_config: RCCConfig) -> Config { + Config { + working_directory: test_dir.join("working"), + results_directory: test_dir.join("results"), + managed_directory: test_dir.join("managed_robots"), + rcc_config, + plan_groups: vec![SequentialPlanGroup { + plans: vec![PlanConfig { + id: "rcc_headless".into(), + source: Source::Manual { + base_dir: suite_dir.into(), + }, + robot_config: RobotConfig { + robot_target: "tasks.robot".into(), + top_level_suite_name: None, + suites: vec![], + tests: vec![], + test_tags_include: vec![], + test_tags_exclude: vec![], + variables: vec![], + variable_files: vec![], + argument_files: vec![], + exit_on_failure: false, + }, + execution_config: ExecutionConfig { + n_attempts_max: 1, + retry_strategy: RetryStrategy::Complete, + timeout: 10, + }, + environment_config: EnvironmentConfig::Rcc(RCCEnvironmentConfig { + robot_yaml_path: "robot.yaml".into(), + build_timeout: 1200, + remote_origin: None, + catalog_zip: Some(test_dir.join("hololib.zip")), + }), + session_config: SessionConfig::Current, + working_directory_cleanup_config: WorkingDirectoryCleanupConfig::MaxExecutions(4), + host: Host::Source, + metadata: PlanMetadata { + application: "app".into(), + suite_name: "minimal_suite".into(), + variant: "".into(), + }, + }], + execution_interval: 30, + }], + } +} + +async fn run_scheduler( + test_dir: &Utf8Path, + config: &Config, + n_seconds_run_max: u64, +) -> AnyhowResult<()> { + let config_path = test_dir.join("config.json"); + write(&config_path, to_string(&config)?)?; + let run_flag_path = test_dir.join("run_flag"); + write(&run_flag_path, "")?; + + let mut robotmk_no_env_cmd = Command::new("sudo"); + robotmk_no_env_cmd + .arg("unshare") + .arg("--net") + .arg("--") + .arg(cargo_bin("robotmk_scheduler")) + .arg("-vv") + .arg("--run-flag") + .arg(&run_flag_path) + .arg(config_path); + let mut robotmk_child_proc = robotmk_no_env_cmd.spawn()?; + + select! { + _ = await_plan_results(config) => {}, + _ = robotmk_child_proc.wait() => { + bail!("Scheduler terminated unexpectedly") + }, + _ = sleep(Duration::from_secs(n_seconds_run_max)) => { + if let Err(e) = remove_file(&run_flag_path) { + eprintln!("Removing run file failed: {e}"); + } + bail!(format!("No plan result files appeared with {n_seconds_run_max} seconds")) + }, + }; + remove_file(&run_flag_path)?; + assert!(timeout(Duration::from_secs(3), robotmk_child_proc.wait()) + .await + .is_ok()); + + Ok(()) +} + +async fn await_plan_results(config: &Config) { + let expected_result_files: Vec = config + .plan_groups + .iter() + .flat_map(|plan_group| { + plan_group.plans.iter().map(|plan_config| { + config + .results_directory + .join("plans") + .join(format!("{}.json", &plan_config.id)) + }) + }) + .collect(); + loop { + if expected_result_files + .iter() + .all(|expected_result_file| expected_result_file.is_file()) + { + break; + } + sleep(Duration::from_secs(5)).await; + } +} + +async fn assert_working_directory(working_directory: &Utf8Path) -> AnyhowResult<()> { + assert!(working_directory.is_dir()); + assert_eq!( + directory_entries(working_directory, 1), + ["environment_building", "plans", "rcc_setup"] + ); + assert_eq!( + directory_entries(working_directory.join("environment_building"), 2), + [ + "current_user", + "current_user/rcc_headless.stderr", + "current_user/rcc_headless.stdout", + ] + ); + assert_eq!( + directory_entries(working_directory.join("plans"), 1), + ["rcc_headless"] + ); + + let entries_rcc_headless = + directory_entries(working_directory.join("plans").join("rcc_headless"), 2).join(""); + assert!(entries_rcc_headless.contains("rebot.xml")); + assert!(!entries_rcc_headless.contains("1.bat")); + + Ok(()) +} + +fn assert_results_directory(results_directory: &Utf8Path) { + assert!(results_directory.is_dir()); + assert_eq!( + directory_entries(results_directory, 2), + [ + "environment_build_states.json", + "plans", + "plans/rcc_headless.json", + "scheduler_phase.json", + "setup_failures.json" + ] + ); +} + +fn directory_entries(directory: impl AsRef, max_depth: usize) -> Vec { + WalkDir::new(&directory) + .max_depth(max_depth) + .sort_by_file_name() + .into_iter() + .map(|dir_entry_result| { + dir_entry_result + .unwrap() + .path() + .strip_prefix(&directory) + .unwrap() + .to_str() + .unwrap() + .into() + }) + .filter(|entry: &String| !entry.is_empty()) + // align unix and windows + .map(|s| s.replace("\\", "/")) + .collect() +}