mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
708 lines
25 KiB
Rust
708 lines
25 KiB
Rust
use std::collections::BTreeMap;
|
|
|
|
use fabro_store::Database;
|
|
use fabro_types::{RunId, RunProvenance, RunSpec, RunStatus};
|
|
|
|
use super::archive::ensure_not_archived;
|
|
use super::run_store::map_open_run_error;
|
|
use crate::error::Error;
|
|
use crate::event::{self, Event};
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct RetryRunInput {
|
|
pub source_run_id: RunId,
|
|
pub new_run_id: RunId,
|
|
pub provenance: RunProvenance,
|
|
pub web_url: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct RetryOutcome {
|
|
pub source_run_id: RunId,
|
|
pub new_run_id: RunId,
|
|
}
|
|
|
|
pub async fn retry_run(
|
|
store: &Database,
|
|
input: &RetryRunInput,
|
|
) -> std::result::Result<RetryOutcome, Error> {
|
|
let source_run_id = input.source_run_id;
|
|
let new_run_id = input.new_run_id;
|
|
let source_store = store
|
|
.open_run(&source_run_id)
|
|
.await
|
|
.map_err(|err| map_open_run_error(&source_run_id, err))?;
|
|
let source = source_store
|
|
.state()
|
|
.await
|
|
.map_err(|err| Error::engine(err.to_string()))?;
|
|
|
|
ensure_not_archived(source.archived_at.is_some(), &source_run_id)?;
|
|
ensure_retryable(source.status, &source_run_id)?;
|
|
|
|
let title = source.title().into_owned();
|
|
let parent_id = source.parent_id;
|
|
let RunSpec {
|
|
run_id: _,
|
|
settings,
|
|
graph,
|
|
graph_source,
|
|
workflow_slug,
|
|
workflow_version_id,
|
|
target,
|
|
automation,
|
|
source_directory,
|
|
labels,
|
|
provenance: _,
|
|
manifest_blob,
|
|
definition_blob,
|
|
spec_blob,
|
|
git,
|
|
fork_source_ref,
|
|
} = source.spec;
|
|
|
|
let settings = serde_json::to_value(&settings).map_err(|err| Error::engine(err.to_string()))?;
|
|
let graph = serde_json::to_value(&graph).map_err(|err| Error::engine(err.to_string()))?;
|
|
|
|
let retry_store = store
|
|
.create_run(&new_run_id)
|
|
.await
|
|
.map_err(|err| Error::engine(err.to_string()))?;
|
|
|
|
event::append_event(&retry_store, &new_run_id, &Event::RunCreated {
|
|
run_id: new_run_id,
|
|
title: Some(title),
|
|
settings,
|
|
graph,
|
|
workflow_source: graph_source,
|
|
labels: labels.into_iter().collect::<BTreeMap<_, _>>(),
|
|
source_directory,
|
|
workflow_slug,
|
|
workflow_version_id,
|
|
target,
|
|
automation,
|
|
provenance: input.provenance.clone(),
|
|
manifest_blob,
|
|
// Blobs are content-addressed, so the retried run reads the source
|
|
// run's unredacted spec bytes through the same id.
|
|
spec_blob,
|
|
git,
|
|
fork_source_ref,
|
|
retried_from: Some(source_run_id),
|
|
parent_id,
|
|
web_url: input.web_url.clone(),
|
|
})
|
|
.await
|
|
.map_err(|err| Error::engine(err.to_string()))?;
|
|
|
|
event::append_event(&retry_store, &new_run_id, &Event::RunSubmitted {
|
|
definition_blob,
|
|
})
|
|
.await
|
|
.map_err(|err| Error::engine(err.to_string()))?;
|
|
|
|
Ok(RetryOutcome {
|
|
source_run_id,
|
|
new_run_id,
|
|
})
|
|
}
|
|
|
|
fn ensure_retryable(status: RunStatus, run_id: &RunId) -> std::result::Result<(), Error> {
|
|
if status.is_terminal() {
|
|
Ok(())
|
|
} else {
|
|
Err(Error::Precondition(format!(
|
|
"run {run_id} cannot be retried from status {status}; expected terminal"
|
|
)))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::collections::{BTreeMap, HashMap};
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use fabro_store::{Database, RunProjectionReducer};
|
|
use fabro_types::{
|
|
AuthMethod, BlobHash, DirtyStatus, FailureReason, ForkSourceRef, GitContext, Graph,
|
|
IdpIdentity, Principal, PullRequestLink, RunRunnableSource, RunServerProvenance, RunTarget,
|
|
RunTiming, WorkflowSettings, fixtures, test_support,
|
|
};
|
|
use object_store::memory::InMemory;
|
|
|
|
use super::*;
|
|
|
|
fn memory_store() -> Database {
|
|
fabro_store::test_support::test_database(
|
|
Arc::new(InMemory::new()),
|
|
"",
|
|
Duration::from_millis(1),
|
|
None,
|
|
)
|
|
}
|
|
|
|
fn actor(login: &str) -> Principal {
|
|
Principal::user(
|
|
IdpIdentity::new("github", format!("user:{login}")).unwrap(),
|
|
login.to_string(),
|
|
AuthMethod::DevToken,
|
|
)
|
|
}
|
|
|
|
fn provenance(login: &str) -> RunProvenance {
|
|
RunProvenance {
|
|
server: Some(RunServerProvenance {
|
|
version: "test".to_string(),
|
|
}),
|
|
client: None,
|
|
subject: actor(login),
|
|
}
|
|
}
|
|
|
|
fn git_context() -> GitContext {
|
|
GitContext {
|
|
origin_url: "https://github.com/fabro-sh/fabro".to_string(),
|
|
branch: "main".to_string(),
|
|
sha: Some("abcdef0123456789abcdef0123456789abcdef01".to_string()),
|
|
dirty: DirtyStatus::Clean,
|
|
}
|
|
}
|
|
|
|
fn run_target() -> RunTarget {
|
|
RunTarget::Git(fabro_types::GitRunTarget {
|
|
repo: "fabro-sh/fabro".to_string(),
|
|
branch: "main".to_string(),
|
|
tag: None,
|
|
sha: Some("abcdef0123456789abcdef0123456789abcdef01".to_string()),
|
|
})
|
|
}
|
|
|
|
async fn append_created(
|
|
store: &fabro_store::RunDatabase,
|
|
run_id: RunId,
|
|
manifest_blob: Option<BlobHash>,
|
|
fork_source_ref: Option<ForkSourceRef>,
|
|
) {
|
|
let mut settings = WorkflowSettings::default();
|
|
settings
|
|
.run
|
|
.metadata
|
|
.insert("env".to_string(), "test".to_string());
|
|
let labels = HashMap::from([("team".to_string(), "core".to_string())]);
|
|
event::append_event(store, &run_id, &Event::RunCreated {
|
|
run_id,
|
|
title: Some("Original title".to_string()),
|
|
settings: serde_json::to_value(&settings).unwrap(),
|
|
graph: serde_json::to_value(Graph::new("retry_source")).unwrap(),
|
|
workflow_source: Some("digraph retry_source { start -> exit }".to_string()),
|
|
labels: labels.into_iter().collect(),
|
|
source_directory: Some("/workspace/source".to_string()),
|
|
workflow_slug: Some("retry-source".to_string()),
|
|
workflow_version_id: Some(test_support::test_workflow_version_id()),
|
|
target: Some(run_target()),
|
|
automation: None,
|
|
provenance: provenance("source-user"),
|
|
manifest_blob,
|
|
spec_blob: None,
|
|
git: Some(git_context()),
|
|
fork_source_ref,
|
|
retried_from: None,
|
|
parent_id: None,
|
|
web_url: None,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
async fn append_runnable(store: &fabro_store::RunDatabase, run_id: RunId) {
|
|
event::append_event(store, &run_id, &Event::RunRunnable {
|
|
source: RunRunnableSource::StartRequested,
|
|
actor: None,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
async fn append_started(store: &fabro_store::RunDatabase, run_id: RunId) {
|
|
append_runnable(store, run_id).await;
|
|
event::append_event(store, &run_id, &Event::RunStarting)
|
|
.await
|
|
.unwrap();
|
|
event::append_event(store, &run_id, &Event::RunRunning)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
async fn append_failed(store: &fabro_store::RunDatabase, run_id: RunId, reason: FailureReason) {
|
|
append_started(store, run_id).await;
|
|
let event = Event::workflow_run_failed_from_error(
|
|
&Error::engine("boom"),
|
|
RunTiming::wall_only(10),
|
|
reason,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
);
|
|
event::append_event(store, &run_id, &event).await.unwrap();
|
|
}
|
|
|
|
async fn append_succeeded(store: &fabro_store::RunDatabase, run_id: RunId) {
|
|
append_started(store, run_id).await;
|
|
event::append_event(store, &run_id, &Event::WorkflowRunCompleted {
|
|
timing: RunTiming::wall_only(10),
|
|
artifact_count: 0,
|
|
status: "succeeded".to_string(),
|
|
reason: fabro_types::SuccessReason::Completed,
|
|
total_usd_micros: None,
|
|
final_git_commit_sha: None,
|
|
final_patch: None,
|
|
diff_summary: None,
|
|
billing: None,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
async fn seed_retryable_failed_source(
|
|
store: &Database,
|
|
source_run_id: RunId,
|
|
) -> (Option<BlobHash>, Option<BlobHash>, ForkSourceRef) {
|
|
let source_store = store.create_run(&source_run_id).await.unwrap();
|
|
let manifest_blob = Some(
|
|
source_store
|
|
.write_blob(br#"{\"manifest\":true}"#)
|
|
.await
|
|
.unwrap(),
|
|
);
|
|
let definition_blob = Some(
|
|
source_store
|
|
.write_blob(br#"{\"definition\":true}"#)
|
|
.await
|
|
.unwrap(),
|
|
);
|
|
let fork_source_ref = ForkSourceRef {
|
|
source_run_id: fixtures::RUN_3,
|
|
checkpoint_sha: "fork-sha".to_string(),
|
|
};
|
|
append_created(
|
|
&source_store,
|
|
source_run_id,
|
|
manifest_blob,
|
|
Some(fork_source_ref.clone()),
|
|
)
|
|
.await;
|
|
event::append_event(&source_store, &source_run_id, &Event::RunSubmitted {
|
|
definition_blob,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
event::append_event(&source_store, &source_run_id, &Event::RunParentLinked {
|
|
previous_parent_id: None,
|
|
parent_id: fixtures::RUN_2,
|
|
actor: None,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
event::append_event(&source_store, &source_run_id, &Event::RunTitleUpdated {
|
|
title: "Current title".to_string(),
|
|
actor: None,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
event::append_event(&source_store, &source_run_id, &Event::CheckpointCompleted {
|
|
graph_visit: None,
|
|
resumed_from_stage_id: None,
|
|
node_id: "work".to_string(),
|
|
status: "succeeded".to_string(),
|
|
current_node: "work".to_string(),
|
|
completed_nodes: vec!["work".to_string()],
|
|
node_retries: BTreeMap::new(),
|
|
context_values: BTreeMap::new(),
|
|
node_outcomes: BTreeMap::new(),
|
|
next_node_id: None,
|
|
git_commit_sha: Some("checkpoint-sha".to_string()),
|
|
loop_failure_signatures: BTreeMap::new(),
|
|
restart_failure_signatures: BTreeMap::new(),
|
|
node_visits: BTreeMap::new(),
|
|
diff: Some("diff --git a/file b/file".to_string()),
|
|
diff_summary: Some(fabro_types::DiffSummary {
|
|
files_changed: 1,
|
|
additions: 1,
|
|
deletions: 0,
|
|
}),
|
|
})
|
|
.await
|
|
.unwrap();
|
|
event::append_event(&source_store, &source_run_id, &Event::SandboxInitialized {
|
|
provider: fabro_types::SandboxProviderKind::Local,
|
|
id: "sandbox-source".to_string(),
|
|
working_directory: "/tmp/source".to_string(),
|
|
image: None,
|
|
snapshot: None,
|
|
repo_cloned: None,
|
|
clone_origin_url: None,
|
|
clone_branch: None,
|
|
workspace_root: None,
|
|
repos_root: None,
|
|
primary_repo_path: None,
|
|
primary_repo_link: None,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
event::append_event(&source_store, &source_run_id, &Event::PullRequestLinked {
|
|
pull_request: PullRequestLink {
|
|
owner: "fabro-sh".to_string(),
|
|
repo: "fabro".to_string(),
|
|
number: 42,
|
|
},
|
|
})
|
|
.await
|
|
.unwrap();
|
|
append_failed(&source_store, source_run_id, FailureReason::WorkflowError).await;
|
|
(manifest_blob, definition_blob, fork_source_ref)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn retry_creates_fresh_run_from_durable_definition_only() {
|
|
let store = memory_store();
|
|
let source_run_id = fixtures::RUN_1;
|
|
let (manifest_blob, definition_blob, fork_source_ref) =
|
|
seed_retryable_failed_source(&store, source_run_id).await;
|
|
let source_event_count = store
|
|
.open_run(&source_run_id)
|
|
.await
|
|
.unwrap()
|
|
.list_events()
|
|
.await
|
|
.unwrap()
|
|
.len();
|
|
|
|
let outcome = retry_run(&store, &RetryRunInput {
|
|
source_run_id,
|
|
new_run_id: RunId::new(),
|
|
provenance: provenance("retry-user"),
|
|
web_url: Some("http://localhost:3000/runs/retry".to_string()),
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_ne!(outcome.new_run_id, source_run_id);
|
|
assert_eq!(outcome.source_run_id, source_run_id);
|
|
|
|
let retry_store = store.open_run(&outcome.new_run_id).await.unwrap();
|
|
let retry_events = retry_store.list_events().await.unwrap();
|
|
let retry_state = fabro_store::RunProjection::apply_events(&retry_events).unwrap();
|
|
assert_eq!(retry_events.len(), 2);
|
|
assert_eq!(retry_state.status, RunStatus::Submitted);
|
|
assert_eq!(retry_state.retried_from, Some(source_run_id));
|
|
assert_eq!(retry_state.parent_id, Some(fixtures::RUN_2));
|
|
assert_eq!(retry_state.title(), "Current title");
|
|
assert_eq!(
|
|
retry_state.spec.labels.get("team"),
|
|
Some(&"core".to_string())
|
|
);
|
|
assert_eq!(
|
|
retry_state.spec.settings.run.metadata.get("env"),
|
|
Some(&"test".to_string())
|
|
);
|
|
assert_eq!(retry_state.spec.graph.name, "retry_source");
|
|
assert_eq!(
|
|
retry_state.spec.workflow_version_id,
|
|
Some(test_support::test_workflow_version_id())
|
|
);
|
|
assert_eq!(
|
|
retry_state.spec.graph_source.as_deref(),
|
|
Some("digraph retry_source { start -> exit }")
|
|
);
|
|
assert_eq!(retry_state.spec.git, Some(git_context()));
|
|
assert_eq!(retry_state.spec.target, Some(run_target()));
|
|
assert_eq!(retry_state.spec.manifest_blob, manifest_blob);
|
|
assert_eq!(retry_state.spec.definition_blob, definition_blob);
|
|
assert_eq!(retry_state.spec.fork_source_ref, Some(fork_source_ref));
|
|
assert_eq!(retry_state.spec.provenance.subject, actor("retry-user"));
|
|
assert_eq!(
|
|
retry_state.web_url.as_deref(),
|
|
Some("http://localhost:3000/runs/retry")
|
|
);
|
|
|
|
assert!(retry_state.checkpoints.is_empty());
|
|
assert!(retry_state.conclusion.is_none());
|
|
assert!(retry_state.pull_request.is_none());
|
|
assert!(retry_state.pending_interviews.is_empty());
|
|
assert!(retry_state.pending_control.is_none());
|
|
assert!(
|
|
retry_state
|
|
.sandbox
|
|
.as_ref()
|
|
.and_then(fabro_types::RunSandbox::instance)
|
|
.is_none()
|
|
);
|
|
|
|
let source_store = store.open_run(&source_run_id).await.unwrap();
|
|
assert_eq!(
|
|
source_store.list_events().await.unwrap().len(),
|
|
source_event_count
|
|
);
|
|
assert_eq!(
|
|
source_store.state().await.unwrap().status,
|
|
RunStatus::Failed {
|
|
reason: FailureReason::WorkflowError,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn retry_preserves_none_target_without_git_or_source_directory() {
|
|
let store = memory_store();
|
|
let source_run_id = fixtures::RUN_1;
|
|
let source_store = store.create_run(&source_run_id).await.unwrap();
|
|
event::append_event(&source_store, &source_run_id, &Event::RunCreated {
|
|
run_id: source_run_id,
|
|
title: Some("None target".to_string()),
|
|
settings: serde_json::to_value(WorkflowSettings::default()).unwrap(),
|
|
graph: serde_json::to_value(Graph::new("none_target_retry")).unwrap(),
|
|
workflow_source: Some("digraph none_target_retry { start -> exit }".to_string()),
|
|
labels: BTreeMap::new(),
|
|
source_directory: None,
|
|
workflow_slug: Some("none-target-retry".to_string()),
|
|
workflow_version_id: Some(test_support::test_workflow_version_id()),
|
|
target: Some(RunTarget::None {}),
|
|
automation: None,
|
|
provenance: provenance("source-user"),
|
|
manifest_blob: None,
|
|
spec_blob: None,
|
|
git: None,
|
|
fork_source_ref: None,
|
|
retried_from: None,
|
|
parent_id: None,
|
|
web_url: None,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
event::append_event(&source_store, &source_run_id, &Event::RunSubmitted {
|
|
definition_blob: None,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
append_failed(&source_store, source_run_id, FailureReason::WorkflowError).await;
|
|
|
|
let source_state = source_store.state().await.unwrap();
|
|
assert_eq!(source_state.status, RunStatus::Failed {
|
|
reason: FailureReason::WorkflowError,
|
|
});
|
|
assert_eq!(source_state.spec.target, Some(RunTarget::None {}));
|
|
assert_eq!(source_state.spec.git, None);
|
|
assert_eq!(source_state.spec.source_directory, None);
|
|
|
|
let outcome = retry_run(&store, &RetryRunInput {
|
|
source_run_id,
|
|
new_run_id: RunId::new(),
|
|
provenance: provenance("retry-user"),
|
|
web_url: None,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
let retry_store = store.open_run(&outcome.new_run_id).await.unwrap();
|
|
let retry_events = retry_store.list_events().await.unwrap();
|
|
let retry_state = fabro_store::RunProjection::apply_events(&retry_events).unwrap();
|
|
assert_eq!(retry_events.len(), 2);
|
|
assert_eq!(retry_state.status, RunStatus::Submitted);
|
|
assert_eq!(retry_state.retried_from, Some(source_run_id));
|
|
assert_eq!(retry_state.spec.target, Some(RunTarget::None {}));
|
|
assert_eq!(retry_state.spec.git, None);
|
|
assert_eq!(retry_state.spec.source_directory, None);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn retry_preserves_folder_target_and_source_directory_without_git() {
|
|
let store = memory_store();
|
|
let source_run_id = fixtures::RUN_1;
|
|
let source_store = store.create_run(&source_run_id).await.unwrap();
|
|
let path = "/canonical/local/folder".to_string();
|
|
let target = RunTarget::Folder { path: path.clone() };
|
|
event::append_event(&source_store, &source_run_id, &Event::RunCreated {
|
|
run_id: source_run_id,
|
|
title: Some("Folder target".to_string()),
|
|
settings: serde_json::to_value(WorkflowSettings::default()).unwrap(),
|
|
graph: serde_json::to_value(Graph::new("folder_target_retry")).unwrap(),
|
|
workflow_source: Some("digraph folder_target_retry { start -> exit }".to_string()),
|
|
labels: BTreeMap::new(),
|
|
source_directory: Some(path.clone()),
|
|
workflow_slug: Some("folder-target-retry".to_string()),
|
|
workflow_version_id: Some(test_support::test_workflow_version_id()),
|
|
target: Some(target.clone()),
|
|
automation: None,
|
|
provenance: provenance("source-user"),
|
|
manifest_blob: None,
|
|
spec_blob: None,
|
|
git: None,
|
|
fork_source_ref: None,
|
|
retried_from: None,
|
|
parent_id: None,
|
|
web_url: None,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
event::append_event(&source_store, &source_run_id, &Event::RunSubmitted {
|
|
definition_blob: None,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
append_failed(&source_store, source_run_id, FailureReason::WorkflowError).await;
|
|
|
|
let outcome = retry_run(&store, &RetryRunInput {
|
|
source_run_id,
|
|
new_run_id: RunId::new(),
|
|
provenance: provenance("retry-user"),
|
|
web_url: None,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
let retry_store = store.open_run(&outcome.new_run_id).await.unwrap();
|
|
let retry_state = retry_store.state().await.unwrap();
|
|
assert_eq!(retry_state.status, RunStatus::Submitted);
|
|
assert_eq!(retry_state.spec.target, Some(target));
|
|
assert_eq!(
|
|
retry_state.spec.source_directory.as_deref(),
|
|
Some(path.as_str())
|
|
);
|
|
assert_eq!(retry_state.spec.git, None);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn retry_creates_fresh_run_from_succeeded_source() {
|
|
let store = memory_store();
|
|
let source_run_id = fixtures::RUN_1;
|
|
let source_store = store.create_run(&source_run_id).await.unwrap();
|
|
append_created(&source_store, source_run_id, None, None).await;
|
|
let definition_blob = Some(
|
|
source_store
|
|
.write_blob(br#"{\"definition\":true}"#)
|
|
.await
|
|
.unwrap(),
|
|
);
|
|
event::append_event(&source_store, &source_run_id, &Event::RunSubmitted {
|
|
definition_blob,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
append_succeeded(&source_store, source_run_id).await;
|
|
|
|
let outcome = retry_run(&store, &RetryRunInput {
|
|
source_run_id,
|
|
new_run_id: RunId::new(),
|
|
provenance: provenance("retry-user"),
|
|
web_url: None,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
let retry_store = store.open_run(&outcome.new_run_id).await.unwrap();
|
|
let retry_events = retry_store.list_events().await.unwrap();
|
|
let retry_state = fabro_store::RunProjection::apply_events(&retry_events).unwrap();
|
|
assert_eq!(retry_events.len(), 2);
|
|
assert_eq!(retry_state.status, RunStatus::Submitted);
|
|
assert_eq!(retry_state.retried_from, Some(source_run_id));
|
|
assert_eq!(retry_state.spec.definition_blob, definition_blob);
|
|
assert_eq!(
|
|
source_store.state().await.unwrap().status,
|
|
RunStatus::Succeeded {
|
|
reason: fabro_types::SuccessReason::Completed,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn retry_rejects_active_and_archived_sources() {
|
|
let store = memory_store();
|
|
|
|
let active = fixtures::RUN_2;
|
|
let active_store = store.create_run(&active).await.unwrap();
|
|
append_created(&active_store, active, None, None).await;
|
|
event::append_event(&active_store, &active, &Event::RunSubmitted {
|
|
definition_blob: None,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
event::append_event(&active_store, &active, &Event::RunRunnable {
|
|
source: RunRunnableSource::StartRequested,
|
|
actor: None,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
let archived = fixtures::RUN_3;
|
|
let archived_store = store.create_run(&archived).await.unwrap();
|
|
append_created(&archived_store, archived, None, None).await;
|
|
append_failed(&archived_store, archived, FailureReason::WorkflowError).await;
|
|
event::append_event(&archived_store, &archived, &Event::RunArchived {
|
|
actor: None,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
for run_id in [active, archived] {
|
|
let err = retry_run(&store, &RetryRunInput {
|
|
source_run_id: run_id,
|
|
new_run_id: RunId::new(),
|
|
provenance: provenance("retry-user"),
|
|
web_url: None,
|
|
})
|
|
.await
|
|
.unwrap_err();
|
|
assert!(
|
|
matches!(err, Error::Precondition(_)),
|
|
"unexpected error: {err:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn retry_reports_missing_source() {
|
|
let store = memory_store();
|
|
let err = retry_run(&store, &RetryRunInput {
|
|
source_run_id: fixtures::RUN_1,
|
|
new_run_id: RunId::new(),
|
|
provenance: provenance("retry-user"),
|
|
web_url: None,
|
|
})
|
|
.await
|
|
.unwrap_err();
|
|
|
|
assert!(
|
|
matches!(err, Error::RunNotFound(_)),
|
|
"unexpected error: {err:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dead_status_is_retryable() {
|
|
ensure_retryable(RunStatus::Dead, &fixtures::RUN_1).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn succeeded_status_is_retryable() {
|
|
ensure_retryable(
|
|
RunStatus::Succeeded {
|
|
reason: fabro_types::SuccessReason::Completed,
|
|
},
|
|
&fixtures::RUN_1,
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn cancelled_status_is_retryable() {
|
|
ensure_retryable(
|
|
RunStatus::Failed {
|
|
reason: FailureReason::Cancelled,
|
|
},
|
|
&fixtures::RUN_1,
|
|
)
|
|
.unwrap();
|
|
}
|
|
}
|