use crate::e2e::config::E2eConfig;
use crate::e2e::fixture::FixtureGroup;
pub(super) fn render_pubspec(
pkg_name: &str,
pkg_path: &str,
pkg_version: &str,
dep_mode: crate::e2e::config::DependencyMode,
) -> String {
let test_ver = crate::core::template_versions::pub_dev::TEST_PACKAGE;
let http_ver = crate::core::template_versions::pub_dev::HTTP_PACKAGE;
let ffi_ver = crate::core::template_versions::pub_dev::FFI_PACKAGE;
let dep_block = match dep_mode {
crate::e2e::config::DependencyMode::Registry => {
format!(" {pkg_name}: {pkg_version}")
}
crate::e2e::config::DependencyMode::Local => {
format!(" {pkg_name}:\n path: {pkg_path}")
}
};
let sdk = crate::core::template_versions::toolchain::DART_SDK_CONSTRAINT;
format!(
r#"name: e2e_dart
version: 0.1.0
publish_to: none
environment:
sdk: "{sdk}"
dependencies:
{dep_block}
ffi: {ffi_ver}
dev_dependencies:
test: {test_ver}
http: {http_ver}
"#
)
}
pub(super) fn render_e2e_helpers() -> String {
r#"// Generated by alef — DO NOT EDIT.
// Shared e2e test support: standalone mock-server spawn + repo-root-relative path
// resolution. Imported by generated `*_test.dart` files that spawn the standalone
// mock-server (see `render_dart_sut_spawn`'s non-server-pattern branch).
import 'dart:async';
import 'dart:convert';
import 'dart:io';
class MockServerHandle {
MockServerHandle._(this._process, this.url, this.fixtureUrls);
final Process? _process;
final String url;
final Map<String, String> fixtureUrls;
Future<void> stop() async {
final process = _process;
if (process == null) return;
try {
await process.stdin.close();
} catch (_) {}
process.kill(ProcessSignal.sigterm);
try {
await process.exitCode.timeout(const Duration(seconds: 5));
} on TimeoutException {
process.kill(ProcessSignal.sigkill);
await process.exitCode;
}
}
}
class _MockServerStartup {
_MockServerStartup(this.url, this.fixtureUrls);
final String url;
final Map<String, String> fixtureUrls;
}
void useTestDocumentsCwd() {
final testDocuments = _findTestDocumentsDir();
Directory.current = testDocuments.path;
}
Future<MockServerHandle> startMockServer() async {
final presetUrl = Platform.environment['MOCK_SERVER_URL'];
if (presetUrl != null && presetUrl.isNotEmpty) {
return MockServerHandle._(null, presetUrl, _fixtureUrlsFromEnvironment());
}
final repoRoot = _findRepoRoot();
final mockServer = File(
'${repoRoot.path}/e2e/rust/target/release/mock-server',
);
if (!mockServer.existsSync()) {
final manifestPath = '${repoRoot.path}/e2e/rust/Cargo.toml';
final build = await Process.run('cargo', [
'build',
'--release',
'--manifest-path',
manifestPath,
'--bin',
'mock-server',
]);
if (build.exitCode != 0) {
throw StateError('mock-server build failed: ${build.stderr}');
}
}
final process = await Process.start(mockServer.path, [
'${repoRoot.path}/fixtures',
]);
process.stderr.transform(utf8.decoder).listen(stderr.write);
final completer = Completer<_MockServerStartup>();
StreamSubscription<String>? subscription;
var collectedUrl = '';
var collectedFixtureUrls = <String, String>{};
subscription = process.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((line) {
final trimmed = line.trim();
if (trimmed.startsWith('MOCK_SERVER_URL=')) {
collectedUrl = trimmed.substring('MOCK_SERVER_URL='.length);
return;
}
if (trimmed.startsWith('MOCK_SERVERS=')) {
final rawJson = trimmed.substring('MOCK_SERVERS='.length);
final decoded = jsonDecode(rawJson) as Map<String, dynamic>;
collectedFixtureUrls = decoded.map(
(key, value) => MapEntry(key, value as String),
);
if (collectedUrl.isNotEmpty && !completer.isCompleted) {
completer.complete(
_MockServerStartup(collectedUrl, collectedFixtureUrls),
);
subscription?.cancel();
}
}
}, onError: completer.completeError);
try {
final startup = await completer.future.timeout(const Duration(seconds: 30));
return MockServerHandle._(process, startup.url, startup.fixtureUrls);
} on TimeoutException {
process.kill(ProcessSignal.sigkill);
throw StateError('mock-server startup timeout');
}
}
Directory _findRepoRoot() {
var current = Directory.current.absolute;
for (var i = 0; i < 16; i++) {
if (File('${current.path}/Cargo.toml').existsSync() &&
Directory('${current.path}/test_documents').existsSync()) {
return current;
}
final parent = current.parent;
if (parent.path == current.path) break;
current = parent;
}
throw StateError(
'could not locate repository root from ${Directory.current.path}',
);
}
Directory _findTestDocumentsDir() {
var current = Directory.current.absolute;
for (var i = 0; i < 16; i++) {
final candidate = Directory('${current.path}/test_documents');
if (candidate.existsSync()) return candidate;
final parent = current.parent;
if (parent.path == current.path) break;
current = parent;
}
throw StateError(
'could not locate test_documents from ${Directory.current.path}',
);
}
Map<String, String> _fixtureUrlsFromEnvironment() {
final result = <String, String>{};
for (final entry in Platform.environment.entries) {
if (!entry.key.startsWith('MOCK_SERVER_') || entry.key == 'MOCK_SERVER_URL') {
continue;
}
result[entry.key.substring('MOCK_SERVER_'.length).toLowerCase()] =
entry.value;
}
return result;
}
"#
.to_string()
}
#[allow(dead_code)]
pub(super) fn render_app_harness(groups: &[FixtureGroup], e2e_config: &E2eConfig, pkg_name: &str) -> String {
let mut fixtures_map = serde_json::Map::new();
for group in groups {
for fixture in &group.fixtures {
if let Some(http) = &fixture.http {
let mut fixture_obj = serde_json::Map::new();
let mut http_obj = serde_json::Map::new();
let mut handler_obj = serde_json::Map::new();
handler_obj.insert("route".to_string(), serde_json::json!(http.handler.route));
handler_obj.insert("method".to_string(), serde_json::json!(http.handler.method.as_str()));
if let Some(body_schema) = &http.handler.body_schema {
handler_obj.insert("body_schema".to_string(), body_schema.clone());
} else {
handler_obj.insert("body_schema".to_string(), serde_json::Value::Null);
}
http_obj.insert("handler".to_string(), serde_json::Value::Object(handler_obj));
let mut response_obj = serde_json::Map::new();
response_obj.insert(
"status_code".to_string(),
serde_json::json!(http.expected_response.status_code),
);
if let Some(body) = &http.expected_response.body {
response_obj.insert("body".to_string(), body.clone());
} else {
response_obj.insert("body".to_string(), serde_json::Value::Null);
}
let headers: serde_json::Map<String, serde_json::Value> = http
.expected_response
.headers
.iter()
.map(|(k, v)| (k.clone(), serde_json::json!(v)))
.collect();
response_obj.insert("headers".to_string(), serde_json::Value::Object(headers));
http_obj.insert("expected_response".to_string(), serde_json::Value::Object(response_obj));
fixture_obj.insert("http".to_string(), serde_json::Value::Object(http_obj));
fixtures_map.insert(fixture.id.clone(), serde_json::Value::Object(fixture_obj));
}
}
}
let fixtures_json = serde_json::to_string(&fixtures_map).unwrap_or_else(|_| "{}".to_string());
let bridge_module = format!("{pkg_name}_bridge_generated");
let ctx = minijinja::context! {
fixtures_json => fixtures_json,
pkg_name => pkg_name,
bridge_module => bridge_module,
host => &e2e_config.harness.host,
port => e2e_config.harness.port,
};
crate::e2e::template_env::render("dart/app_harness.dart.jinja", ctx)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::e2e::config::DependencyMode;
#[test]
fn render_pubspec_registry_uses_exact_pin() {
let out = render_pubspec("my_pkg", "", "1.2.3", DependencyMode::Registry);
assert!(out.contains("my_pkg: 1.2.3"), "got: {out}");
assert!(
!out.contains("my_pkg: ^1.2.3"),
"must not add a caret range, got: {out}"
);
}
#[test]
fn render_pubspec_registry_already_prefixed_passes_through() {
let out = render_pubspec("my_pkg", "", "^1.2.3", DependencyMode::Registry);
assert!(out.contains("my_pkg: ^1.2.3"), "got: {out}");
assert!(!out.contains("^^"), "must not double the prefix, got: {out}");
}
#[test]
fn render_pubspec_local_uses_path_dependency() {
let out = render_pubspec("my_pkg", "../my_pkg", "1.2.3", DependencyMode::Local);
assert!(out.contains("my_pkg:\n path: ../my_pkg"), "got: {out}");
}
}