mirror of
https://seed.radicle.dev/rad:z3qg5TKmN83afz2fj9z3fQjU8vaYE.git
synced 2026-10-30 16:30:59 +08:00
This adapter is only ever run by the Radicle CI broker, and that can now limit CI run duration itself. Thus having this adapter do it too is pointless duplication. Further, this removes the run time dependency on an external "timeout" command, which turns out not to be portable: it isn't available on Macs, by default. Signed-off-by: Lars Wirzenius <liw@liw.fi>
736 lines
22 KiB
Rust
736 lines
22 KiB
Rust
//! Integration tests for radicle-native-ci.
|
|
//!
|
|
//! Most of this module is helpers to make the actual tests easier to
|
|
//! express.
|
|
//!
|
|
//! These test the `radicle-ci-binary` by running it with different
|
|
//! inputs, and verifying the output is as expected.
|
|
//!
|
|
//! The binary is run with `cargo run -q`.
|
|
//!
|
|
//! Each can set up a configuration file for the adapter, a Radicle
|
|
//! node, and a git repository with a `.radicle/native.yaml` that runs
|
|
//! the appropriate shell command for the test case. Depending on the
|
|
//! test case, some or all of the previous may or may not happen.
|
|
//!
|
|
//! Each test case is represented by [`TestCase`], and those are built
|
|
//! with [`TestCaseBuilder`]. The builder pattern makes it easy to
|
|
//! vary what happens in each test case in a reasonably clear fashion.
|
|
|
|
use std::{
|
|
collections::HashMap,
|
|
fmt,
|
|
fs::File,
|
|
io::Write,
|
|
path::{Path, PathBuf},
|
|
process::Command,
|
|
str::FromStr,
|
|
};
|
|
|
|
use radicle::{git::Oid, identity::Did, prelude::RepoId};
|
|
use radicle_ci_broker::msg::{
|
|
Author, EventCommonFields, EventType, Patch, PatchAction, PatchEvent, Repository, Request,
|
|
Response, RunId, RunResult, State,
|
|
};
|
|
use tempfile::{tempdir, TempDir};
|
|
|
|
use radicle_native_ci::{config::Config, runspec::RunSpec};
|
|
|
|
// The result of running the adapter.
|
|
#[allow(dead_code)]
|
|
struct AdapterResult {
|
|
exit: i32,
|
|
stdout: String,
|
|
stderr: String,
|
|
}
|
|
|
|
impl AdapterResult {
|
|
// The messages that the adapter outputs, if parsing them is
|
|
// possible.
|
|
fn messages(&self) -> Result<Vec<Response>, serde_json::Error> {
|
|
self.stdout
|
|
.lines()
|
|
.map(serde_json::from_str::<Response>)
|
|
.collect()
|
|
}
|
|
|
|
// Assert that adapter stderr contains the wanted sub-string.
|
|
fn assert_stderr_contains(&self, wanted: &str) {
|
|
assert!(self.stderr.contains(wanted));
|
|
}
|
|
|
|
// Assert the adapter terminated with a specific exit code.
|
|
fn assert_got_exit(&self, wanted: i32) {
|
|
assert_eq!(self.exit, wanted);
|
|
}
|
|
|
|
// Return run id from adapter, if one was returned.
|
|
fn run_id(&self) -> Option<RunId> {
|
|
let msgs = self.messages().unwrap();
|
|
assert!(!msgs.is_empty());
|
|
match &msgs[0] {
|
|
Response::Triggered { run_id, .. } => Some(run_id.clone()),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
// Assert that the adapter sent a message with a run ID.
|
|
fn assert_got_run_id(&self) {
|
|
assert!(self.run_id().is_some());
|
|
}
|
|
|
|
// Assert that the adapter sent a message the expected info URL.
|
|
fn assert_url_is(&self, expected: &str) {
|
|
let msgs = self.messages().unwrap();
|
|
assert!(!msgs.is_empty());
|
|
match &msgs[0] {
|
|
Response::Triggered {
|
|
run_id: _,
|
|
info_url: Some(url),
|
|
} => {
|
|
assert_eq!(url, expected);
|
|
}
|
|
_ => panic!("unexpected message {:#?}", msgs[0]),
|
|
}
|
|
}
|
|
|
|
// Assert that the adapter wrote a message indicating the CI run
|
|
// succeeded.
|
|
fn assert_got_success(&self) {
|
|
let msgs = self.messages().unwrap();
|
|
assert!(msgs.len() == 2);
|
|
assert!(matches!(
|
|
msgs[1],
|
|
Response::Finished {
|
|
result: RunResult::Success
|
|
}
|
|
));
|
|
}
|
|
|
|
// Assert that the adapter wrote a message indicating the CI run
|
|
// failed.
|
|
fn assert_got_failure(&self) {
|
|
let msgs = self.messages().unwrap();
|
|
assert!(msgs.len() == 2);
|
|
assert!(matches!(
|
|
msgs[1],
|
|
Response::Finished {
|
|
result: RunResult::Failure
|
|
}
|
|
));
|
|
}
|
|
}
|
|
|
|
// Format an [`AdapterResult`] in a more readable way.
|
|
impl fmt::Debug for AdapterResult {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.write_str(&format!(
|
|
"AdapterResult:\n exit: {}\n stdout:\n{} stderr:\n{}",
|
|
self.exit,
|
|
indent(&self.stdout),
|
|
indent(&self.stderr),
|
|
))
|
|
}
|
|
}
|
|
|
|
// Indent every line in a string.
|
|
fn indent(s: &str) -> String {
|
|
let mut out = String::new();
|
|
for line in s.lines() {
|
|
out.push_str(&format!(" {line:?}\n"));
|
|
}
|
|
out
|
|
}
|
|
|
|
// All the information needed to run the adapter for a specific test
|
|
// case. Constructed by [`TestCaseBuilder::build`].
|
|
struct TestCase {
|
|
tmp: TempDir,
|
|
envs: HashMap<String, String>,
|
|
git: Git,
|
|
rad: Rad,
|
|
request: TriggerKind,
|
|
}
|
|
|
|
impl TestCase {
|
|
fn run(&self) -> Result<AdapterResult, std::io::Error> {
|
|
println!("run adapter");
|
|
|
|
let tmp = self.tmp.path().to_path_buf();
|
|
|
|
// Create a request and write it to a file, if requested. The
|
|
// file is always created so that it can be the adapter's
|
|
// stdin.
|
|
let req_filename = tmp.join("request.json");
|
|
let mut req_file = File::create(&req_filename)?;
|
|
match self.request {
|
|
TriggerKind::Empty => {}
|
|
TriggerKind::Trigger => {
|
|
let commit = self.git.head()?;
|
|
self.rad.init(self.git.path())?;
|
|
let rid = self.rad.rid(self.git.path())?;
|
|
let req = trigger(rid, commit);
|
|
let mut line = serde_json::to_string(&req).unwrap();
|
|
line.push('\n');
|
|
req_file.write_all(line.as_bytes())?;
|
|
}
|
|
TriggerKind::MissingRepo => {
|
|
let commit = Oid::from_str("54aacc96197a48b79fcc260f94312d824f5e0a34").unwrap();
|
|
let rid = RepoId::from_str("rad:z3qg5TKmN83afz2fj9z3fQjU8vaYE").unwrap();
|
|
let req = trigger(rid, commit);
|
|
let mut line = serde_json::to_string(&req).unwrap();
|
|
line.push('\n');
|
|
req_file.write_all(line.as_bytes())?;
|
|
}
|
|
TriggerKind::MissingCommit => {
|
|
let commit = Oid::from_str("54aacc96197a48b79fcc260f94312d824f5e0a34").unwrap();
|
|
self.rad.init(self.git.path())?;
|
|
let rid = self.rad.rid(self.git.path())?;
|
|
let req = trigger(rid, commit);
|
|
let mut line = serde_json::to_string(&req).unwrap();
|
|
line.push('\n');
|
|
req_file.write_all(line.as_bytes())?;
|
|
}
|
|
}
|
|
|
|
println!("run adapter with cargo run");
|
|
|
|
println!("self.envs start");
|
|
for (name, value) in self.envs.iter() {
|
|
println!(" {name}={value:?}");
|
|
}
|
|
println!("self.envs end");
|
|
|
|
let adapter = Command::new("cargo")
|
|
.args(["run", "-q"])
|
|
.env_clear()
|
|
.envs(self.envs.iter())
|
|
.stdin(File::open(&req_filename)?)
|
|
.output()?;
|
|
let result = AdapterResult {
|
|
exit: adapter.status.code().unwrap(),
|
|
stdout: String::from_utf8_lossy(&adapter.stdout).into(),
|
|
stderr: String::from_utf8_lossy(&adapter.stderr).into(),
|
|
};
|
|
println!("{result:#?}");
|
|
Ok(result)
|
|
}
|
|
}
|
|
|
|
// What kind of config should the adapter be provided?
|
|
enum ConfigKind {
|
|
// No config: do not set RADICLE_NATIVE_CI in the environment.
|
|
UnsetEnvVar,
|
|
|
|
// No config: file does not exist.
|
|
DoesNotExist(PathBuf),
|
|
|
|
// No config: file is empty.
|
|
EmptyConfig(PathBuf),
|
|
|
|
// Valid config, without info base URL.
|
|
Valid(PathBuf),
|
|
|
|
// Valid, but with info base URL.
|
|
ValidWithUrl(PathBuf, String),
|
|
}
|
|
|
|
// What kind of trigger message should we give the adapter?
|
|
enum TriggerKind {
|
|
// Broken: an empty input.
|
|
Empty,
|
|
|
|
// Broken: the repository referred to doesn't exist.
|
|
MissingRepo,
|
|
|
|
// Broken: the commit referred to doesn't exist in the repo.
|
|
MissingCommit,
|
|
|
|
// Valid Trigger message.
|
|
Trigger,
|
|
}
|
|
|
|
// Build a new [`TestCase`].
|
|
//
|
|
// Before `build` is called, a config and a request must be chosen.
|
|
// The shell command to run is optional, and defaults to an empty
|
|
// command.
|
|
struct TestCaseBuilder {
|
|
tmp: TempDir,
|
|
config: Option<ConfigKind>,
|
|
request: Option<TriggerKind>,
|
|
shell: String,
|
|
}
|
|
|
|
impl TestCaseBuilder {
|
|
fn new() -> Result<Self, std::io::Error> {
|
|
// Create a temporary directory for all files for this test
|
|
// case. When the test case is dropped, the directory and all
|
|
// its contents are deleted.
|
|
let tmp = tempdir()?;
|
|
|
|
Ok(Self {
|
|
tmp,
|
|
config: None,
|
|
request: None,
|
|
shell: "".into(),
|
|
})
|
|
}
|
|
|
|
fn without_config(mut self) -> Self {
|
|
self.config = Some(ConfigKind::UnsetEnvVar);
|
|
self
|
|
}
|
|
|
|
fn with_empty_config(mut self) -> Self {
|
|
self.config = Some(ConfigKind::EmptyConfig(PathBuf::from("/dev/null")));
|
|
self
|
|
}
|
|
|
|
fn with_nonexistent_config(mut self) -> Self {
|
|
self.config = Some(ConfigKind::DoesNotExist(
|
|
self.tmp.path().join("does-not-exist.yaml"),
|
|
));
|
|
self
|
|
}
|
|
|
|
fn with_config(mut self) -> Self {
|
|
self.config = Some(ConfigKind::Valid(self.tmp.path().join("config.yaml")));
|
|
self
|
|
}
|
|
|
|
fn with_config_with_url(mut self, url: &str) -> Self {
|
|
self.config = Some(ConfigKind::ValidWithUrl(
|
|
self.tmp.path().join("config.yaml"),
|
|
url.into(),
|
|
));
|
|
self
|
|
}
|
|
|
|
fn with_empty_request(mut self) -> Self {
|
|
self.request = Some(TriggerKind::Empty);
|
|
self
|
|
}
|
|
|
|
fn with_request_for_nonexistent_repo(mut self) -> Self {
|
|
self.request = Some(TriggerKind::MissingRepo);
|
|
self
|
|
}
|
|
|
|
fn with_request_for_nonexistent_commit(mut self) -> Self {
|
|
self.request = Some(TriggerKind::MissingCommit);
|
|
self
|
|
}
|
|
|
|
fn with_request(mut self) -> Self {
|
|
self.request = Some(TriggerKind::Trigger);
|
|
self
|
|
}
|
|
|
|
fn with_shell(mut self, shell: &str) -> Self {
|
|
self.shell = shell.into();
|
|
self
|
|
}
|
|
|
|
fn build(self) -> Result<TestCase, std::io::Error> {
|
|
let tmp = self.tmp.path();
|
|
|
|
// Create a Radicle node.
|
|
let rad = Rad::new(&tmp.join("dot-radicle"))?;
|
|
rad.auth()?;
|
|
|
|
// Create a git repository.
|
|
let path = tmp.join("git");
|
|
let git = Git::new(&path)?;
|
|
git.init()?;
|
|
git.write("README.md", "Test README")?;
|
|
|
|
// Add .radicle/native.yaml.
|
|
let runspec = RunSpec { shell: self.shell };
|
|
std::fs::create_dir(git.path().join(".radicle"))?;
|
|
git.write(
|
|
".radicle/native.yaml",
|
|
&serde_yaml::to_string(&runspec).unwrap(),
|
|
)?;
|
|
|
|
// Commit everything.
|
|
git.commit("first commit")?;
|
|
|
|
// Create a native CI adapter config.
|
|
assert!(self.config.is_some());
|
|
let config = self.config.unwrap();
|
|
if let ConfigKind::Valid(filename) = &config {
|
|
let config = Config {
|
|
state: tmp.join("state"),
|
|
log: tmp.join("log.html"),
|
|
base_url: None,
|
|
};
|
|
let config = serde_yaml::to_string(&config).unwrap();
|
|
std::fs::write(filename, config)?;
|
|
} else if let ConfigKind::ValidWithUrl(filename, url) = &config {
|
|
let config = Config {
|
|
state: tmp.join("state"),
|
|
log: tmp.join("log.html"),
|
|
base_url: Some(url.into()),
|
|
};
|
|
let config = serde_yaml::to_string(&config).unwrap();
|
|
std::fs::write(filename, config)?;
|
|
}
|
|
|
|
// Set up the requested environment with RAD_HOME and
|
|
// RAD_PASSPHRASE set to the right values.
|
|
let mut envs: HashMap<String, String> = std::env::vars()
|
|
.filter(|(k, _)| k != "RADICLE_NATIVE_CI" && !k.starts_with("RAD_"))
|
|
.collect();
|
|
assert!(!envs.contains_key("RADICLE_NATIVE_CI"));
|
|
envs.insert("RAD_HOME".into(), format!("{}", rad.home().display()));
|
|
envs.insert("RAD_PASSPHRASE".into(), Rad::PASSPHRASE.into());
|
|
|
|
// Set RADICLE_NATIVE_CI if it should be set.
|
|
match config {
|
|
ConfigKind::UnsetEnvVar => (),
|
|
ConfigKind::DoesNotExist(filename)
|
|
| ConfigKind::EmptyConfig(filename)
|
|
| ConfigKind::Valid(filename)
|
|
| ConfigKind::ValidWithUrl(filename, _) => {
|
|
envs.insert("RADICLE_NATIVE_CI".into(), filename.display().to_string());
|
|
}
|
|
}
|
|
|
|
assert!(self.request.is_some());
|
|
Ok(TestCase {
|
|
tmp: self.tmp,
|
|
envs,
|
|
git,
|
|
rad,
|
|
request: self.request.unwrap(),
|
|
})
|
|
}
|
|
}
|
|
|
|
// Manage a git repository for test purposes.
|
|
struct Git {
|
|
path: PathBuf,
|
|
}
|
|
|
|
impl Git {
|
|
fn new(path: &Path) -> Result<Self, std::io::Error> {
|
|
println!("create git repository at {}", path.display());
|
|
std::fs::create_dir(path)?;
|
|
Ok(Self { path: path.into() })
|
|
}
|
|
|
|
fn path(&self) -> &Path {
|
|
&self.path
|
|
}
|
|
|
|
fn write(&self, relative: &str, content: &str) -> Result<(), std::io::Error> {
|
|
println!("write to file {relative} content {content:?}");
|
|
std::fs::write(self.path.join(relative), content)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn _git(&self, args: &[&str]) -> Result<String, std::io::Error> {
|
|
println!("run git {args:?} in {}", self.path.display());
|
|
|
|
let output = Command::new("git")
|
|
.current_dir(&self.path)
|
|
.args(args)
|
|
.output()?;
|
|
|
|
let exit = output.status.code().unwrap();
|
|
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
|
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
|
println!(" exit: {}", exit);
|
|
println!(" stdout: {:?}", stdout);
|
|
println!(" stderr: {:?}", stderr);
|
|
|
|
assert_eq!(exit, 0);
|
|
Ok(stdout)
|
|
}
|
|
|
|
fn init(&self) -> Result<(), std::io::Error> {
|
|
self._git(&["init", "."])?;
|
|
Ok(())
|
|
}
|
|
|
|
fn commit(&self, msg: &str) -> Result<(), std::io::Error> {
|
|
self._git(&["add", "."])?;
|
|
self._git(&["commit", "-m", msg])?;
|
|
Ok(())
|
|
}
|
|
|
|
fn head(&self) -> Result<Oid, std::io::Error> {
|
|
let commit = self._git(&["rev-parse", "HEAD"])?;
|
|
let commit = Oid::from_str(commit.trim()).unwrap();
|
|
Ok(commit)
|
|
}
|
|
}
|
|
|
|
// Manage a Radicle node with `rad` for test purposes. This sets up a
|
|
// new node for the test, it does not use the default node on the host.
|
|
struct Rad {
|
|
home: PathBuf,
|
|
}
|
|
|
|
impl Rad {
|
|
const PASSPHRASE: &'static str = "xyzzy";
|
|
|
|
fn new(home: &Path) -> Result<Self, std::io::Error> {
|
|
std::fs::create_dir(home)?;
|
|
Ok(Self { home: home.into() })
|
|
}
|
|
|
|
fn home(&self) -> &Path {
|
|
&self.home
|
|
}
|
|
|
|
fn _rad(&self, args: &[&str], cwd: &Path) -> Result<String, std::io::Error> {
|
|
println!("run rad {args:?} in {}", cwd.display());
|
|
|
|
let output = Command::new("rad")
|
|
.current_dir(cwd)
|
|
.args(args)
|
|
.env("RAD_HOME", self.home.display().to_string())
|
|
.env("RAD_PASSPHRASE", Self::PASSPHRASE)
|
|
.output()?;
|
|
|
|
let exit = output.status.code().unwrap();
|
|
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
|
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
|
println!(" exit: {}", exit);
|
|
println!(" stdout: {:?}", stdout);
|
|
println!(" stderr: {:?}", stderr);
|
|
|
|
assert_eq!(exit, 0);
|
|
Ok(stdout)
|
|
}
|
|
|
|
fn auth(&self) -> Result<(), std::io::Error> {
|
|
self._rad(&["auth", "--alias=test-node"], &self.home)?;
|
|
|
|
// FIXME: this is a temporary workaround for rad on the system
|
|
// using a newer heartwood than what radicle-native-ci is
|
|
// using, and there being a config entry that has changed.
|
|
// This hack should be removed once the native CI adapter is
|
|
// using a newer heartwood.
|
|
let filename = self.home.join("config.json");
|
|
let config = std::fs::read(&filename).unwrap();
|
|
let config = String::from_utf8_lossy(&config);
|
|
let config = config.replace(r#""relay": "auto","#, r#""relay": false,"#);
|
|
std::fs::write(&filename, config.as_bytes()).unwrap();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn init(&self, git_dir: &Path) -> Result<(), std::io::Error> {
|
|
let name = format!("{}", Path::new(git_dir.file_stem().unwrap()).display());
|
|
self._rad(
|
|
&[
|
|
"init",
|
|
"--name",
|
|
&name,
|
|
"--description=test repo",
|
|
"--public",
|
|
"--no-confirm",
|
|
],
|
|
git_dir,
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn rid(&self, git_dir: &Path) -> Result<RepoId, std::io::Error> {
|
|
let rid = self._rad(&["."], git_dir)?;
|
|
let rid = RepoId::from_str(rid.trim()).unwrap();
|
|
Ok(rid)
|
|
}
|
|
}
|
|
|
|
// Construct a trigger message for test purposes.
|
|
fn trigger(repo_id: RepoId, commit: Oid) -> Request {
|
|
Request::Trigger {
|
|
common: EventCommonFields {
|
|
version: 1,
|
|
event_type: EventType::Push,
|
|
repository: Repository {
|
|
id: repo_id,
|
|
name: "test-repo".into(),
|
|
description: "test repo".into(),
|
|
private: false,
|
|
default_branch: "main".into(),
|
|
delegates: vec![],
|
|
},
|
|
},
|
|
push: None,
|
|
patch: Some(PatchEvent {
|
|
action: PatchAction::Updated,
|
|
patch: Patch {
|
|
id: Oid::try_from("ff3099ba5de28d954c41d0b5a84316f943794ea4").unwrap(),
|
|
author: Author {
|
|
id: Did::decode("did:key:z6MkgEMYod7Hxfy9qCvDv5hYHkZ4ciWmLFgfvm3Wn1b2w2FV")
|
|
.unwrap(),
|
|
alias: None,
|
|
},
|
|
title: "title".into(),
|
|
state: State {
|
|
status: "status".into(),
|
|
conflicts: vec![],
|
|
},
|
|
before: commit,
|
|
after: commit,
|
|
commits: vec![commit],
|
|
target: Oid::try_from("244130556b47dbc83323ad8e0c2b53c491e6b925").unwrap(),
|
|
labels: vec![],
|
|
assignees: vec![],
|
|
revisions: vec![],
|
|
},
|
|
}),
|
|
}
|
|
}
|
|
|
|
type TestResult = Result<(), std::io::Error>;
|
|
|
|
// What does the adapter do if the RADICLE_NATIVE_CI environment
|
|
// variable is not set?
|
|
#[test]
|
|
fn no_config_env_var() -> TestResult {
|
|
let result = TestCaseBuilder::new()?
|
|
.without_config()
|
|
.with_empty_request()
|
|
.build()?
|
|
.run()?;
|
|
result.assert_got_exit(2);
|
|
result.assert_stderr_contains("RADICLE_NATIVE_CI");
|
|
Ok(())
|
|
}
|
|
|
|
// What does the adapter do if its configuration file does not exist?
|
|
#[test]
|
|
fn config_does_not_exist() -> TestResult {
|
|
let result = TestCaseBuilder::new()?
|
|
.with_nonexistent_config()
|
|
.with_empty_request()
|
|
.build()?
|
|
.run()?;
|
|
result.assert_got_exit(2);
|
|
result.assert_stderr_contains("failed to read configuration file");
|
|
result.assert_stderr_contains("No such file or directory");
|
|
Ok(())
|
|
}
|
|
|
|
// What does the adapter do if its configuration file is empty?
|
|
#[test]
|
|
fn empty_config() -> TestResult {
|
|
let result = TestCaseBuilder::new()?
|
|
.with_empty_config()
|
|
.with_empty_request()
|
|
.build()?
|
|
.run()?;
|
|
|
|
result.assert_got_exit(2);
|
|
result.assert_stderr_contains("YAML");
|
|
Ok(())
|
|
}
|
|
|
|
// What does the adapter do if its stdin is empty and does not have a
|
|
// request message?
|
|
#[test]
|
|
fn empty_request() -> TestResult {
|
|
let result = TestCaseBuilder::new()?
|
|
.with_config()
|
|
.with_empty_request()
|
|
.build()?
|
|
.run()?;
|
|
|
|
result.assert_got_exit(2);
|
|
result.assert_stderr_contains("read request from stdin");
|
|
result.assert_stderr_contains("JSON");
|
|
Ok(())
|
|
}
|
|
|
|
// What does the adapter do if CI runs a command that fails?
|
|
#[test]
|
|
fn command_fails() -> TestResult {
|
|
let result = TestCaseBuilder::new()?
|
|
.with_config()
|
|
.with_request()
|
|
.with_shell("false")
|
|
.build()?
|
|
.run()?;
|
|
|
|
result.assert_got_exit(1);
|
|
result.assert_got_run_id();
|
|
result.assert_got_failure();
|
|
Ok(())
|
|
}
|
|
|
|
// What does the adapter do if it's triggered to run for a repository
|
|
// that doesn't exist?
|
|
#[test]
|
|
fn repo_doesnt_exist() -> TestResult {
|
|
let result = TestCaseBuilder::new()?
|
|
.with_config()
|
|
.with_request_for_nonexistent_repo()
|
|
.with_shell("false")
|
|
.build()?
|
|
.run()?;
|
|
|
|
result.assert_got_exit(1);
|
|
result.assert_got_run_id();
|
|
result.assert_got_failure();
|
|
Ok(())
|
|
}
|
|
|
|
// What does the adapter do if it's triggered to run for a commit that
|
|
// does not exist in the repository it's running on?
|
|
#[test]
|
|
fn commit_doesnt_exist() -> TestResult {
|
|
let result = TestCaseBuilder::new()?
|
|
.with_config()
|
|
.with_request_for_nonexistent_commit()
|
|
.with_shell("false")
|
|
.build()?
|
|
.run()?;
|
|
|
|
result.assert_got_exit(1);
|
|
result.assert_got_run_id();
|
|
result.assert_got_failure();
|
|
Ok(())
|
|
}
|
|
|
|
// What does the adapter do if everything goes well?
|
|
#[test]
|
|
fn happy_path() -> TestResult {
|
|
let result = TestCaseBuilder::new()?
|
|
.with_config()
|
|
.with_request()
|
|
.with_shell("echo hello, world")
|
|
.build()?
|
|
.run()?;
|
|
|
|
result.assert_got_exit(0);
|
|
result.assert_got_run_id();
|
|
result.assert_got_success();
|
|
Ok(())
|
|
}
|
|
|
|
// Does the adapter construct a URL to the build log?
|
|
#[test]
|
|
fn happy_path_with_log_url() -> TestResult {
|
|
let result = TestCaseBuilder::new()?
|
|
.with_config_with_url("https://ci.radicle.liw.fi") // note lack of trailing slash
|
|
.with_request()
|
|
.with_shell("echo hello, world")
|
|
.build()?
|
|
.run()?;
|
|
|
|
result.assert_got_exit(0);
|
|
result.assert_got_run_id();
|
|
result.assert_got_success();
|
|
|
|
let run_id = result.run_id().unwrap();
|
|
let expected = format!("https://ci.radicle.liw.fi/{}/log.html", run_id);
|
|
result.assert_url_is(&expected);
|
|
Ok(())
|
|
}
|