use crate::core::hash::{self, CommentStyle};
use crate::core::template_versions::toolchain;
use crate::e2e::config::E2eConfig;
use crate::e2e::fixture::FixtureGroup;
pub(super) const SWIFT_FORMAT_IGNORE_DIRECTIVE: &str = "// swift-format-ignore-file\n\n";
pub(super) fn render_test_helpers_swift() -> String {
let header = hash::header(CommentStyle::DoubleSlash);
let ignore = SWIFT_FORMAT_IGNORE_DIRECTIVE;
format!(
r#"{header}{ignore}import Foundation
#if canImport(FoundationNetworking)
// URLSession, URLRequest, HTTPURLResponse, and URLSessionTaskDelegate live in
// the FoundationNetworking submodule on swift-corelibs-foundation (Linux). On
// Apple platforms these types remain in plain Foundation and this submodule
// does not exist; the canImport guard skips the import there.
import FoundationNetworking
#endif
import RustBridge
// Make `RustString` print its content in XCTest failure output. Without this,
// every error thrown from the swift-bridge layer surfaces as
// `caught error: "RustBridge.RustString"` with the actual message hidden
// inside the opaque class instance. The `@retroactive` keyword acknowledges
// that the conformed-to protocol (`CustomStringConvertible`) and the
// conforming type (`RustString`) both live outside this module — required by
// Swift 6 to silence the retroactive-conformance warning. swift-bridge does
// not give `RustString` a `description` of its own, so there is no conflict.
extension RustString: @retroactive CustomStringConvertible {{
public var description: String {{ self.toString() }}
}}
// URLSession delegate that does not follow redirects, so tests can assert on 3xx status codes
// and Location headers instead of transparently chasing them to the final response.
final class AlefE2ENoRedirectDelegate: NSObject, URLSessionTaskDelegate {{
func urlSession(
_ session: URLSession,
task: URLSessionTask,
willPerformHTTPRedirection response: HTTPURLResponse,
newRequest request: URLRequest,
completionHandler: @escaping (URLRequest?) -> Void
) {{
completionHandler(nil)
}}
}}
// Mock server base URL accessor used by the generated test bodies.
// The `MOCK_SERVER_URL` env var is exported by `scripts/e2e/run-with-mock-server.sh`
// (which spawns the `mock-server` binary built from `e2e/rust`) before invoking
// `swift test`. We fall back to a `localhost` URL that will fail-fast at request
// time so misconfigured runs surface a clear error instead of silently hitting
// production endpoints.
enum AlefE2EMockServer {{
static var baseURL: String {{
ProcessInfo.processInfo.environment["MOCK_SERVER_URL"]
?? "http://127.0.0.1:0"
}}
}}
"#
)
}
fn chunk_fixtures_for_swift(json: &str) -> Vec<String> {
const CHUNK_SIZE: usize = 30000;
let mut max_hash_run = 0;
let mut current_run = 0;
for c in json.chars() {
if c == '#' {
current_run += 1;
max_hash_run = max_hash_run.max(current_run);
} else {
current_run = 0;
}
}
let delimiter_level = max_hash_run + 1;
let delimiter = "#".repeat(delimiter_level);
let mut chunks = Vec::new();
let mut current_chunk = String::new();
for c in json.chars() {
if !current_chunk.is_empty() && current_chunk.len() + c.len_utf8() > CHUNK_SIZE {
chunks.push(format!("{0}\"{1}\"{0}", delimiter, current_chunk));
current_chunk.clear();
}
current_chunk.push(c);
}
if !current_chunk.is_empty() {
chunks.push(format!("{0}\"{1}\"{0}", delimiter, current_chunk));
}
chunks
}
pub(super) fn render_app_harness(e2e_config: &E2eConfig, groups: &[FixtureGroup], module_name: &str) -> String {
let mut fixtures_map = serde_json::Map::new();
for group in groups {
for fixture in &group.fixtures {
if fixture.http.is_none() {
continue;
}
let http_data = &fixture.http.as_ref().unwrap();
let fixture_json = serde_json::json!({
"http": {
"handler": {
"route": &http_data.handler.route,
"method": &http_data.handler.method,
"body_schema": http_data.handler.body_schema.clone(),
},
"request": {
"path": &http_data.request.path,
},
"expected_response": {
"status_code": http_data.expected_response.status_code,
"body": &http_data.expected_response.body,
"headers": &http_data.expected_response.headers,
}
}
});
fixtures_map.insert(fixture.id.clone(), fixture_json);
}
}
let fixtures_json = serde_json::to_string(&fixtures_map).unwrap_or_default();
let fixtures_json_chunks = chunk_fixtures_for_swift(&fixtures_json);
let host = &e2e_config.harness.host;
let port = e2e_config.harness.port;
let app_class = e2e_config.harness.app_class_for_lang("swift");
let register_route_method = e2e_config
.harness
.register_method_idiomatic("swift")
.unwrap_or_else(|| "registerRoute".to_string());
let body_schema_setter = &e2e_config.harness.body_schema_setter;
let method_enum = &e2e_config.harness.method_enum;
let run_method = e2e_config.harness.run_method_for_lang("swift");
let header = hash::header(CommentStyle::DoubleSlash);
let mut imports = e2e_config.harness.imports_for_lang("swift");
if !imports.iter().any(|i| i.to_lowercase() == module_name.to_lowercase()) {
imports.insert(0, module_name.to_string());
}
let imports_str = imports
.iter()
.map(|m| format!("import {}", m))
.collect::<Vec<_>>()
.join("\n");
let ctx = minijinja::context! {
header => header,
imports => imports_str,
app_class => app_class.as_deref().unwrap_or("App"),
route_builder_constructor => "RouteBuilder",
route_builder_schema_setter => body_schema_setter.as_deref().unwrap_or("requestSchemaJson"),
method_enum_class => method_enum.as_deref().unwrap_or("Method"),
register_route_method => register_route_method.as_str(),
run_method => run_method.as_deref().unwrap_or("run"),
response_body_field => e2e_config.harness.response_body_field.as_str(),
host => host,
port => port,
fixtures_json_chunks => fixtures_json_chunks,
};
crate::e2e::template_env::render("swift/app_harness.swift.jinja", ctx)
}
fn extract_package_name(url: &str, default_name: &str) -> String {
url.trim_end_matches('/')
.trim_end_matches(".git")
.rsplit('/')
.next()
.unwrap_or(default_name)
.to_string()
}
fn inject_package_swift_extras(
dependencies_block: &mut String,
test_target_dep: &mut String,
extras: &crate::core::config::manifest_extras::ManifestExtras,
) {
use crate::core::config::manifest_extras::ExtraDepSpec;
let mut all_extras: std::collections::BTreeMap<String, (String, String)> = std::collections::BTreeMap::new();
for (key, spec) in &extras.dependencies {
match spec {
ExtraDepSpec::Simple(version) => {
let pkg_name = extract_package_name(key, key);
all_extras.insert(pkg_name.clone(), (key.clone(), version.clone()));
}
ExtraDepSpec::Detailed(table) => {
if let (Some(url), Some(version)) = (
table.get("url").and_then(|u| u.as_str()),
table.get("version").and_then(|v| v.as_str()),
) {
let pkg_name = extract_package_name(url, key);
all_extras.insert(pkg_name, (url.to_string(), version.to_string()));
}
}
}
}
for (key, spec) in &extras.dev_dependencies {
match spec {
ExtraDepSpec::Simple(version) => {
let pkg_name = extract_package_name(key, key);
all_extras.insert(pkg_name.clone(), (key.clone(), version.clone()));
}
ExtraDepSpec::Detailed(table) => {
if let (Some(url), Some(version)) = (
table.get("url").and_then(|u| u.as_str()),
table.get("version").and_then(|v| v.as_str()),
) {
let pkg_name = extract_package_name(url, key);
all_extras.insert(pkg_name, (url.to_string(), version.to_string()));
}
}
}
}
if all_extras.is_empty() {
return;
}
let extras_packages: String = all_extras
.iter()
.map(|(_pkg_name, (url, version))| format!(" .package(url: \"{url}\", from: \"{version}\"),\n"))
.collect();
if let Some(pos) = dependencies_block.rfind(" ],") {
dependencies_block.insert_str(pos, &extras_packages);
}
let extras_products: String = all_extras
.keys()
.map(|pkg_name| format!(", .product(name: \"{pkg_name}\", package: \"{pkg_name}\")"))
.collect();
test_target_dep.push_str(&extras_products);
}
pub(super) fn render_package_swift(
module_name: &str,
registry_url: &str,
pkg_path: &str,
pkg_version: &str,
dep_mode: crate::e2e::config::DependencyMode,
include_harness_target: bool,
extras: Option<&crate::core::config::manifest_extras::ManifestExtras>,
) -> String {
let min_macos = toolchain::SWIFT_MIN_MACOS;
let (mut dependencies_block, mut test_target_dep) = match dep_mode {
crate::e2e::config::DependencyMode::Registry => {
let github_repo_url = registry_url.trim_end_matches(".git");
let package_dep = format!(
r#" .package(url: "{github_repo_url}", from: "{pkg_version}"),
"#
);
let deps_block = format!(" dependencies: [\n{package_dep} ],\n");
let pkg_id = github_repo_url
.trim_end_matches('/')
.rsplit('/')
.next()
.unwrap_or(module_name)
.trim_end_matches(".git");
let prod = format!(r#".product(name: "{module_name}", package: "{pkg_id}")"#);
(deps_block, prod)
}
crate::e2e::config::DependencyMode::Local => {
let pkg_id = pkg_path.trim_end_matches('/').rsplit('/').next().unwrap_or(module_name);
let deps_block = format!(" dependencies: [\n .package(path: \"{pkg_path}\"),\n ],\n");
let prod = format!(r#".product(name: "{module_name}", package: "{pkg_id}")"#);
(deps_block, prod)
}
};
if let Some(extra) = extras {
if !extra.is_empty() {
inject_package_swift_extras(&mut dependencies_block, &mut test_target_dep, extra);
}
}
let min_macos_major = min_macos.split('.').next().unwrap_or(min_macos);
let min_ios = toolchain::SWIFT_MIN_IOS;
let min_ios_major = min_ios.split('.').next().unwrap_or(min_ios);
let harness_target = if include_harness_target {
format!(
r#" .executableTarget(
name: "Harness",
dependencies: [{test_target_dep}],
path: "Sources/Harness"
),
"#
)
} else {
String::new()
};
let targets_block = format!(
r#"{harness_target} .testTarget(
name: "{module_name}E2ETests",
dependencies: [{test_target_dep}]
),
"#
);
format!(
r#"// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "E2eSwift",
platforms: [
.macOS(.v{min_macos_major}),
.iOS(.v{min_ios_major}),
],
{dependencies_block} targets: [
{targets_block} ]
)
"#
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::config::manifest_extras::{ExtraDepSpec, ManifestExtras};
#[test]
fn render_package_swift_local_mode_baseline() {
let out = render_package_swift("TreeSitter", "https://example.com/tree-sitter.git", "../../packages/swift", "0.25.0", crate::e2e::config::DependencyMode::Local, false, None);
assert!(out.contains(".package(path:"), "baseline Package.swift should have .package(path:)");
assert!(out.contains("\"../../packages/swift\""), "should contain local path");
assert!(out.contains(".product(name: \"TreeSitter\", package: \"swift\")"), "should have product dep");
}
#[test]
fn render_package_swift_registry_mode_baseline() {
let out = render_package_swift("TreeSitter", "https://github.com/tree-sitter/tree-sitter-swift.git", "", "0.25.0", crate::e2e::config::DependencyMode::Registry, false, None);
assert!(out.contains(".package(url:"), "registry mode should use .package(url:)");
assert!(out.contains("https://github.com/tree-sitter/tree-sitter-swift"), "should contain GitHub URL");
assert!(out.contains("from: \"0.25.0\""), "should pin version");
assert!(out.contains(".product(name: \"TreeSitter\", package: \"tree-sitter-swift\")"), "should have product dep");
}
#[test]
fn render_package_swift_with_extras_detailed_form() {
let mut extras = ManifestExtras::default();
extras.dependencies.insert(
"upstream-key".to_string(),
ExtraDepSpec::Detailed({
let mut t = toml::Table::new();
t.insert("url".to_string(), "https://github.com/foo/SwiftTreeSitter.git".into());
t.insert("version".to_string(), "0.25.0".into());
t
}),
);
let out = render_package_swift("TreeSitter", "https://example.com/tree-sitter.git", "../../packages/swift", "0.25.0", crate::e2e::config::DependencyMode::Local, false, Some(&extras));
assert!(out.contains(".package(url: \"https://github.com/foo/SwiftTreeSitter.git\", from: \"0.25.0\")"), "should inject extras .package with url and version from Detailed form. Got:\n{out}");
assert!(out.contains(".product(name: \"SwiftTreeSitter\", package: \"SwiftTreeSitter\")"), "should inject product dep from extracted package name. Got:\n{out}");
}
#[test]
fn render_package_swift_with_extras_simple_form() {
let mut extras = ManifestExtras::default();
extras.dev_dependencies.insert(
"https://github.com/bar/MyLib.git".to_string(),
ExtraDepSpec::Simple("1.0.0".to_string()),
);
let out = render_package_swift("TreeSitter", "https://example.com/tree-sitter.git", "../../packages/swift", "0.25.0", crate::e2e::config::DependencyMode::Local, false, Some(&extras));
assert!(out.contains(".package(url: \"https://github.com/bar/MyLib.git\", from: \"1.0.0\")"), "should inject extras .package with URL as key and version as value. Got:\n{out}");
assert!(out.contains(".product(name: \"MyLib\", package: \"MyLib\")"), "should extract package name from URL. Got:\n{out}");
}
#[test]
fn render_package_swift_extras_both_buckets() {
let mut extras = ManifestExtras::default();
extras.dependencies.insert(
"runtime-pkg".to_string(),
ExtraDepSpec::Detailed({
let mut t = toml::Table::new();
t.insert("url".to_string(), "https://github.com/x/RuntimePkg.git".into());
t.insert("version".to_string(), "1.0.0".into());
t
}),
);
extras.dev_dependencies.insert(
"https://github.com/y/DevPkg.git".to_string(),
ExtraDepSpec::Simple("2.0.0".to_string()),
);
let out = render_package_swift("TreeSitter", "https://example.com/tree-sitter.git", "../../packages/swift", "0.25.0", crate::e2e::config::DependencyMode::Local, false, Some(&extras));
assert!(out.contains(".package(url: \"https://github.com/x/RuntimePkg.git\", from: \"1.0.0\")"), "should inject runtime dep. Got:\n{out}");
assert!(out.contains(".package(url: \"https://github.com/y/DevPkg.git\", from: \"2.0.0\")"), "should inject dev dep. Got:\n{out}");
assert!(out.contains(".product(name: \"RuntimePkg\", package: \"RuntimePkg\")"), "should include runtime product. Got:\n{out}");
assert!(out.contains(".product(name: \"DevPkg\", package: \"DevPkg\")"), "should include dev product. Got:\n{out}");
}
#[test]
fn render_package_swift_empty_extras_matches_none() {
let extras = ManifestExtras::default();
let with_empty = render_package_swift("TreeSitter", "https://example.com/tree-sitter.git", "../../packages/swift", "0.25.0", crate::e2e::config::DependencyMode::Local, false, Some(&extras));
let without = render_package_swift("TreeSitter", "https://example.com/tree-sitter.git", "../../packages/swift", "0.25.0", crate::e2e::config::DependencyMode::Local, false, None);
assert_eq!(with_empty, without, "empty extras should produce identical output to None");
}
#[test]
fn render_package_swift_extras_idempotent() {
let mut extras = ManifestExtras::default();
extras.dependencies.insert(
"https://github.com/a/PkgA.git".to_string(),
ExtraDepSpec::Simple("1.0.0".to_string()),
);
let first = render_package_swift("TreeSitter", "https://example.com/tree-sitter.git", "../../packages/swift", "0.25.0", crate::e2e::config::DependencyMode::Local, false, Some(&extras));
let second = render_package_swift("TreeSitter", "https://example.com/tree-sitter.git", "../../packages/swift", "0.25.0", crate::e2e::config::DependencyMode::Local, false, Some(&extras));
assert_eq!(first, second, "re-rendering with same extras should be byte-stable");
}
#[test]
fn render_package_swift_includes_harness_target_when_needed() {
let out = render_package_swift("TreeSitter", "https://example.com/tree-sitter.git", "../../packages/swift", "0.25.0", crate::e2e::config::DependencyMode::Local, true, None);
assert!(out.contains(".executableTarget("), "should include harness executable target");
assert!(out.contains("\"Harness\""), "harness target name should be present");
}
#[test]
fn render_package_swift_omits_harness_target_when_not_needed() {
let out = render_package_swift("TreeSitter", "https://example.com/tree-sitter.git", "../../packages/swift", "0.25.0", crate::e2e::config::DependencyMode::Local, false, None);
assert!(!out.contains(".executableTarget("), "should omit harness executable target");
}
}