Update public CLI diagnostics dry run
This commit is contained in:
parent
b808dad0e5
commit
afaf918973
3 changed files with 522 additions and 13 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"kind": "disasmer-filtered-public-tree",
|
"kind": "disasmer-filtered-public-tree",
|
||||||
"source_commit": "143d109ea0261671012be8ef6b48e86284601eec",
|
"source_commit": "d7700a764f26f3f6963a12f20395e361c66d2a83",
|
||||||
"release_name": "dryrun-143d109ea026",
|
"release_name": "dryrun-d7700a764f26",
|
||||||
"filtered_out": [
|
"filtered_out": [
|
||||||
"private/**",
|
"private/**",
|
||||||
"experiments/**",
|
"experiments/**",
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use clap::{Args, Parser, Subcommand};
|
use clap::{Args, Parser, Subcommand};
|
||||||
use disasmer_core::{
|
use disasmer_core::{
|
||||||
BrowserLoginFlow, BundleIdentityInputs, BundleMetadata, Capability, CliLoginFlow, Digest,
|
diagnose_environment_references, BrowserLoginFlow, BundleIdentityInputs, BundleMetadata,
|
||||||
LimitKind, NodeCapabilities, ProjectModel, ResourceLimits, SelectedInput, SourceProviderKind,
|
Capability, CliLoginFlow, Digest, EnvironmentReference, LimitKind, NodeCapabilities,
|
||||||
SourceProviderManifest,
|
ProjectModel, ResourceLimits, SelectedInput, SourceProviderKind, SourceProviderManifest,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
|
@ -236,6 +236,10 @@ struct ProjectSelectArgs {
|
||||||
struct BundleInspectArgs {
|
struct BundleInspectArgs {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
project: Option<PathBuf>,
|
project: Option<PathBuf>,
|
||||||
|
#[arg(long = "source-provider")]
|
||||||
|
source_provider: Option<String>,
|
||||||
|
#[arg(long = "disable-source-provider")]
|
||||||
|
disabled_source_providers: Vec<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
json: bool,
|
json: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -244,6 +248,10 @@ struct BundleInspectArgs {
|
||||||
struct BuildArgs {
|
struct BuildArgs {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
project: Option<PathBuf>,
|
project: Option<PathBuf>,
|
||||||
|
#[arg(long = "source-provider")]
|
||||||
|
source_provider: Option<String>,
|
||||||
|
#[arg(long = "disable-source-provider")]
|
||||||
|
disabled_source_providers: Vec<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
output: Option<PathBuf>,
|
output: Option<PathBuf>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
|
|
@ -610,9 +618,38 @@ struct BundleInspection {
|
||||||
project: PathBuf,
|
project: PathBuf,
|
||||||
default_source_providers: Vec<SourceProviderKind>,
|
default_source_providers: Vec<SourceProviderKind>,
|
||||||
source_provider_manifest: SourceProviderManifest,
|
source_provider_manifest: SourceProviderManifest,
|
||||||
|
source_provider_statuses: Vec<SourceProviderStatus>,
|
||||||
|
environment_diagnostics: Vec<EnvironmentDiagnosticReport>,
|
||||||
|
pre_schedule_diagnostics: Vec<CliDiagnostic>,
|
||||||
metadata: BundleMetadata,
|
metadata: BundleMetadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||||
|
struct SourceProviderStatus {
|
||||||
|
provider: String,
|
||||||
|
status: String,
|
||||||
|
active: bool,
|
||||||
|
reason: String,
|
||||||
|
coordinator_checkout_required: bool,
|
||||||
|
coordinator_receives_source_bytes_by_default: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||||
|
struct EnvironmentDiagnosticReport {
|
||||||
|
path: String,
|
||||||
|
reference: EnvironmentReference,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||||
|
struct CliDiagnostic {
|
||||||
|
severity: String,
|
||||||
|
category: String,
|
||||||
|
code: String,
|
||||||
|
message: String,
|
||||||
|
next_actions: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||||
struct NodeAttachPlan {
|
struct NodeAttachPlan {
|
||||||
node: String,
|
node: String,
|
||||||
|
|
@ -997,6 +1034,8 @@ fn project_status_report(args: ProjectStatusArgs, cwd: PathBuf) -> Result<Value>
|
||||||
let inspection = bundle_inspection(
|
let inspection = bundle_inspection(
|
||||||
BundleInspectArgs {
|
BundleInspectArgs {
|
||||||
project: Some(cwd.clone()),
|
project: Some(cwd.clone()),
|
||||||
|
source_provider: None,
|
||||||
|
disabled_source_providers: Vec::new(),
|
||||||
json: true,
|
json: true,
|
||||||
},
|
},
|
||||||
cwd.clone(),
|
cwd.clone(),
|
||||||
|
|
@ -1092,17 +1131,45 @@ fn build_report(args: BuildArgs, cwd: PathBuf) -> Result<Value> {
|
||||||
let inspection = bundle_inspection(
|
let inspection = bundle_inspection(
|
||||||
BundleInspectArgs {
|
BundleInspectArgs {
|
||||||
project: args.project.clone(),
|
project: args.project.clone(),
|
||||||
|
source_provider: args.source_provider.clone(),
|
||||||
|
disabled_source_providers: args.disabled_source_providers.clone(),
|
||||||
json: true,
|
json: true,
|
||||||
},
|
},
|
||||||
cwd,
|
cwd,
|
||||||
)?;
|
)?;
|
||||||
let report = json!({
|
let diagnostics = inspection.pre_schedule_diagnostics.clone();
|
||||||
|
let blocking_diagnostic = diagnostics
|
||||||
|
.iter()
|
||||||
|
.find(|diagnostic| diagnostic.severity == "error")
|
||||||
|
.cloned();
|
||||||
|
let status = if blocking_diagnostic.is_some() {
|
||||||
|
"blocked_before_schedule"
|
||||||
|
} else {
|
||||||
|
"built"
|
||||||
|
};
|
||||||
|
let mut report = json!({
|
||||||
"command": "build",
|
"command": "build",
|
||||||
|
"status": status,
|
||||||
"bundle": inspection,
|
"bundle": inspection,
|
||||||
|
"diagnostics": diagnostics,
|
||||||
"contains_full_repository_upload": false,
|
"contains_full_repository_upload": false,
|
||||||
"content_addressed": true,
|
"content_addressed": true,
|
||||||
"debug_metadata_available": true,
|
"debug_metadata_available": true,
|
||||||
|
"scheduled_work": false,
|
||||||
});
|
});
|
||||||
|
if let Some(diagnostic) = blocking_diagnostic.as_ref() {
|
||||||
|
let machine_category = match diagnostic.category.as_str() {
|
||||||
|
"environment" => "environment",
|
||||||
|
"source_provider" | "capability" => "capability",
|
||||||
|
_ => "unknown",
|
||||||
|
};
|
||||||
|
if let Some(report) = report.as_object_mut() {
|
||||||
|
report.insert(
|
||||||
|
"machine_error".to_owned(),
|
||||||
|
cli_error_summary_for_category(machine_category, &diagnostic.message),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Some(output) = args.output {
|
if let Some(output) = args.output {
|
||||||
if let Some(parent) = output.parent() {
|
if let Some(parent) = output.parent() {
|
||||||
std::fs::create_dir_all(parent)?;
|
std::fs::create_dir_all(parent)?;
|
||||||
|
|
@ -1800,6 +1867,31 @@ fn human_report(value: &Value) -> String {
|
||||||
if let Some(artifacts) = value.get("artifacts").and_then(Value::as_array) {
|
if let Some(artifacts) = value.get("artifacts").and_then(Value::as_array) {
|
||||||
lines.push(format!("artifacts: {}", artifacts.len()));
|
lines.push(format!("artifacts: {}", artifacts.len()));
|
||||||
}
|
}
|
||||||
|
if let Some(statuses) = value
|
||||||
|
.get("source_provider_statuses")
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
{
|
||||||
|
push_source_provider_statuses(&mut lines, statuses);
|
||||||
|
}
|
||||||
|
if let Some(bundle_statuses) = value
|
||||||
|
.pointer("/bundle/source_provider_statuses")
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
{
|
||||||
|
push_source_provider_statuses(&mut lines, bundle_statuses);
|
||||||
|
}
|
||||||
|
if let Some(diagnostics) = value
|
||||||
|
.get("pre_schedule_diagnostics")
|
||||||
|
.or_else(|| value.get("diagnostics"))
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
{
|
||||||
|
push_cli_diagnostics(&mut lines, diagnostics);
|
||||||
|
}
|
||||||
|
if let Some(bundle_diagnostics) = value
|
||||||
|
.pointer("/bundle/pre_schedule_diagnostics")
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
{
|
||||||
|
push_cli_diagnostics(&mut lines, bundle_diagnostics);
|
||||||
|
}
|
||||||
if let Some(current_usage) = value.get("current_usage") {
|
if let Some(current_usage) = value.get("current_usage") {
|
||||||
lines.push(format!("quota usage: {}", compact_json(current_usage)));
|
lines.push(format!("quota usage: {}", compact_json(current_usage)));
|
||||||
}
|
}
|
||||||
|
|
@ -2046,6 +2138,45 @@ fn push_machine_error_line(lines: &mut Vec<String>, machine_error: Option<&Value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn push_source_provider_statuses(lines: &mut Vec<String>, statuses: &[Value]) {
|
||||||
|
let entries = statuses
|
||||||
|
.iter()
|
||||||
|
.filter_map(|status| {
|
||||||
|
let provider = status.get("provider").and_then(Value::as_str)?;
|
||||||
|
let state = status.get("status").and_then(Value::as_str)?;
|
||||||
|
let active = status
|
||||||
|
.get("active")
|
||||||
|
.and_then(Value::as_bool)
|
||||||
|
.unwrap_or(false);
|
||||||
|
Some(format!(
|
||||||
|
"{provider}={state}{}",
|
||||||
|
if active { "(active)" } else { "" }
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if !entries.is_empty() {
|
||||||
|
lines.push(format!("source providers: {}", entries.join(", ")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_cli_diagnostics(lines: &mut Vec<String>, diagnostics: &[Value]) {
|
||||||
|
if diagnostics.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lines.push(format!("diagnostics: {}", diagnostics.len()));
|
||||||
|
for diagnostic in diagnostics.iter().take(3) {
|
||||||
|
let severity = diagnostic
|
||||||
|
.get("severity")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.unwrap_or("info");
|
||||||
|
let message = diagnostic
|
||||||
|
.get("message")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.unwrap_or("diagnostic");
|
||||||
|
lines.push(format!("diagnostic {severity}: {message}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn compact_json(value: &Value) -> String {
|
fn compact_json(value: &Value) -> String {
|
||||||
serde_json::to_string(value).unwrap_or_else(|_| "<unprintable>".to_owned())
|
serde_json::to_string(value).unwrap_or_else(|_| "<unprintable>".to_owned())
|
||||||
}
|
}
|
||||||
|
|
@ -3673,20 +3804,35 @@ fn bundle_inspection(args: BundleInspectArgs, cwd: PathBuf) -> Result<BundleInsp
|
||||||
let project = args.project.unwrap_or(cwd);
|
let project = args.project.unwrap_or(cwd);
|
||||||
let model = ProjectModel::discover_without_config(&project)?;
|
let model = ProjectModel::discover_without_config(&project)?;
|
||||||
let selected_inputs = discover_selected_inputs(&project)?;
|
let selected_inputs = discover_selected_inputs(&project)?;
|
||||||
let source_provider_manifest = source_provider_manifest(&project);
|
let provider_selection = source_provider_selection(
|
||||||
|
&project,
|
||||||
|
args.source_provider.as_deref(),
|
||||||
|
&args.disabled_source_providers,
|
||||||
|
);
|
||||||
|
let source_provider_manifest = source_provider_manifest(&provider_selection.active_provider);
|
||||||
source_provider_manifest.validate_public_mvp()?;
|
source_provider_manifest.validate_public_mvp()?;
|
||||||
|
let environment_diagnostics =
|
||||||
|
environment_diagnostics_for_inputs(&project, &selected_inputs, &model.environments)?;
|
||||||
let identity_inputs = BundleIdentityInputs {
|
let identity_inputs = BundleIdentityInputs {
|
||||||
wasm_code: wasm_source_proxy_digest(&selected_inputs),
|
wasm_code: wasm_source_proxy_digest(&selected_inputs),
|
||||||
task_abi: task_abi_digest(&model),
|
task_abi: task_abi_digest(&model),
|
||||||
environments: model.environments,
|
environments: model.environments.clone(),
|
||||||
source_provider_manifest: source_provider_manifest.digest.clone(),
|
source_provider_manifest: source_provider_manifest.digest.clone(),
|
||||||
selected_inputs,
|
selected_inputs,
|
||||||
};
|
};
|
||||||
|
let pre_schedule_diagnostics = pre_schedule_diagnostics(
|
||||||
|
&provider_selection.statuses,
|
||||||
|
&environment_diagnostics,
|
||||||
|
&model.environments,
|
||||||
|
);
|
||||||
|
|
||||||
Ok(BundleInspection {
|
Ok(BundleInspection {
|
||||||
project,
|
project,
|
||||||
default_source_providers: vec![SourceProviderKind::Filesystem, SourceProviderKind::Git],
|
default_source_providers: vec![SourceProviderKind::Filesystem, SourceProviderKind::Git],
|
||||||
source_provider_manifest,
|
source_provider_manifest,
|
||||||
|
source_provider_statuses: provider_selection.statuses,
|
||||||
|
environment_diagnostics,
|
||||||
|
pre_schedule_diagnostics,
|
||||||
metadata: identity_inputs.inspectable_metadata(),
|
metadata: identity_inputs.inspectable_metadata(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -3717,18 +3863,220 @@ fn discover_selected_inputs(project: &Path) -> Result<Vec<SelectedInput>> {
|
||||||
Ok(inputs)
|
Ok(inputs)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn source_provider_manifest(project: &Path) -> SourceProviderManifest {
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
let kind = if project.join(".git").exists() {
|
struct SourceProviderSelection {
|
||||||
|
active_provider: SourceProviderKind,
|
||||||
|
statuses: Vec<SourceProviderStatus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source_provider_selection(
|
||||||
|
project: &Path,
|
||||||
|
requested_provider: Option<&str>,
|
||||||
|
disabled_providers: &[String],
|
||||||
|
) -> SourceProviderSelection {
|
||||||
|
let has_git_checkout = project.join(".git").exists();
|
||||||
|
let disabled = disabled_providers
|
||||||
|
.iter()
|
||||||
|
.map(|provider| provider.trim().to_ascii_lowercase())
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
let requested = requested_provider.map(source_provider_kind_from_id);
|
||||||
|
let active_provider = requested.clone().unwrap_or_else(|| {
|
||||||
|
if has_git_checkout && !disabled.contains("git") {
|
||||||
SourceProviderKind::Git
|
SourceProviderKind::Git
|
||||||
} else {
|
} else {
|
||||||
SourceProviderKind::Filesystem
|
SourceProviderKind::Filesystem
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let active_provider_id = active_provider.provider_id().to_owned();
|
||||||
|
let mut statuses = vec![
|
||||||
|
builtin_source_provider_status(
|
||||||
|
SourceProviderKind::Filesystem,
|
||||||
|
true,
|
||||||
|
disabled.contains("filesystem"),
|
||||||
|
active_provider_id == "filesystem",
|
||||||
|
),
|
||||||
|
builtin_source_provider_status(
|
||||||
|
SourceProviderKind::Git,
|
||||||
|
has_git_checkout,
|
||||||
|
disabled.contains("git"),
|
||||||
|
active_provider_id == "git",
|
||||||
|
),
|
||||||
|
SourceProviderStatus {
|
||||||
|
provider: "custom".to_owned(),
|
||||||
|
status: "disabled".to_owned(),
|
||||||
|
active: false,
|
||||||
|
reason: "no custom source-provider module was configured for this project".to_owned(),
|
||||||
|
coordinator_checkout_required: false,
|
||||||
|
coordinator_receives_source_bytes_by_default: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if let Some(SourceProviderKind::Custom(provider)) = requested {
|
||||||
|
statuses.push(SourceProviderStatus {
|
||||||
|
provider,
|
||||||
|
status: "unsupported".to_owned(),
|
||||||
|
active: true,
|
||||||
|
reason: "custom source-provider modules must be supplied by public plugin/API code before use".to_owned(),
|
||||||
|
coordinator_checkout_required: false,
|
||||||
|
coordinator_receives_source_bytes_by_default: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
SourceProviderSelection {
|
||||||
|
active_provider,
|
||||||
|
statuses,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source_provider_kind_from_id(provider: &str) -> SourceProviderKind {
|
||||||
|
match provider.trim().to_ascii_lowercase().as_str() {
|
||||||
|
"filesystem" => SourceProviderKind::Filesystem,
|
||||||
|
"git" => SourceProviderKind::Git,
|
||||||
|
other => SourceProviderKind::Custom(other.to_owned()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn builtin_source_provider_status(
|
||||||
|
kind: SourceProviderKind,
|
||||||
|
available: bool,
|
||||||
|
disabled: bool,
|
||||||
|
active: bool,
|
||||||
|
) -> SourceProviderStatus {
|
||||||
|
let provider = kind.provider_id().to_owned();
|
||||||
|
let (status, reason) = if disabled {
|
||||||
|
(
|
||||||
|
"disabled",
|
||||||
|
format!("source provider `{provider}` was disabled by CLI override"),
|
||||||
|
)
|
||||||
|
} else if available {
|
||||||
|
(
|
||||||
|
"enabled",
|
||||||
|
format!("source provider `{provider}` is available in this checkout"),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
"missing",
|
||||||
|
format!("source provider `{provider}` is not available for this checkout"),
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
SourceProviderStatus {
|
||||||
|
provider,
|
||||||
|
status: status.to_owned(),
|
||||||
|
active,
|
||||||
|
reason,
|
||||||
|
coordinator_checkout_required: false,
|
||||||
|
coordinator_receives_source_bytes_by_default: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source_provider_manifest(kind: &SourceProviderKind) -> SourceProviderManifest {
|
||||||
SourceProviderManifest::local_first(
|
SourceProviderManifest::local_first(
|
||||||
kind,
|
kind.clone(),
|
||||||
"default source provider manifest; snapshot creation can be scheduled as a node task",
|
"default source provider manifest; snapshot creation can be scheduled as a node task",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn environment_diagnostics_for_inputs(
|
||||||
|
project: &Path,
|
||||||
|
selected_inputs: &[SelectedInput],
|
||||||
|
environments: &[disasmer_core::EnvironmentResource],
|
||||||
|
) -> Result<Vec<EnvironmentDiagnosticReport>> {
|
||||||
|
let mut diagnostics = Vec::new();
|
||||||
|
for input in selected_inputs {
|
||||||
|
if !input.path.ends_with(".rs") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let source = std::fs::read_to_string(project.join(&input.path))
|
||||||
|
.with_context(|| format!("failed to read {}", project.join(&input.path).display()))?;
|
||||||
|
diagnostics.extend(
|
||||||
|
diagnose_environment_references(&source, environments)
|
||||||
|
.into_iter()
|
||||||
|
.map(|diagnostic| EnvironmentDiagnosticReport {
|
||||||
|
path: input.path.clone(),
|
||||||
|
reference: diagnostic.reference,
|
||||||
|
message: diagnostic.message,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(diagnostics)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pre_schedule_diagnostics(
|
||||||
|
source_provider_statuses: &[SourceProviderStatus],
|
||||||
|
environment_diagnostics: &[EnvironmentDiagnosticReport],
|
||||||
|
environments: &[disasmer_core::EnvironmentResource],
|
||||||
|
) -> Vec<CliDiagnostic> {
|
||||||
|
let mut diagnostics = Vec::new();
|
||||||
|
diagnostics.extend(
|
||||||
|
environment_diagnostics
|
||||||
|
.iter()
|
||||||
|
.map(|diagnostic| CliDiagnostic {
|
||||||
|
severity: "error".to_owned(),
|
||||||
|
category: "environment".to_owned(),
|
||||||
|
code: "missing_environment".to_owned(),
|
||||||
|
message: format!("{} at {}", diagnostic.message, diagnostic.path),
|
||||||
|
next_actions: vec![
|
||||||
|
"create the missing envs/<name>/Containerfile or envs/<name>/Dockerfile"
|
||||||
|
.to_owned(),
|
||||||
|
"rerun disasmer inspect".to_owned(),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
diagnostics.extend(
|
||||||
|
source_provider_statuses
|
||||||
|
.iter()
|
||||||
|
.filter(|status| {
|
||||||
|
status.active
|
||||||
|
&& matches!(
|
||||||
|
status.status.as_str(),
|
||||||
|
"missing" | "disabled" | "unsupported"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map(|status| CliDiagnostic {
|
||||||
|
severity: "error".to_owned(),
|
||||||
|
category: "source_provider".to_owned(),
|
||||||
|
code: format!("source_provider_{}", status.status),
|
||||||
|
message: format!(
|
||||||
|
"active source provider `{}` is {}: {}",
|
||||||
|
status.provider, status.status, status.reason
|
||||||
|
),
|
||||||
|
next_actions: vec![
|
||||||
|
"choose an available source provider with --source-provider".to_owned(),
|
||||||
|
"rerun disasmer inspect --json".to_owned(),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
for environment in environments {
|
||||||
|
if environment.requirements.capabilities.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut capabilities = environment
|
||||||
|
.requirements
|
||||||
|
.capabilities
|
||||||
|
.iter()
|
||||||
|
.map(|capability| format!("{capability:?}"))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
capabilities.sort();
|
||||||
|
diagnostics.push(CliDiagnostic {
|
||||||
|
severity: "info".to_owned(),
|
||||||
|
category: "capability".to_owned(),
|
||||||
|
code: "environment_capability_requirements".to_owned(),
|
||||||
|
message: format!(
|
||||||
|
"environment `{}` requires node capabilities: {}",
|
||||||
|
environment.name,
|
||||||
|
capabilities.join(", ")
|
||||||
|
),
|
||||||
|
next_actions: vec![
|
||||||
|
"attach a node that reports these capabilities before scheduling work".to_owned(),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
fn wasm_source_proxy_digest(selected_inputs: &[SelectedInput]) -> Digest {
|
fn wasm_source_proxy_digest(selected_inputs: &[SelectedInput]) -> Digest {
|
||||||
let mut parts = vec![b"wasm-source-proxy:v1".to_vec()];
|
let mut parts = vec![b"wasm-source-proxy:v1".to_vec()];
|
||||||
for input in selected_inputs {
|
for input in selected_inputs {
|
||||||
|
|
@ -4975,6 +5323,13 @@ mod tests {
|
||||||
assert!(inspection
|
assert!(inspection
|
||||||
.default_source_providers
|
.default_source_providers
|
||||||
.contains(&SourceProviderKind::Git));
|
.contains(&SourceProviderKind::Git));
|
||||||
|
assert!(inspection.source_provider_statuses.iter().any(|status| {
|
||||||
|
status.provider == "filesystem" && status.status == "enabled" && status.active
|
||||||
|
}));
|
||||||
|
assert!(inspection
|
||||||
|
.source_provider_statuses
|
||||||
|
.iter()
|
||||||
|
.any(|status| status.provider == "git" && status.status == "missing"));
|
||||||
assert!(inspection
|
assert!(inspection
|
||||||
.source_provider_manifest
|
.source_provider_manifest
|
||||||
.description
|
.description
|
||||||
|
|
@ -5002,6 +5357,101 @@ mod tests {
|
||||||
.selected_inputs
|
.selected_inputs
|
||||||
.iter()
|
.iter()
|
||||||
.any(|input| input.path == "src/main.rs"));
|
.any(|input| input.path == "src/main.rs"));
|
||||||
|
assert!(inspection.environment_diagnostics.is_empty());
|
||||||
|
assert!(inspection
|
||||||
|
.pre_schedule_diagnostics
|
||||||
|
.iter()
|
||||||
|
.any(|diagnostic| diagnostic.category == "capability"
|
||||||
|
&& diagnostic.message.contains("environment `linux`")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bundle_inspect_reports_missing_environment_references_before_schedule() {
|
||||||
|
let temp = tempfile::tempdir().unwrap();
|
||||||
|
fs::create_dir_all(temp.path().join("src")).unwrap();
|
||||||
|
fs::write(temp.path().join("Cargo.toml"), "[package]\nname='demo'\n").unwrap();
|
||||||
|
fs::write(
|
||||||
|
temp.path().join("src/main.rs"),
|
||||||
|
"fn main() { let _target = env!(\"linux\"); }\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let inspection = bundle_inspection(
|
||||||
|
BundleInspectArgs {
|
||||||
|
project: Some(temp.path().to_path_buf()),
|
||||||
|
source_provider: None,
|
||||||
|
disabled_source_providers: Vec::new(),
|
||||||
|
json: false,
|
||||||
|
},
|
||||||
|
PathBuf::from("/unused"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(inspection.environment_diagnostics.len(), 1);
|
||||||
|
assert_eq!(inspection.environment_diagnostics[0].path, "src/main.rs");
|
||||||
|
assert_eq!(
|
||||||
|
inspection.environment_diagnostics[0].reference.name,
|
||||||
|
"linux"
|
||||||
|
);
|
||||||
|
assert!(inspection.environment_diagnostics[0]
|
||||||
|
.message
|
||||||
|
.contains("missing Disasmer environment `linux`"));
|
||||||
|
assert!(inspection
|
||||||
|
.pre_schedule_diagnostics
|
||||||
|
.iter()
|
||||||
|
.any(|diagnostic| {
|
||||||
|
diagnostic.severity == "error"
|
||||||
|
&& diagnostic.category == "environment"
|
||||||
|
&& diagnostic.code == "missing_environment"
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bundle_inspect_reports_source_provider_overrides_before_schedule() {
|
||||||
|
let temp = tempfile::tempdir().unwrap();
|
||||||
|
fs::write(temp.path().join("Cargo.toml"), "[package]\nname='demo'\n").unwrap();
|
||||||
|
|
||||||
|
let missing_git = bundle_inspection(
|
||||||
|
BundleInspectArgs {
|
||||||
|
project: Some(temp.path().to_path_buf()),
|
||||||
|
source_provider: Some("git".to_owned()),
|
||||||
|
disabled_source_providers: Vec::new(),
|
||||||
|
json: false,
|
||||||
|
},
|
||||||
|
PathBuf::from("/unused"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(missing_git.source_provider_statuses.iter().any(|status| {
|
||||||
|
status.provider == "git" && status.status == "missing" && status.active
|
||||||
|
}));
|
||||||
|
assert!(missing_git
|
||||||
|
.pre_schedule_diagnostics
|
||||||
|
.iter()
|
||||||
|
.any(|diagnostic| {
|
||||||
|
diagnostic.category == "source_provider"
|
||||||
|
&& diagnostic.code == "source_provider_missing"
|
||||||
|
}));
|
||||||
|
|
||||||
|
let unsupported = bundle_inspection(
|
||||||
|
BundleInspectArgs {
|
||||||
|
project: Some(temp.path().to_path_buf()),
|
||||||
|
source_provider: Some("custom-lfs".to_owned()),
|
||||||
|
disabled_source_providers: Vec::new(),
|
||||||
|
json: false,
|
||||||
|
},
|
||||||
|
PathBuf::from("/unused"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(unsupported.source_provider_statuses.iter().any(|status| {
|
||||||
|
status.provider == "custom-lfs" && status.status == "unsupported" && status.active
|
||||||
|
}));
|
||||||
|
assert!(unsupported
|
||||||
|
.pre_schedule_diagnostics
|
||||||
|
.iter()
|
||||||
|
.any(|diagnostic| {
|
||||||
|
diagnostic.category == "source_provider"
|
||||||
|
&& diagnostic.code == "source_provider_unsupported"
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -5012,6 +5462,8 @@ mod tests {
|
||||||
let first = bundle_inspection(
|
let first = bundle_inspection(
|
||||||
BundleInspectArgs {
|
BundleInspectArgs {
|
||||||
project: Some(temp.path().to_path_buf()),
|
project: Some(temp.path().to_path_buf()),
|
||||||
|
source_provider: None,
|
||||||
|
disabled_source_providers: Vec::new(),
|
||||||
json: false,
|
json: false,
|
||||||
},
|
},
|
||||||
PathBuf::from("/unused"),
|
PathBuf::from("/unused"),
|
||||||
|
|
@ -5025,6 +5477,8 @@ mod tests {
|
||||||
let second = bundle_inspection(
|
let second = bundle_inspection(
|
||||||
BundleInspectArgs {
|
BundleInspectArgs {
|
||||||
project: Some(temp.path().to_path_buf()),
|
project: Some(temp.path().to_path_buf()),
|
||||||
|
source_provider: None,
|
||||||
|
disabled_source_providers: Vec::new(),
|
||||||
json: false,
|
json: false,
|
||||||
},
|
},
|
||||||
PathBuf::from("/unused"),
|
PathBuf::from("/unused"),
|
||||||
|
|
@ -5044,6 +5498,8 @@ mod tests {
|
||||||
let first = bundle_inspection(
|
let first = bundle_inspection(
|
||||||
BundleInspectArgs {
|
BundleInspectArgs {
|
||||||
project: Some(first.path().to_path_buf()),
|
project: Some(first.path().to_path_buf()),
|
||||||
|
source_provider: None,
|
||||||
|
disabled_source_providers: Vec::new(),
|
||||||
json: false,
|
json: false,
|
||||||
},
|
},
|
||||||
PathBuf::from("/unused"),
|
PathBuf::from("/unused"),
|
||||||
|
|
@ -5052,6 +5508,8 @@ mod tests {
|
||||||
let second = bundle_inspection(
|
let second = bundle_inspection(
|
||||||
BundleInspectArgs {
|
BundleInspectArgs {
|
||||||
project: Some(second.path().to_path_buf()),
|
project: Some(second.path().to_path_buf()),
|
||||||
|
source_provider: None,
|
||||||
|
disabled_source_providers: Vec::new(),
|
||||||
json: false,
|
json: false,
|
||||||
},
|
},
|
||||||
PathBuf::from("/unused"),
|
PathBuf::from("/unused"),
|
||||||
|
|
@ -6153,6 +6611,8 @@ mod tests {
|
||||||
let report = build_report(
|
let report = build_report(
|
||||||
BuildArgs {
|
BuildArgs {
|
||||||
project: Some(temp.path().to_path_buf()),
|
project: Some(temp.path().to_path_buf()),
|
||||||
|
source_provider: None,
|
||||||
|
disabled_source_providers: Vec::new(),
|
||||||
output: None,
|
output: None,
|
||||||
json: false,
|
json: false,
|
||||||
},
|
},
|
||||||
|
|
@ -6167,6 +6627,37 @@ mod tests {
|
||||||
.as_str()
|
.as_str()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.starts_with("sha256:"));
|
.starts_with("sha256:"));
|
||||||
|
assert_eq!(report["status"], "built");
|
||||||
|
assert_eq!(report["scheduled_work"], false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_blocks_before_schedule_on_missing_environment_reference() {
|
||||||
|
let temp = tempfile::tempdir().unwrap();
|
||||||
|
fs::create_dir_all(temp.path().join("src")).unwrap();
|
||||||
|
fs::write(temp.path().join("Cargo.toml"), "[package]\nname='demo'\n").unwrap();
|
||||||
|
fs::write(
|
||||||
|
temp.path().join("src/main.rs"),
|
||||||
|
"fn main() { let _target = env!(\"linux\"); }\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let report = build_report(
|
||||||
|
BuildArgs {
|
||||||
|
project: Some(temp.path().to_path_buf()),
|
||||||
|
source_provider: None,
|
||||||
|
disabled_source_providers: Vec::new(),
|
||||||
|
output: None,
|
||||||
|
json: false,
|
||||||
|
},
|
||||||
|
PathBuf::from("/unused"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(report["status"], "blocked_before_schedule");
|
||||||
|
assert_eq!(report["scheduled_work"], false);
|
||||||
|
assert_eq!(report["machine_error"]["category"], "environment");
|
||||||
|
assert_eq!(report["diagnostics"][0]["code"], "missing_environment");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,9 @@ for (const [name, pattern] of [
|
||||||
["process control report coverage", /fn process_restart_and_cancel_reports_expose_control_boundaries\(\)/],
|
["process control report coverage", /fn process_restart_and_cancel_reports_expose_control_boundaries\(\)/],
|
||||||
["task restart report coverage", /fn task_restart_reports_clean_boundary_requirements\(\)/],
|
["task restart report coverage", /fn task_restart_reports_clean_boundary_requirements\(\)/],
|
||||||
["build no full repo upload coverage", /fn build_command_reuses_bundle_inspection_without_full_repo_upload\(\)/],
|
["build no full repo upload coverage", /fn build_command_reuses_bundle_inspection_without_full_repo_upload\(\)/],
|
||||||
|
["inspect missing environment coverage", /fn bundle_inspect_reports_missing_environment_references_before_schedule\(\)/],
|
||||||
|
["inspect source provider override coverage", /fn bundle_inspect_reports_source_provider_overrides_before_schedule\(\)/],
|
||||||
|
["build missing environment gate coverage", /fn build_blocks_before_schedule_on_missing_environment_reference\(\)/],
|
||||||
["safe coordinator-required plans", /fn node_enroll_and_process_commands_have_safe_plan_without_coordinator\(\)/],
|
["safe coordinator-required plans", /fn node_enroll_and_process_commands_have_safe_plan_without_coordinator\(\)/],
|
||||||
]) {
|
]) {
|
||||||
expect(cli, name, pattern);
|
expect(cli, name, pattern);
|
||||||
|
|
@ -257,6 +260,21 @@ expect(
|
||||||
"CLI parses dangerous capability overrides",
|
"CLI parses dangerous capability overrides",
|
||||||
/"host-filesystem"[\s\S]*Capability::HostFilesystem[\s\S]*"network"[\s\S]*Capability::Network[\s\S]*"secrets"[\s\S]*Capability::Secrets/
|
/"host-filesystem"[\s\S]*Capability::HostFilesystem[\s\S]*"network"[\s\S]*Capability::Network[\s\S]*"secrets"[\s\S]*Capability::Secrets/
|
||||||
);
|
);
|
||||||
|
expect(
|
||||||
|
cli,
|
||||||
|
"CLI exposes source provider diagnostics",
|
||||||
|
/source_provider_statuses[\s\S]*source_provider_selection[\s\S]*source_provider_unsupported/
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
cli,
|
||||||
|
"CLI exposes environment pre-schedule diagnostics",
|
||||||
|
/environment_diagnostics_for_inputs[\s\S]*diagnose_environment_references[\s\S]*missing_environment/
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
cli,
|
||||||
|
"CLI blocks build before scheduling unsafe inputs",
|
||||||
|
/blocked_before_schedule[\s\S]*scheduled_work[\s\S]*false/
|
||||||
|
);
|
||||||
|
|
||||||
for (const [name, pattern] of [
|
for (const [name, pattern] of [
|
||||||
["agent --json flag", /struct AgentEnrollArgs[\s\S]*#\[arg\(long\)\]\s*json: bool/],
|
["agent --json flag", /struct AgentEnrollArgs[\s\S]*#\[arg\(long\)\]\s*json: bool/],
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue