Authentication-Results: mail-b.sr.ht; dkim=pass header.d=gmail.com header.i=@gmail.com Received: from mail-pf1-f172.google.com (mail-pf1-f172.google.com [209.85.210.172]) by mail-b.sr.ht (Postfix) with ESMTPS id 1A29311EEB5 for <~radicle-link/dev@lists.sr.ht>; Wed, 10 Aug 2022 05:41:32 +0000 (UTC) Received: by mail-pf1-f172.google.com with SMTP id z187so12717924pfb.12 for <~radicle-link/dev@lists.sr.ht>; Tue, 09 Aug 2022 22:41:32 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc; bh=QVEHvuUtXWsYbWe07QZFAr2NiAh4/GHkAMYo6CDA0UA=; b=cPRveymYW913tLQ0N0k7lKSk/2bhXC+2jTgDm+USK3reBYn+BR3EXmVxM7onpVh0mg b8GY1pGwUMaGtvvjNDaNxg/lGluF/acwU67Bssn5vFrV+xLX/VcuVbSNw1neUgdi/fOz lIGJgRfs3jsZ0odmgd8aAbD5u2Z9lccGKizxL+3WoOeFLjtnSsmC5en9ryY6LuD5w0A9 yeS3S+cwWngZMS9fWfy4N9d5VKWUzprtYZUofH13LBuAfpTd/Y3JB5rT/JqCgRk0hFGM wBLm0rn0dugbRybU2tc8rofR5W/ryWzVSThPfPv+Cg5KMV7RjcN7xYzmwan+yRZGWRPV vOsQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc; bh=QVEHvuUtXWsYbWe07QZFAr2NiAh4/GHkAMYo6CDA0UA=; b=JhJuQE3/kNc/v9bBNZnjhrFV3282vzzdKGVpx1BF0h5G/9xTL5uOBd2J9Y835PqQPt jY2SE6TeW/L6HM8UyNcDPJuN/8QJsq4rZ1I1aCgmDFO4M66SjleUt966VoWi0hRJVJfI +t3THHf7+Q3VkrznZDotTEbxBEQnPEcoRJT0O892MA17Y4K9JH5z8jT+SyigvgocWQRk /miOuAMDXKeoMasdZGXdfVOn4RFw6HcOKCqdXKlXJK41ldO1rt3W1TdxdklxxXjQWE2R OmhjrUl3yI7If+GgyFMrvKJfnbrFib4liQjMVsMRUmwJ181of5dovc5FyQybmDlkI9Mr mx9w== X-Gm-Message-State: ACgBeo1Q2yqE7bXQzIrK1inXtuonYizyOVgCOLoWLndR8AxSB+bps+H/ Iyu9Xt2CE/WSk8klFxJRYyOnIO2u+kQ= X-Google-Smtp-Source: AA6agR4OuQISk1RCPSkPaHS20isQL8eAhokGtHxO0sgTsYK/QGeFqIooZSfCJHnJE843+kKucmeh9A== X-Received: by 2002:a65:6944:0:b0:41b:4483:35cc with SMTP id w4-20020a656944000000b0041b448335ccmr22592246pgq.296.1660110090735; Tue, 09 Aug 2022 22:41:30 -0700 (PDT) Received: from localhost.localdomain (c-98-207-161-235.hsd1.ca.comcast.net. [98.207.161.235]) by smtp.gmail.com with ESMTPSA id m5-20020a170902768500b0016d4f05eb95sm11710349pll.272.2022.08.09.22.41.30 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 09 Aug 2022 22:41:30 -0700 (PDT) From: Han Xu To: ~radicle-link/dev@lists.sr.ht Cc: Han Xu Subject: [PATCH radicle-link v1 1/1] basic integration test for bins Date: Tue, 9 Aug 2022 22:41:16 -0700 Message-Id: <20220810054116.2487-2-keepsimple@gmail.com> X-Mailer: git-send-email 2.32.0 (Apple Git-132) In-Reply-To: <20220810054116.2487-1-keepsimple@gmail.com> References: <20220810054116.2487-1-keepsimple@gmail.com> MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bins/Cargo.lock | 29 +++ bins/Cargo.toml | 1 + bins/tests/Cargo.toml | 13 ++ bins/tests/integration_test.rs | 392 +++++++++++++++++++++++++++++++++ 4 files changed, 435 insertions(+) create mode 100644 bins/tests/Cargo.toml create mode 100644 bins/tests/integration_test.rs diff --git a/bins/Cargo.lock b/bins/Cargo.lock index d5756144..4d0ea413 100644 --- a/bins/Cargo.lock +++ b/bins/Cargo.lock @@ -838,6 +838,17 @@ dependencies = [ "termcolor", ] +[[package]] +name = "errno" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e2b2decb0484e15560df3210cf0d78654bb0864b2c138977c07e377a1bae0e2" +dependencies = [ + "kernel32-sys", + "libc", + "winapi 0.2.8", +] + [[package]] name = "event-listener" version = "2.5.2" @@ -2945,6 +2956,16 @@ dependencies = [ "human_format", ] +[[package]] +name = "pty" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50f3d255966981eb4e4c5df3e983e6f7d163221f547406d83b6a460ff5c5ee8" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "quanta" version = "0.4.1" @@ -3567,6 +3588,14 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "tests" +version = "0.1.0" +dependencies = [ + "pty", + "serde_json", +] + [[package]] name = "textwrap" version = "0.15.0" diff --git a/bins/Cargo.toml b/bins/Cargo.toml index 3d1786f4..99b3c69f 100644 --- a/bins/Cargo.toml +++ b/bins/Cargo.toml @@ -5,4 +5,5 @@ members = [ "lnk-gitd", "lnk-identities-dev", "lnk-profile-dev", + "tests", ] diff --git a/bins/tests/Cargo.toml b/bins/tests/Cargo.toml new file mode 100644 index 00000000..3ae82769 --- /dev/null +++ b/bins/tests/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "tests" +version = "0.1.0" +authors = ["Han Xu "] +edition = "2021" + +[dev-dependencies] +pty = "0.2" +serde_json = "1.0" + +[[test]] +name = "integration_test" +path = "integration_test.rs" diff --git a/bins/tests/integration_test.rs b/bins/tests/integration_test.rs new file mode 100644 index 00000000..4ff581a0 --- /dev/null +++ b/bins/tests/integration_test.rs @@ -0,0 +1,392 @@ +// Copyright © 2022 The Radicle Link Contributors +// +// This file is part of radicle-link, distributed under the GPLv3 with Radicle +// Linking Exception. For full terms see the included LICENSE file. + +//! Integration test to exercise the programs in `bins`. + +use pty::fork::*; +use serde_json::{json, Value}; +use std::{ + env, + fs::File, + io::{BufRead, BufReader, Write}, + process::{Child, Command, Stdio}, + thread, + time::{Duration, SystemTime}, +}; + +/// This test is inspired by https://github.com/alexjg/linkd-playground +#[test] +fn happy_path_to_push_changes() { + let peer1_home = "/tmp/link-local-1"; + let peer2_home = "/tmp/link-local-2"; + let seed_home = "/tmp/seed-home"; + let passphrase = b"play\n"; + + println!("\n== create lnk homes for two peers and one seed ==\n"); + let (is_parent, _) = run_lnk(LnkCmd::ProfileCreate, peer1_home, passphrase); + if !is_parent { + return; + } + let (is_parent, _) = run_lnk(LnkCmd::ProfileCreate, peer2_home, passphrase); + if !is_parent { + return; + } + let (is_parent, _) = run_lnk(LnkCmd::ProfileCreate, seed_home, passphrase); + if !is_parent { + return; + } + + println!("\n== add ssh keys for each profile to the ssh-agent ==\n"); + let (is_parent, _) = run_lnk(LnkCmd::ProfileSshAdd, peer1_home, passphrase); + if !is_parent { + return; + } + let (is_parent, _) = run_lnk(LnkCmd::ProfileSshAdd, peer2_home, passphrase); + if !is_parent { + return; + } + let (is_parent, _) = run_lnk(LnkCmd::ProfileSshAdd, seed_home, passphrase); + if !is_parent { + return; + } + + println!("\n== Creating local link 1 identity ==\n"); + let peer1_name = "sockpuppet1".to_string(); + let (is_parent, output) = run_lnk(LnkCmd::IdPersonCreate(peer1_name), peer1_home, passphrase); + if !is_parent { + return; + } + let v: Value = serde_json::from_str(&output).unwrap(); + let urn1 = v["urn"].as_str().unwrap().to_string(); + let (is_parent, _) = run_lnk(LnkCmd::IdLocalSet(urn1), peer1_home, passphrase); + if !is_parent { + return; + } + + println!("\n== Creating local link 2 identity ==\n"); + let peer2_name = "sockpuppet2".to_string(); + let (is_parent, output) = run_lnk(LnkCmd::IdPersonCreate(peer2_name), peer2_home, passphrase); + if !is_parent { + return; + } + let v: Value = serde_json::from_str(&output).unwrap(); + let urn2 = v["urn"].as_str().unwrap().to_string(); + let (is_parent, _) = run_lnk(LnkCmd::IdLocalSet(urn2), peer2_home, passphrase); + if !is_parent { + return; + } + + println!("\n== Create a local repository ==\n"); + let peer1_proj = format!("peer1_proj_{}", timestamp()); + let (is_parent, output) = run_lnk( + LnkCmd::IdProjectCreate(peer1_proj.clone()), + peer1_home, + passphrase, + ); + if !is_parent { + return; + } + let v: Value = serde_json::from_str(&output).unwrap(); + let proj_urn = v["urn"].as_str().unwrap().to_string(); + println!("our project URN: {}", &proj_urn); + + println!("\n== Add the seed to the local peer seed configs ==\n"); + let (is_parent, seed_peer_id) = run_lnk(LnkCmd::ProfilePeer, seed_home, passphrase); + if !is_parent { + return; + } + let seed_endpoint = format!("{}@127.0.0.1:8799", &seed_peer_id); + + let (is_parent, peer1_profile) = run_lnk(LnkCmd::ProfileGet, peer1_home, passphrase); + if !is_parent { + return; + } + let peer1_seed = format!("{}/{}/seeds", peer1_home, peer1_profile); + let mut peer1_f = File::create(peer1_seed).unwrap(); + peer1_f.write_all(seed_endpoint.as_bytes()).unwrap(); + + let (is_parent, peer2_profile) = run_lnk(LnkCmd::ProfileGet, peer2_home, passphrase); + if !is_parent { + return; + } + let peer2_seed = format!("{}/{}/seeds", peer2_home, peer2_profile); + let mut peer2_f = File::create(peer2_seed).unwrap(); + peer2_f.write_all(seed_endpoint.as_bytes()).unwrap(); + + println!("\n== Start the seed linkd ==\n"); + let manifest_path = manifest_path(); + let mut linkd = spawn_linkd(seed_home, &manifest_path); + + println!("\n== Start the peer 1 gitd ==\n"); + let (is_parent, peer1_peer_id) = run_lnk(LnkCmd::ProfilePeer, peer1_home, passphrase); + if !is_parent { + return; + } + let mut lnk_gitd = spawn_lnk_gitd(peer1_home, &manifest_path, &peer1_peer_id); + + println!("\n== Make some changes in the repo ==\n"); + env::set_current_dir(&peer1_proj).unwrap(); + let mut test_file = File::create("test").unwrap(); + test_file.write_all(b"test").unwrap(); + Command::new("git") + .arg("add") + .arg("test") + .output() + .expect("failed to do git add"); + let output = Command::new("git") + .arg("commit") + .arg("-m") + .arg("test commit") + .output() + .expect("failed to do git commit"); + println!("git-commit: {:?}", &output); + + println!("\n== Add the linkd remote to the repo ==\n"); + let remote_url = format!("ssh://rad@127.0.0.1:9987/{}.git", &proj_urn); + Command::new("git") + .arg("remote") + .arg("add") + .arg("linkd") + .arg(remote_url) + .output() + .expect("failed to do git remote add"); + + clean_up_known_hosts(); + + linkd.kill().ok(); + lnk_gitd.kill().ok(); +} + +enum LnkCmd { + ProfileCreate, + ProfileGet, + ProfilePeer, + ProfileSshAdd, + IdPersonCreate(String), // the associated string is "the person's name". + IdLocalSet(String), // the associated string is "urn". + IdProjectCreate(String), // the associated string is "the project name". +} + +/// Runs a `cmd` for `lnk_home`. Rebuilds `lnk` if necessary. +/// Return.0: true if this is the parent (i.e. test) process, +/// false if this is the child (i.e. lnk) process. +/// Return.1: an output that depends on the `cmd`. +fn run_lnk(cmd: LnkCmd, lnk_home: &str, passphrase: &[u8]) -> (bool, String) { + let fork = Fork::from_ptmx().unwrap(); + if let Some(mut parent) = fork.is_parent().ok() { + // Input the passphrase if necessary. + match cmd { + LnkCmd::ProfileCreate | LnkCmd::ProfileSshAdd => { + parent.write_all(passphrase).unwrap(); + println!("{}: wrote passphase", lnk_home); + }, + _ => {}, + } + + // Print the output and decode them if necessary. + let buf_reader = BufReader::new(parent); + let mut output = String::new(); + for line in buf_reader.lines() { + let line = line.unwrap(); + println!("{}: {}", lnk_home, line); + + match cmd { + LnkCmd::IdPersonCreate(ref _name) => { + if line.find("\"urn\":").is_some() { + output = line; // get the line with URN. + } + }, + LnkCmd::IdProjectCreate(ref _name) => { + if line.find("\"urn\":").is_some() { + output = line; // get the line with URN. + } + }, + LnkCmd::ProfileGet => { + output = line; // get the last line for profile id. + }, + LnkCmd::ProfilePeer => { + output = line; // get the last line for peer id. + }, + _ => {}, + } + } + + (true, output) + } else { + // Child process is to run `lnk`. + let manifest_path = manifest_path(); + + // cargo run \ + // --manifest-path $LINK_CHECKOUT/bins/Cargo.toml \ + // -p lnk -- "$@" + let mut lnk_cmd = Command::new("cargo"); + lnk_cmd + .env("LNK_HOME", lnk_home) + .arg("run") + .arg("--manifest-path") + .arg(manifest_path) + .arg("-p") + .arg("lnk") + .arg("--"); + let full_cmd = match cmd { + LnkCmd::ProfileCreate => lnk_cmd.arg("profile").arg("create"), + LnkCmd::ProfileGet => lnk_cmd.arg("profile").arg("get"), + LnkCmd::ProfilePeer => lnk_cmd.arg("profile").arg("peer"), + LnkCmd::ProfileSshAdd => lnk_cmd.arg("profile").arg("ssh").arg("add"), + LnkCmd::IdPersonCreate(name) => { + let payload = json!({ "name": name }); + lnk_cmd + .arg("identities") + .arg("person") + .arg("create") + .arg("new") + .arg("--payload") + .arg(payload.to_string()) + }, + LnkCmd::IdLocalSet(urn) => lnk_cmd + .arg("identities") + .arg("local") + .arg("set") + .arg("--urn") + .arg(urn), + LnkCmd::IdProjectCreate(name) => { + let payload = json!({"name": name, "default_branch": "master"}); + let project_path = format!("./{}", name); + lnk_cmd + .arg("identities") + .arg("project") + .arg("create") + .arg("new") + .arg("--path") + .arg(project_path) + .arg("--payload") + .arg(payload.to_string()) + }, + }; + full_cmd.status().expect("lnk cmd failed:"); + + (false, String::new()) + } +} + +fn spawn_linkd(lnk_home: &str, manifest_path: &str) -> Child { + let log_name = format!("linkd_{}.log", ×tamp()); + let log_file = File::create(&log_name).unwrap(); + let child = Command::new("cargo") + .arg("run") + .arg("--manifest-path") + .arg(manifest_path) + .arg("-p") + .arg("linkd") + .arg("--") + .arg("--lnk-home") + .arg(lnk_home) + .arg("--track") + .arg("everything") + .arg("--protocol-listen") + .arg("127.0.0.1:8799") + .stdout(Stdio::from(log_file)) + .spawn() + .expect("linkd failed to start"); + println!("linkd stdout redirected to {}", &log_name); + thread::sleep(Duration::from_secs(1)); + child +} + +fn spawn_lnk_gitd(lnk_home: &str, manifest_path: &str, peer_id: &str) -> Child { + let log_name = format!("lnk-gitd_{}.log", ×tamp()); + let log_file = File::create(&log_name).unwrap(); + let port = "9987"; + let xdg_runtime_dir = env!("XDG_RUNTIME_DIR"); + let rpc_socket = format!("{}/link-peer-{}-rpc.socket", xdg_runtime_dir, peer_id); + + Command::new("cargo") + .arg("build") + .arg("--target-dir") + .arg("./target") + .arg("--manifest-path") + .arg(manifest_path) + .arg("-p") + .arg("lnk-gitd") + .output() + .expect("cargo build lnk-gitd failed"); + + let child = Command::new("systemd-socket-activate") + .arg("-l") + .arg(port) + .arg("--fdname=ssh") + .arg("-E") + .arg("SSH_AUTH_SOCK") + .arg("-E") + .arg("RUST_BACKTRACE") + .arg("./target/debug/lnk-gitd") + .arg(lnk_home) + .arg("--linkd-rpc-socket") + .arg(rpc_socket) + .arg("--push-seeds") + .arg("--fetch-seeds") + .arg("--linger-timeout") + .arg("10000") + .stdout(Stdio::from(log_file)) + .spawn() + .expect("lnk-gitd failed to start"); + println!("lnk-gitd stdout redirected to {}", &log_name); + thread::sleep(Duration::from_secs(1)); + + child +} + +/// Returns true if this is the parent process, +/// returns false if this is the child process. +fn _run_git_push() -> bool { + let fork = Fork::from_ptmx().unwrap(); + if let Some(mut parent) = fork.is_parent().ok() { + let yes = b"yes\n"; + parent.write_all(yes).unwrap(); + + let buf_reader = BufReader::new(parent); + for line in buf_reader.lines() { + let line = line.unwrap(); + println!("git-push: {}", line); + } + + true + } else { + Command::new("git") + .arg("push") + .arg("linkd") + .status() + .expect("failed to do git push"); + false + } +} + +/// Returns UNIX_TIME in millis. +fn timestamp() -> u128 { + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + now.as_millis() +} + +/// Returns the full path of `bins` manifest file. +fn manifest_path() -> String { + let package_dir = env!("CARGO_MANIFEST_DIR"); + format!("{}/Cargo.toml", package_dir.strip_suffix("/tests").unwrap()) +} + +fn clean_up_known_hosts() { + // ssh-keygen -f "/home/pi/.ssh/known_hosts" -R "[127.0.0.1]:9987" + let home_dir = env!("HOME"); + let known_hosts = format!("{}/.ssh/known_hosts", &home_dir); + let output = Command::new("ssh-keygen") + .arg("-f") + .arg(known_hosts) + .arg("-R") + .arg("[127.0.0.1]:9987") + .output() + .expect("failed to do ssh-keygen"); + println!("ssh-keygen: {:?}", &output); +} -- 2.32.0 (Apple Git-132)