Radish alpha
r
Radicle CI broker
Radicle
Git (anonymous pull)
Log in to clone via SSH
test: add higher level scenario steps
Lars Wirzenius committed 1 year ago
commit 964d3e1ed224a1efd21da67091e178984df2c53e
parent b4f379ad262b12392d349a56033efb3cc078efbd
4 files changed +452 -19
modified ci-broker.md
@@ -241,22 +241,13 @@ nothing else has a hope of working.
_Who:_ `cib-devs`

~~~scenario
-
given an installed CI broker
-
given a CI adapter adapter.sh from dummy.sh
-

-
given file radenv.sh
-
given file setup-node.sh
-
when I run bash radenv.sh bash setup-node.sh
-

-
given file refsfetched.json
-
given file set-rid
-
when I run bash radenv.sh env HOME=../homedir python3 set-rid refsfetched.json testy
-
when I run synthetic-events synt.sock refsfetched.json --log log.txt
-

+
given a Radicle node, with CI configured with broker.yaml and adapter dummy.sh
+
given a Git repository xyzzy in the Radicle node
+
given the Radicle node emits a refsUpdated event for xyzzy
+
when I run ./env.sh synthetic-events synt.sock event.json --log log.txt
given a directory reports
-
given file broker.yaml
+
when I run ./env.sh cib --config broker.yaml process-events

-
when I run bash radenv.sh RAD_SOCKET=synt.sock cib --config broker.yaml process-events
then stderr contains "CI broker starts"
then stderr contains "loaded configuration"
then stderr contains "CI broker ends successfully"
modified ci-broker.yaml
@@ -1,13 +1,50 @@
-
- given: "an installed CI broker"
+
- given: "a Radicle node, with CI configured with {config} and adapter {adapter}"
+
  types:
+
    config: file
+
    adapter: file
  impl:
    rust:
-
      function: install_ci_broker
+
      function: setup_node
+

+
- given: "a Git repository {name} in the Radicle node"
+
  types:
+
    name: word
+
  impl:
+
    rust:
+
      function: create_repo
+

+
- given: "the Radicle node emits a refsUpdated event for {repodir}"
+
  types:
+
    repodir: path
+
  impl:
+
    rust:
+
      function: add_event_file
+

+
- given: "a running Radicle node"
+
  impl:
+
    rust:
+
      function: start_node

- given: "a CI adapter {filename:path} from {embedded:file}"
  impl:
    rust:
      function: install_adapter

+
- when: "I run, with the Radicle node, in {dirname}, {argv0}{args}"
+
  types:
+
    dirname: path
+
    argv0: word
+
    args: text
+
  impl:
+
    rust:
+
      function:
+
        run_command_with_node
+

+
- given: "an installed CI broker"
+
  impl:
+
    rust:
+
      function: install_ci_broker
+

- then: "stdout has one line"
  impl:
    rust:
modified src/logger.rs
@@ -110,7 +110,7 @@ pub fn start_cib() {
pub fn end_cib_successfully() {
    info!(
        kind = %Kind::Shutdown,
-
        succss = true,
+
        success = true,
        "CI broker ends successfully"
    );
}
@@ -118,7 +118,7 @@ pub fn end_cib_successfully() {
pub fn end_cib_in_error() {
    error!(
        kind = %Kind::Shutdown,
-
        succss = false,
+
        success = false,
        "CI broker ends in unrecoverable error"
    );
}
modified src/subplot.rs
@@ -1,12 +1,22 @@
// Implementations of Subplot scenario steps for the CI broker.

use std::{
-
    fs::{set_permissions, Permissions},
+
    fs::{metadata, set_permissions, Permissions},
    io::Write,
    os::unix::fs::PermissionsExt,
    path::{Path, PathBuf},
+
    process::Command,
+
    str::FromStr,
};

+
use radicle::{
+
    git::RefString,
+
    node::{Event, NodeId},
+
    prelude::RepoId,
+
    storage::RefUpdate,
+
};
+
use radicle_git_ext::Oid;
+

use subplotlib::steplibrary::datadir::Datadir;
use subplotlib::steplibrary::files::Files;
use subplotlib::steplibrary::runcmd::Runcmd;
@@ -18,6 +28,401 @@ impl ContextElement for SubplotContext {}

#[step]
#[context(SubplotContext)]
+
#[context(Datadir)]
+
#[context(Runcmd)]
+
fn setup_node(context: &ScenarioContext, config: SubplotDataFile, adapter: SubplotDataFile) {
+
    // Install binaries.
+
    let target_path = bindir();
+
    println!("check CI broker binaries are in {}", target_path.display());
+
    assert!(target_path.join("cib").exists());
+
    assert!(target_path.join("cibtool").exists());
+
    assert!(target_path.join("synthetic-events").exists());
+
    context.with_mut(
+
        |context: &mut Runcmd| {
+
            context.prepend_to_path(target_path);
+
            Ok(())
+
        },
+
        false,
+
    )?;
+

+
    // Create configuration file.
+
    if let Some(parent) = config.name().parent() {
+
        if parent != Path::new("") {
+
            println!(
+
                "create directory for configuration file: {:?}",
+
                parent.display()
+
            );
+
            context.with_mut(
+
                |context: &mut Datadir| {
+
                    context.create_dir_all(parent)?;
+
                    Ok(())
+
                },
+
                false,
+
            )?;
+
        }
+
    }
+

+
    println!("write configuration file {}", config.name().display());
+
    context.with_mut(
+
        |context: &mut Datadir| {
+
            context
+
                .open_write(config.name())?
+
                .write_all(config.data())?;
+
            Ok(())
+
        },
+
        false,
+
    )?;
+

+
    // Create an executable adapter.
+
    if let Some(parent) = adapter.name().parent() {
+
        if parent != Path::new("") {
+
            println!("create directory for adapter: {}", parent.display());
+
            context.with_mut(
+
                |context: &mut Datadir| {
+
                    context.create_dir_all(parent)?;
+
                    Ok(())
+
                },
+
                false,
+
            )?;
+
        }
+
    }
+

+
    context.with_mut(
+
        |context: &mut Datadir| {
+
            // Unix mode bits for an executable file: read/write/exec for
+
            // owner, read/exec for group and others
+
            const EXECUTABLE: u32 = 0o755;
+

+
            println!("create env file");
+
            let home = context.canonicalise_filename(".")?;
+
            let home_str = home.display().to_string();
+

+
            let rad_home = context.canonicalise_filename(".radicle")?;
+
            let rad_home_str = rad_home.display().to_string();
+

+
            let envs = &[
+
                ("HOME", home_str.as_str()),
+
                ("RAD_HOME", rad_home_str.as_str()),
+
                ("RAD_PASSPHRASE", "secret"),
+
                ("RAD_SOCKET", "synt.sock"),
+
            ];
+
            {
+
                let mut file = context.open_write("env")?;
+
                for (k, v) in envs.iter() {
+
                    file.write_all(format!("export {k}='{v}'\n").as_bytes())?;
+
                }
+
            }
+

+
            println!("create env.sh script");
+
            {
+
                const SCRIPT: &str = r#"#!/bin/bash
+
echo "env.sh starts"
+
if [ -e env ]; then . ./env; fi
+
exec "$@"
+
"#;
+
                let mut file = context.open_write("env.sh")?;
+
                file.write_all(SCRIPT.as_bytes())?;
+
            }
+

+
            println!("make env.sh executable");
+
            let filename = context.canonicalise_filename("env.sh")?;
+
            let meta = metadata(&filename)?;
+
            let mut perm = meta.permissions();
+
            perm.set_mode(EXECUTABLE);
+
            set_permissions(&filename, perm)?;
+

+
            let filename = Path::new("adapter.sh");
+

+
            println!(
+
                "write adapter file {} from {}",
+
                filename.display(),
+
                adapter.name().display()
+
            );
+
            context.open_write(filename)?.write_all(adapter.data())?;
+

+
            println!("make {} executable", filename.display());
+
            let filename = context.canonicalise_filename("adapter.sh")?;
+
            let meta = metadata(&filename)?;
+
            let mut perm = meta.permissions();
+
            perm.set_mode(EXECUTABLE);
+
            set_permissions(&filename, perm)?;
+

+
            Ok(())
+
        },
+
        false,
+
    )?;
+

+
    // Create node by running "rad auth".
+

+
    context.with_mut(
+
        |context: &mut Datadir| {
+
            let home = context.canonicalise_filename(".")?;
+
            let rad_home = context.canonicalise_filename(".radicle")?;
+

+
            rad_in(
+
                &["auth", "--alias=brokertest"],
+
                &[
+
                    ("RAD_HOME", &rad_home.display().to_string()),
+
                    ("RAD_PASSPHRASE", "secret"),
+
                    ("RAD_SOCKET", "synt.sock"),
+
                ],
+
                &home,
+
            )?;
+

+
            Ok(())
+
        },
+
        false,
+
    )?;
+
}
+

+
#[step]
+
#[context(SubplotContext)]
+
#[context(Datadir)]
+
#[context(Runcmd)]
+
fn start_node(_context: &ScenarioContext, _config: SubplotDataFile, _adapter: SubplotDataFile) {
+
    unimplemented!();
+
}
+

+
#[step]
+
#[context(SubplotContext)]
+
#[context(Datadir)]
+
#[context(Runcmd)]
+
fn create_repo(context: &ScenarioContext, name: &str) {
+
    // Create a Git repository and add it to the Radicle node.
+
    context.with_mut(
+
        |context: &mut Datadir| {
+
            let home = context.canonicalise_filename(".")?;
+
            let home_str = home.display().to_string();
+

+
            let rad_home = context.canonicalise_filename(".radicle")?;
+
            let rad_home_str = rad_home.display().to_string();
+

+
            let envs = &[
+
                ("HOME", home_str.as_str()),
+
                ("RAD_HOME", rad_home_str.as_str()),
+
                ("RAD_PASSPHRASE", "secret"),
+
                ("RAD_SOCKET", "synt.sock"),
+
            ];
+

+
            git_in(
+
                &["config", "--global", "user.email", "radicle@example.com"],
+
                envs,
+
                &home,
+
            )?;
+

+
            git_in(
+
                &["config", "--global", "user.name", "TestyMcTestFace"],
+
                envs,
+
                &home,
+
            )?;
+

+
            git_in(&["init", "-b", "main", name], envs, &home)?;
+

+
            {
+
                let filename = Path::new(name).join("file.dat");
+
                let mut file = context.open_write(filename)?;
+
                file.write_all(b"hello, world")?;
+
            }
+

+
            let repodir = context.canonicalise_filename(name)?;
+
            git_in(&["add", "."], envs, &repodir)?;
+
            git_in(&["commit", "-am", "test"], envs, &repodir)?;
+

+
            rad_in(
+
                &[
+
                    "init",
+
                    "--name",
+
                    name,
+
                    "--description=test",
+
                    "--default-branch=main",
+
                    "--private",
+
                    "--no-confirm",
+
                    "--no-seed",
+
                ],
+
                envs,
+
                &repodir,
+
            )?;
+

+
            // rad init --name testy --description test --default-branch main --private --no-confirm --no-seed
+
            // rad inspect --identity
+
            // rad id list
+

+
            Ok(())
+
        },
+
        false,
+
    )?;
+
}
+
#[step]
+
#[context(SubplotContext)]
+
#[context(Datadir)]
+
#[context(Runcmd)]
+
fn run_command_with_node(context: &ScenarioContext, dirname: &Path, argv0: &str, args: &str) {
+
    let path: String = context.with_mut(
+
        |context: &mut Runcmd| Ok(context.join_paths().unwrap().to_string_lossy().to_string()),
+
        false,
+
    )?;
+
    println!("run_command_with_node: PATH={path}");
+

+
    context.with_mut(
+
        |context: &mut Datadir| {
+
            let home = context.canonicalise_filename(".")?;
+
            let home_str = home.display().to_string();
+

+
            let rad_home = context.canonicalise_filename(".radicle")?;
+
            let rad_home_str = rad_home.display().to_string();
+

+
            let envs = &[
+
                ("HOME", home_str.as_str()),
+
                ("RAD_HOME", rad_home_str.as_str()),
+
                ("RAD_PASSPHRASE", "secret"),
+
                ("RAD_SOCKET", "synt.sock"),
+
                ("PATH", path.as_str()),
+
            ];
+

+
            let dirname = context.canonicalise_filename(dirname)?;
+
            let words: Vec<&str> = args.split_ascii_whitespace().collect();
+
            run_in(argv0, &words, envs, &dirname)?;
+

+
            Ok(())
+
        },
+
        false,
+
    )?;
+
}
+

+
fn rad_in(args: &[&str], envs: &[(&str, &str)], cwd: &Path) -> Result<(), std::io::Error> {
+
    run_in("rad", args, envs, cwd)
+
}
+

+
fn git_in(args: &[&str], envs: &[(&str, &str)], cwd: &Path) -> Result<(), std::io::Error> {
+
    run_in("git", args, envs, cwd)
+
}
+

+
fn run_in(
+
    argv0: &str,
+
    args: &[&str],
+
    envs: &[(&str, &str)],
+
    cwd: &Path,
+
) -> Result<(), std::io::Error> {
+
    println!("running command {argv0} {args:?}");
+
    println!("envs: {envs:?}");
+
    println!("cwd: {cwd:?}; exists? {}", cwd.exists());
+

+
    let output = Command::new(argv0)
+
        .args(args)
+
        .envs(envs.iter().copied())
+
        .current_dir(cwd)
+
        .output()?;
+
    println!("{argv0} exit code: {:?}", output.status.code());
+
    println!(
+
        "{argv0}: stdout:\n{}\n====================",
+
        String::from_utf8_lossy(&output.stdout)
+
    );
+
    println!(
+
        "{argv0}: stderr:\n{}\n=====================",
+
        String::from_utf8_lossy(&output.stderr)
+
    );
+
    if !output.status.success() {
+
        panic!("command failed");
+
    }
+
    Ok(())
+
}
+

+
#[step]
+
#[context(SubplotContext)]
+
#[context(Datadir)]
+
#[context(Runcmd)]
+
fn add_event_file(context: &ScenarioContext, repodir: &Path) {
+
    // Write embedded file to "event.json". We only need one, at least for now.
+

+
    context.with_mut(
+
        |datadir: &mut Datadir| {
+
            let rad_home = datadir.canonicalise_filename(".radicle")?;
+
            println!("rad_home: {rad_home:#?}");
+

+
            let nid = nid(&rad_home)?;
+
            println!("nid: {nid:#?}");
+

+
            let repodir = datadir.canonicalise_filename(repodir)?;
+
            let rid = rid(&rad_home, &repodir)?;
+
            println!("rid: {rid:#?}");
+

+
            let head = head(&rad_home, &repodir)?;
+

+
            let node_event = Event::RefsFetched {
+
                remote: nid,
+
                rid,
+
                updated: vec![RefUpdate::Updated {
+
                    name: RefString::try_from(
+
                        format!("refs/namespaces/{nid}/refs/heads/main").as_str(),
+
                    )?,
+
                    old: head,
+
                    new: head,
+
                }],
+
            };
+

+
            println!("node_event: {node_event:#?}");
+
            let node_event = serde_json::to_string(&node_event)?;
+

+
            let event_json = Path::new("event.json");
+
            let filename = datadir.canonicalise_filename(event_json)?;
+
            assert!(!filename.exists());
+

+
            let mut file = datadir.open_write(event_json)?;
+
            file.write_all(node_event.as_bytes())?;
+
            Ok(())
+
        },
+
        false,
+
    )?;
+
}
+

+
fn nid(rad_home: &Path) -> Result<NodeId, Box<dyn std::error::Error>> {
+
    let output = Command::new("rad")
+
        .arg("self")
+
        .arg("--nid")
+
        .env("RAD_HOME", rad_home.display().to_string().as_str())
+
        .output()?;
+
    if !output.status.success() {
+
        panic!("rad self --nid failed");
+
    }
+

+
    Ok(NodeId::from_str(
+
        String::from_utf8_lossy(&output.stdout).to_string().trim(),
+
    )?)
+
}
+

+
fn rid(rad_home: &Path, repo: &Path) -> Result<RepoId, Box<dyn std::error::Error>> {
+
    let output = Command::new("rad")
+
        .arg(".")
+
        .env("RAD_HOME", rad_home.display().to_string().as_str())
+
        .current_dir(repo)
+
        .output()?;
+
    if !output.status.success() {
+
        panic!("rad . failed");
+
    }
+

+
    Ok(RepoId::from_str(
+
        String::from_utf8_lossy(&output.stdout).to_string().trim(),
+
    )?)
+
}
+

+
fn head(rad_home: &Path, repo: &Path) -> Result<Oid, Box<dyn std::error::Error>> {
+
    let output = Command::new("git")
+
        .arg("rev-parse")
+
        .arg("HEAD")
+
        .env("RAD_HOME", rad_home.display().to_string().as_str())
+
        .current_dir(repo)
+
        .output()?;
+
    if !output.status.success() {
+
        panic!("git rev-parse HEAD failed");
+
    }
+

+
    Ok(Oid::from_str(
+
        String::from_utf8_lossy(&output.stdout).to_string().trim(),
+
    )?)
+
}
+

+
#[step]
+
#[context(SubplotContext)]
#[context(Runcmd)]
fn install_ci_broker(context: &ScenarioContext) {
    let target_path = bindir();