Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions conformance/sdk-lifecycle-v1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"schemaVersion": 1,
"engineApiVersion": "1.2.0",
"vectors": [
{
"id": "start-rejected",
"callback": "start",
"guestError": "rejected",
"message": "conformance start rejection",
"expectedHostOutcome": "rejected"
},
{
"id": "event-invalid-input",
"callback": "event",
"guestError": "invalid-input",
"message": "conformance event rejection",
"expectedHostOutcome": "rejected"
},
{
"id": "event-internal",
"callback": "event",
"guestError": "internal",
"message": null,
"expectedHostOutcome": "rejected"
}
]
}
127 changes: 127 additions & 0 deletions crates/floatile-cli/src/conformance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
//! Versioned SDK conformance kit exposed to language adapters and CI.

use std::collections::BTreeSet;

use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::OUTPUT_SCHEMA_VERSION;

pub const LIFECYCLE_SUITE_JSON: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../conformance/sdk-lifecycle-v1.json"
));

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LifecycleSuite {
pub schema_version: u32,
pub engine_api_version: String,
pub vectors: Vec<LifecycleVector>,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LifecycleVector {
pub id: String,
pub callback: String,
pub guest_error: String,
pub message: Option<String>,
pub expected_host_outcome: String,
}

#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ConformanceReport {
pub schema_version: u32,
pub status: &'static str,
pub suite: &'static str,
pub contract: LifecycleSuite,
pub warnings: Vec<crate::CommandWarning>,
}

#[derive(Debug, Error)]
pub enum ConformanceError {
#[error("内置 conformance JSON 无效")]
InvalidJson,
#[error("不支持的 conformance schemaVersion: {0}")]
UnsupportedSchema(u32),
#[error("conformance 向量无效: {0}")]
InvalidVector(String),
}

impl ConformanceError {
pub fn code(&self) -> &'static str {
match self {
Self::InvalidJson => "FCONF_JSON",
Self::UnsupportedSchema(_) => "FCONF_SCHEMA_VERSION",
Self::InvalidVector(_) => "FCONF_VECTOR",
}
}
}

pub fn lifecycle_report() -> Result<ConformanceReport, ConformanceError> {
let contract: LifecycleSuite =
serde_json::from_str(LIFECYCLE_SUITE_JSON).map_err(|_| ConformanceError::InvalidJson)?;
validate(&contract)?;
Ok(ConformanceReport {
schema_version: OUTPUT_SCHEMA_VERSION,
status: "ok",
suite: "sdk-lifecycle-v1",
contract,
warnings: Vec::new(),
})
}

fn validate(suite: &LifecycleSuite) -> Result<(), ConformanceError> {
if suite.schema_version != 1 {
return Err(ConformanceError::UnsupportedSchema(suite.schema_version));
}
let mut ids = BTreeSet::new();
for vector in &suite.vectors {
if !ids.insert(vector.id.as_str()) {
return Err(ConformanceError::InvalidVector("vector id 重复".to_owned()));
}
if !matches!(vector.callback.as_str(), "start" | "event") {
return Err(ConformanceError::InvalidVector(
"callback 不受支持".to_owned(),
));
}
if !matches!(
vector.guest_error.as_str(),
"invalid-input" | "rejected" | "internal"
) {
return Err(ConformanceError::InvalidVector(
"guestError 不受支持".to_owned(),
));
}
if vector.expected_host_outcome != "rejected" {
return Err(ConformanceError::InvalidVector(
"expectedHostOutcome 不受支持".to_owned(),
));
}
let requires_message = vector.guest_error != "internal";
if requires_message != vector.message.is_some() {
return Err(ConformanceError::InvalidVector(
"message 与 guestError 不匹配".to_owned(),
));
}
}
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn embedded_lifecycle_suite_is_valid_and_versioned() -> Result<(), ConformanceError> {
let report = lifecycle_report()?;
assert_eq!(report.schema_version, OUTPUT_SCHEMA_VERSION);
assert_eq!(report.suite, "sdk-lifecycle-v1");
assert_eq!(report.contract.schema_version, 1);
assert_eq!(report.contract.engine_api_version, "1.2.0");
assert_eq!(report.contract.vectors.len(), 3);
Ok(())
}
}
2 changes: 2 additions & 0 deletions crates/floatile-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

pub mod build;
pub mod check;
pub mod conformance;
pub mod dev;
pub mod inspect;
pub mod install;
Expand All @@ -18,6 +19,7 @@ pub mod test;

pub use build::{BuildError, build_project, package};
pub use check::{CheckError, CheckPhases, CheckReport, CheckWarning, check_project};
pub use conformance::{ConformanceError, ConformanceReport, lifecycle_report};
pub use dev::{BuildStatus, build_once, dev_loop, ensure_project};
pub use inspect::{InspectError, InspectReport, inspect_package, inspect_package_bytes};
pub use install::{InstallError, InstalledPackage, install_dir, install_package};
Expand Down
39 changes: 36 additions & 3 deletions crates/floatile-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ use std::process::ExitCode;
use std::time::{Duration, SystemTime, UNIX_EPOCH};

use floatile_cli::{
CommandErrorReport, build, check, dev, inspect, install, instance, package, preview, project,
run, test,
CommandErrorReport, build, check, conformance, dev, inspect, install, instance, package,
preview, project, run, test,
};
use floatile_core::{InstanceConfig, InstanceDesiredState, InstanceId};

fn main() -> ExitCode {
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 {
eprintln!(
"用法: floatile <new|validate|check|inspect|build|install|instance|dev|test|preview|run|schema> [参数]"
"用法: floatile <new|validate|check|inspect|build|install|instance|dev|test|preview|run|schema|conformance> [参数]"
);
return ExitCode::from(2);
}
Expand All @@ -31,13 +31,46 @@ fn main() -> ExitCode {
"preview" => cmd_preview(&args[2..]),
"run" => cmd_run(&args[2..]),
"schema" => cmd_schema(&args[2..]),
"conformance" => cmd_conformance(&args[2..]),
other => {
eprintln!("未知命令: {other}");
ExitCode::from(2)
}
}
}

fn cmd_conformance(args: &[String]) -> ExitCode {
let json = args.iter().any(|argument| argument == "--json");
if let Err(detail) = author_positionals(args, &[], &[], 0) {
return render_basic_error("FCONF_ARGUMENT", &detail, json, true);
}
match conformance::lifecycle_report() {
Ok(report) => {
if json {
println!("{}", serialize_json(&report));
} else {
println!(
"conformance: PASS suite={} engine={} vectors={}",
report.suite,
report.contract.engine_api_version,
report.contract.vectors.len()
);
for vector in report.contract.vectors {
println!(
" {} callback={} guest-error={} host={}",
vector.id,
vector.callback,
vector.guest_error,
vector.expected_host_outcome
);
}
}
ExitCode::SUCCESS
}
Err(error) => render_basic_error(error.code(), &error.to_string(), json, false),
}
}

fn cmd_check(args: &[String]) -> ExitCode {
let json = args.iter().any(|argument| argument == "--json");
let deny_warnings = args.iter().any(|argument| argument == "--deny-warnings");
Expand Down
8 changes: 5 additions & 3 deletions crates/floatile-cli/src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ floatile-sdk = "0.1"
serde = { version = "1", features = ["derive"] }
"#
.to_owned();
let lib_rs = r#"use floatile_sdk::{Context, LogLevel, State, Widget, WidgetEvent, view};
let lib_rs = r#"use floatile_sdk::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, State)]
Expand All @@ -279,12 +279,14 @@ impl Widget for MyWidget {
view::column(vec![view::text_bind("$.message")])
}

fn start(&mut self, ctx: &mut Context<Self>) {
fn start(&mut self, ctx: &mut Context<Self>) -> WidgetResult {
let _ = ctx.timer().schedule(1000);
Ok(())
}

fn event(&mut self, _event: WidgetEvent, ctx: &mut Context<Self>) {
fn event(&mut self, _event: WidgetEvent, ctx: &mut Context<Self>) -> WidgetResult {
let _ = ctx.log(LogLevel::Info, "event received");
Ok(())
}
}

Expand Down
37 changes: 37 additions & 0 deletions crates/floatile-cli/tests/conformance_cmd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#![allow(clippy::unwrap_used)]

use std::process::Command;

#[test]
fn conformance_command_exposes_the_versioned_lifecycle_suite() {
let output = Command::new(env!("CARGO_BIN_EXE_floatile"))
.args([
"conformance",
"--json",
"--no-interactive",
"--deny-warnings",
])
.output()
.unwrap();
assert!(output.status.success(), "stderr: {:?}", output.stderr);
assert!(output.stderr.is_empty());
let report: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(report["schemaVersion"], 1);
assert_eq!(report["status"], "ok");
assert_eq!(report["suite"], "sdk-lifecycle-v1");
assert_eq!(report["contract"]["engineApiVersion"], "1.2.0");
assert_eq!(report["contract"]["vectors"].as_array().unwrap().len(), 3);
assert_eq!(report["warnings"], serde_json::json!([]));
}

#[test]
fn conformance_command_rejects_unknown_arguments() {
let output = Command::new(env!("CARGO_BIN_EXE_floatile"))
.args(["conformance", "unexpected", "--json"])
.output()
.unwrap();
assert_eq!(output.status.code(), Some(2));
assert!(output.stdout.is_empty());
let report: serde_json::Value = serde_json::from_slice(&output.stderr).unwrap();
assert_eq!(report["code"], "FCONF_ARGUMENT");
}
1 change: 1 addition & 0 deletions crates/floatile-runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ wasmtime-wasi = { workspace = true }
[dev-dependencies]
floatile-platform = { workspace = true }
floatile-store = { workspace = true }
serde = { workspace = true }
Loading