From 9e4fd37824c81d81ea9f3a1744db68b60430c014 Mon Sep 17 00:00:00 2001 From: Kinflou Date: Wed, 2 Sep 2026 02:04:53 +0800 Subject: [PATCH 1/2] =?UTF-8?q?test:=20end-to-end=20=E2=80=94=20a=20genera?= =?UTF-8?q?ted=20protocol=20crate=20runs=20a=20real=20round-trip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The whole pipeline for real: `tests/fixtures/chat_project/src/chat.ids` (a struct, an error, a protocol with a throwing request/response call and a one-way call) → `comline generate --mode lib` via the actual binary → the emitted crate compiled against comline-runtime → a hand-written driver runs ChatClient ⇆ ChatDispatcher over `duplex()`: a request/response call, a raised `Rejected` reaching the client typed, and a one-way `note`. Surfaced and fixed a real gap: cli pinned `comline-codegen-rust` at the pre-protocol-codegen rev (`a70cafcd`), so `comline generate` still emitted the old bare-trait shape. Bumped to comline-rust main (`1191e76`, PRs 2-4) and `comline-codegen-typescript` to its merged rev. tests/cli/end_to_end.rs is slow (fetches + builds comline-runtime, ~25s) and needs network. util.rs gains `copy_fixture(name, temp)`. --- Cargo.lock | 4 +- Cargo.toml | 4 +- tests/cli/end_to_end.rs | 106 +++++++++++++++++++++++ tests/cli/main.rs | 1 + tests/cli/util.rs | 9 +- tests/fixtures/chat_project/config.idp | 8 ++ tests/fixtures/chat_project/src/chat.ids | 16 ++++ 7 files changed, 143 insertions(+), 5 deletions(-) create mode 100644 tests/cli/end_to_end.rs create mode 100644 tests/fixtures/chat_project/config.idp create mode 100644 tests/fixtures/chat_project/src/chat.ids diff --git a/Cargo.lock b/Cargo.lock index 202c90f..13ea968 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -398,7 +398,7 @@ dependencies = [ [[package]] name = "comline-codegen-rust" version = "0.1.0" -source = "git+https://github.com/ComlineProject/comline-rust?rev=a70cafcdd2015686ff1dbfba59b2b56ff70b730d#a70cafcdd2015686ff1dbfba59b2b56ff70b730d" +source = "git+https://github.com/ComlineProject/comline-rust?rev=1191e76068eca75ea7c4148d58ab127bb005e81a#1191e76068eca75ea7c4148d58ab127bb005e81a" dependencies = [ "comline-codegen", "comline-core", @@ -408,7 +408,7 @@ dependencies = [ [[package]] name = "comline-codegen-typescript" version = "0.1.0" -source = "git+https://github.com/ComlineProject/comline-typescript?rev=7f2ddd3ed97c412389e0a0bf06420774b1a659d3#7f2ddd3ed97c412389e0a0bf06420774b1a659d3" +source = "git+https://github.com/ComlineProject/comline-typescript?rev=86fc4eb586529ef92041f8dd3c46ff9340103411#86fc4eb586529ef92041f8dd3c46ff9340103411" dependencies = [ "comline-codegen", "comline-core", diff --git a/Cargo.toml b/Cargo.toml index 0f99b82..f933aaf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,8 +31,8 @@ comline-core = { git = "https://github.com/ComlineProject/core", rev = "47ac5f10 # `generation` tip (only `conformance/`, which this crate never touches, # differs), so matching it here keeps one `comline-codegen` in the tree. comline-codegen = { git = "https://github.com/ComlineProject/generation", rev = "1f8c5e290eba72a578860ebb6dc33453a2f0726b" } -comline-codegen-rust = { git = "https://github.com/ComlineProject/comline-rust", rev = "a70cafcdd2015686ff1dbfba59b2b56ff70b730d", optional = true } -comline-codegen-typescript = { git = "https://github.com/ComlineProject/comline-typescript", rev = "7f2ddd3ed97c412389e0a0bf06420774b1a659d3", optional = true } +comline-codegen-rust = { git = "https://github.com/ComlineProject/comline-rust", rev = "1191e76068eca75ea7c4148d58ab127bb005e81a", optional = true } +comline-codegen-typescript = { git = "https://github.com/ComlineProject/comline-typescript", rev = "86fc4eb586529ef92041f8dd3c46ff9340103411", optional = true } [features] # Which language generators are compiled in. Drop one to shed its whole diff --git a/tests/cli/end_to_end.rs b/tests/cli/end_to_end.rs new file mode 100644 index 0000000..599d863 --- /dev/null +++ b/tests/cli/end_to_end.rs @@ -0,0 +1,106 @@ +//! The whole pipeline, for real: a `.comline` schema with a `protocol` → +//! `comline generate --mode lib` → the emitted crate is compiled against +//! `comline-runtime` and a hand-written driver runs a client ⇆ provider +//! round-trip over an in-memory transport (a request/response call, a raised +//! typed error, a one-way notify). +//! +//! Slow: it fetches and builds `comline-runtime`. Needs network. + +use std::fs; +use std::process::Command; + +use crate::util::*; + +/// Driven against the generated `chat_e2e` crate (see +/// `tests/fixtures/chat_project/src/chat.ids`). +const DRIVER: &str = r#" +use std::sync::{Arc, Mutex}; +use std::thread; + +use chat_e2e::chat::{Chat, ChatClient, ChatDispatcher, ChatSendError, Message, Rejected}; +use comline_runtime::client::Client; +use comline_runtime::contract::CallError; +use comline_runtime::format::MsgPack; +use comline_runtime::serve::Server; +use comline_runtime::transport::duplex; + +struct Svc { + notes: Arc>>, +} + +impl Chat for Svc { + fn send(&self, text: &str) -> Result { + if text.is_empty() { + return Err(ChatSendError::Rejected(Rejected { reason: "empty".into() })); + } + Ok(Message { body: format!("echo: {text}"), seq: 1 }) + } + fn note(&self, text: &str) { + self.notes.lock().unwrap().push(text.to_string()); + } +} + +#[test] +fn client_and_provider_over_duplex() { + let (client_side, provider_side) = duplex(); + let notes = Arc::new(Mutex::new(Vec::new())); + let notes_for_svc = notes.clone(); + + let provider = thread::spawn(move || { + let mut provider_side = provider_side; + Server::new(ChatDispatcher(Svc { notes: notes_for_svc }), MsgPack) + .serve(&mut provider_side) + .unwrap(); + }); + + let mut client = ChatClient::new(Client::new(client_side, MsgPack)); + + assert_eq!(client.send("hi").unwrap().body, "echo: hi"); + + match client.send("").unwrap_err() { + CallError::App(ChatSendError::Rejected(r)) => assert_eq!(r.reason, "empty"), + other => panic!("expected a Rejected, got {other:?}"), + } + + client.note("saved").unwrap(); // one-way + + drop(client); + provider.join().unwrap(); + + assert_eq!(&*notes.lock().unwrap(), &["saved".to_string()]); +} +"#; + +#[test] +fn a_generated_protocol_crate_runs_a_real_round_trip() { + let temp = tempfile::tempdir().unwrap(); + let project = copy_fixture("chat_project", temp.path()); + + comline_cmd() + .current_dir(&project) + .args(["generate", "--target", "rust", "--mode", "lib", "--out", "gen"]) + .assert() + .success(); + + let crate_dir = project.join("gen/rust"); + assert!(crate_dir.join("Cargo.toml").exists()); + assert!(crate_dir.join("src/chat.rs").exists()); + + // A lib crate git-deps `comline-runtime`; drop the driver in `tests/`. + fs::create_dir_all(crate_dir.join("tests")).unwrap(); + fs::write(crate_dir.join("tests/roundtrip.rs"), DRIVER).unwrap(); + + let out = Command::new(env!("CARGO")) + .args(["test", "--quiet"]) + .current_dir(&crate_dir) + .env("CARGO_TARGET_DIR", crate_dir.join("target")) + .output() + .expect("run cargo test on the generated crate"); + + assert!( + out.status.success(), + "the generated crate's round-trip test failed\n--- stdout ---\n{}\n--- stderr ---\n{}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr), + ); +} diff --git a/tests/cli/main.rs b/tests/cli/main.rs index 6f96ab3..414a82b 100644 --- a/tests/cli/main.rs +++ b/tests/cli/main.rs @@ -10,6 +10,7 @@ mod build; mod check; mod clean; mod diff; +mod end_to_end; mod generate; mod global; mod new; diff --git a/tests/cli/util.rs b/tests/cli/util.rs index e1db537..c192d20 100644 --- a/tests/cli/util.rs +++ b/tests/cli/util.rs @@ -21,8 +21,15 @@ pub fn comline_cmd() -> Command { /// files) is skipped, so the tests stay hermetic even if someone has run the /// CLI against the fixture locally. pub fn fixture_project(temp: &Path) -> PathBuf { + copy_fixture("simple_project", temp) +} + +/// Like [`fixture_project`] for an arbitrary fixture under `tests/fixtures/`. +pub fn copy_fixture(name: &str, temp: &Path) -> PathBuf { let dest = temp.join("proj"); - let src = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/simple_project"); + let src = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures") + .join(name); fs::create_dir_all(&dest).unwrap(); fs::copy(src.join("config.idp"), dest.join("config.idp")).unwrap(); copy_dir(&src.join("src"), &dest.join("src")); diff --git a/tests/fixtures/chat_project/config.idp b/tests/fixtures/chat_project/config.idp new file mode 100644 index 0000000..9bbae6a --- /dev/null +++ b/tests/fixtures/chat_project/config.idp @@ -0,0 +1,8 @@ +congregation chat_e2e +specification_version = 1 + +code_generation = { + languages = { + rust#1.70.0 = {} + } +} diff --git a/tests/fixtures/chat_project/src/chat.ids b/tests/fixtures/chat_project/src/chat.ids new file mode 100644 index 0000000..2005a14 --- /dev/null +++ b/tests/fixtures/chat_project/src/chat.ids @@ -0,0 +1,16 @@ +struct Message { + body: str + seq: u64 +} + +error Rejected { + message = "rejected: {self.reason}" + reason: str +} + +protocol Chat { + /// Request/response with a struct return and a raised error. + function send(text: str) -> Message ! Rejected; + /// Fire-and-forget. + function note(text: str); +} From 3320f754487a1c0fb543e08eae7d24a2aceafa21 Mon Sep 17 00:00:00 2001 From: Kinflou Date: Wed, 2 Sep 2026 02:07:39 +0800 Subject: [PATCH 2/2] test: rustfmt end_to_end.rs --- tests/cli/end_to_end.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/cli/end_to_end.rs b/tests/cli/end_to_end.rs index 599d863..ca82226 100644 --- a/tests/cli/end_to_end.rs +++ b/tests/cli/end_to_end.rs @@ -78,7 +78,9 @@ fn a_generated_protocol_crate_runs_a_real_round_trip() { comline_cmd() .current_dir(&project) - .args(["generate", "--target", "rust", "--mode", "lib", "--out", "gen"]) + .args([ + "generate", "--target", "rust", "--mode", "lib", "--out", "gen", + ]) .assert() .success();