Skip to main content

alef_e2e/codegen/
dart.rs

1//! Dart e2e test generator using package:test and package:http.
2//!
3//! Generates `e2e/dart/test/<category>_test.dart` files from JSON fixtures.
4//! HTTP fixtures hit the mock server at `MOCK_SERVER_URL/fixtures/<id>`.
5//! Non-HTTP fixtures without a dart-specific call override emit a skip stub.
6
7use crate::config::E2eConfig;
8use crate::escape::sanitize_filename;
9use crate::fixture::{Fixture, FixtureGroup, HttpFixture, ValidationErrorExpectation};
10use alef_core::backend::GeneratedFile;
11use alef_core::config::ResolvedCrateConfig;
12use alef_core::hash::{self, CommentStyle};
13use alef_core::template_versions::pub_dev;
14use anyhow::Result;
15use std::cell::Cell;
16use std::fmt::Write as FmtWrite;
17use std::path::PathBuf;
18
19use super::E2eCodegen;
20use super::client;
21
22/// Dart e2e code generator.
23pub struct DartE2eCodegen;
24
25impl E2eCodegen for DartE2eCodegen {
26    fn generate(
27        &self,
28        groups: &[FixtureGroup],
29        e2e_config: &E2eConfig,
30        config: &ResolvedCrateConfig,
31    ) -> Result<Vec<GeneratedFile>> {
32        let lang = self.language_name();
33        let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
34
35        let mut files = Vec::new();
36
37        // Resolve package config.
38        let dart_pkg = e2e_config.resolve_package("dart");
39        let pkg_name = dart_pkg
40            .as_ref()
41            .and_then(|p| p.name.as_ref())
42            .cloned()
43            .unwrap_or_else(|| config.dart_pubspec_name());
44        let pkg_path = dart_pkg
45            .as_ref()
46            .and_then(|p| p.path.as_ref())
47            .cloned()
48            .unwrap_or_else(|| "../../packages/dart".to_string());
49        let pkg_version = dart_pkg
50            .as_ref()
51            .and_then(|p| p.version.as_ref())
52            .cloned()
53            .unwrap_or_else(|| "0.1.0".to_string());
54
55        // Generate pubspec.yaml with http dependency for HTTP client tests.
56        files.push(GeneratedFile {
57            path: output_base.join("pubspec.yaml"),
58            content: render_pubspec(&pkg_name, &pkg_path, &pkg_version, e2e_config.dep_mode),
59            generated_header: false,
60        });
61
62        // Generate dart_test.yaml to limit parallelism — the mock server uses keep-alive
63        // connections and gets overwhelmed when test files run in parallel.
64        files.push(GeneratedFile {
65            path: output_base.join("dart_test.yaml"),
66            content: concat!(
67                "# Generated by alef — DO NOT EDIT.\n",
68                "# Run test files sequentially to avoid overwhelming the mock server with\n",
69                "# concurrent keep-alive connections.\n",
70                "concurrency: 1\n",
71            )
72            .to_string(),
73            generated_header: false,
74        });
75
76        let test_base = output_base.join("test");
77
78        // One test file per fixture group.
79        for group in groups {
80            let active: Vec<&Fixture> = group
81                .fixtures
82                .iter()
83                .filter(|f| super::should_include_fixture(f, lang, e2e_config))
84                .collect();
85
86            if active.is_empty() {
87                continue;
88            }
89
90            let filename = format!("{}_test.dart", sanitize_filename(&group.category));
91            let content = render_test_file(&group.category, &active, e2e_config, lang);
92            files.push(GeneratedFile {
93                path: test_base.join(filename),
94                content,
95                generated_header: true,
96            });
97        }
98
99        Ok(files)
100    }
101
102    fn language_name(&self) -> &'static str {
103        "dart"
104    }
105}
106
107// ---------------------------------------------------------------------------
108// Rendering
109// ---------------------------------------------------------------------------
110
111fn render_pubspec(
112    pkg_name: &str,
113    pkg_path: &str,
114    pkg_version: &str,
115    dep_mode: crate::config::DependencyMode,
116) -> String {
117    let test_ver = pub_dev::TEST_PACKAGE;
118    let http_ver = pub_dev::HTTP_PACKAGE;
119
120    let dep_block = match dep_mode {
121        crate::config::DependencyMode::Registry => {
122            format!("  {pkg_name}: ^{pkg_version}")
123        }
124        crate::config::DependencyMode::Local => {
125            format!("  {pkg_name}:\n    path: {pkg_path}")
126        }
127    };
128
129    format!(
130        r#"name: e2e_dart
131version: 0.1.0
132publish_to: none
133
134environment:
135  sdk: ">=3.0.0 <4.0.0"
136
137dependencies:
138{dep_block}
139
140dev_dependencies:
141  test: {test_ver}
142  http: {http_ver}
143"#
144    )
145}
146
147fn render_test_file(category: &str, fixtures: &[&Fixture], e2e_config: &E2eConfig, lang: &str) -> String {
148    let mut out = String::new();
149    out.push_str(&hash::header(CommentStyle::DoubleSlash));
150
151    // Check if any fixture needs the http package (HTTP server tests).
152    let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
153
154    let _ = writeln!(out, "import 'package:test/test.dart';");
155    let _ = writeln!(out, "import 'dart:io';");
156    if has_http_fixtures {
157        let _ = writeln!(out, "import 'dart:async';");
158        let _ = writeln!(out, "import 'dart:convert';");
159    }
160    let _ = writeln!(out);
161
162    // Emit file-level HTTP client and serialization mutex.
163    //
164    // The shared HttpClient reuses keep-alive connections to minimize TCP overhead.
165    // The mutex (_lock) ensures requests are serialized within the file so the
166    // connection pool is not exercised concurrently by dart:test's async runner.
167    //
168    // _withRetry wraps the entire request closure with one automatic retry on
169    // transient connection errors (keep-alive connections can be silently closed
170    // by the server just as the client tries to reuse them).
171    if has_http_fixtures {
172        let _ = writeln!(out, "HttpClient _httpClient = HttpClient()..maxConnectionsPerHost = 1;");
173        let _ = writeln!(out);
174        let _ = writeln!(out, "var _lock = Future<void>.value();");
175        let _ = writeln!(out);
176        let _ = writeln!(out, "Future<T> _serialized<T>(Future<T> Function() fn) async {{");
177        let _ = writeln!(out, "  final current = _lock;");
178        let _ = writeln!(out, "  final next = Completer<void>();");
179        let _ = writeln!(out, "  _lock = next.future;");
180        let _ = writeln!(out, "  try {{");
181        let _ = writeln!(out, "    await current;");
182        let _ = writeln!(out, "    return await fn();");
183        let _ = writeln!(out, "  }} finally {{");
184        let _ = writeln!(out, "    next.complete();");
185        let _ = writeln!(out, "  }}");
186        let _ = writeln!(out, "}}");
187        let _ = writeln!(out);
188        // The `fn` here should be the full request closure — on socket failure we
189        // recreate the HttpClient (drops old pooled connections) and retry once.
190        let _ = writeln!(out, "Future<T> _withRetry<T>(Future<T> Function() fn) async {{");
191        let _ = writeln!(out, "  try {{");
192        let _ = writeln!(out, "    return await fn();");
193        let _ = writeln!(out, "  }} on SocketException {{");
194        let _ = writeln!(out, "    _httpClient.close(force: true);");
195        let _ = writeln!(out, "    _httpClient = HttpClient()..maxConnectionsPerHost = 1;");
196        let _ = writeln!(out, "    return fn();");
197        let _ = writeln!(out, "  }} on HttpException {{");
198        let _ = writeln!(out, "    _httpClient.close(force: true);");
199        let _ = writeln!(out, "    _httpClient = HttpClient()..maxConnectionsPerHost = 1;");
200        let _ = writeln!(out, "    return fn();");
201        let _ = writeln!(out, "  }}");
202        let _ = writeln!(out, "}}");
203        let _ = writeln!(out);
204    }
205
206    let _ = writeln!(out, "// E2e tests for category: {category}");
207    let _ = writeln!(out, "void main() {{");
208
209    // Close the shared client after all tests in this file complete.
210    if has_http_fixtures {
211        let _ = writeln!(out, "  tearDownAll(() => _httpClient.close());");
212        let _ = writeln!(out);
213    }
214
215    for fixture in fixtures {
216        render_test_case(&mut out, fixture, e2e_config, lang);
217    }
218
219    let _ = writeln!(out, "}}");
220    out
221}
222
223fn render_test_case(out: &mut String, fixture: &Fixture, e2e_config: &E2eConfig, lang: &str) {
224    // HTTP fixtures: hit the mock server.
225    if let Some(http) = &fixture.http {
226        render_http_test_case(out, fixture, http);
227        return;
228    }
229
230    // Non-HTTP fixtures: check if there is a dart-specific call override.
231    let call_config = e2e_config.resolve_call(fixture.call.as_deref());
232    let call_overrides = call_config.overrides.get(lang);
233
234    if call_overrides.is_none() {
235        // No dart-specific call override — emit a skip stub.
236        render_skip_stub(out, fixture);
237        return;
238    }
239
240    // Has a dart call override — render a call-based test.
241    let function_name = call_overrides
242        .and_then(|o| o.function.as_ref())
243        .cloned()
244        .unwrap_or_else(|| call_config.function.clone());
245    let result_var = &call_config.result_var;
246    let description = escape_dart(&fixture.description);
247    let is_async = call_config.r#async;
248
249    if is_async {
250        let _ = writeln!(out, "  test('{description}', () async {{");
251    } else {
252        let _ = writeln!(out, "  test('{description}', () {{");
253    }
254
255    if is_async {
256        let _ = writeln!(out, "    final {result_var} = await {function_name}();");
257    } else {
258        let _ = writeln!(out, "    final {result_var} = {function_name}();");
259    }
260
261    let _ = writeln!(out, "  }});");
262    let _ = writeln!(out);
263}
264
265// ---------------------------------------------------------------------------
266// HTTP server test rendering — DartTestClientRenderer impl + thin driver wrapper
267// ---------------------------------------------------------------------------
268
269/// Renderer that emits `package:test` `test(...)` blocks using `dart:io HttpClient`
270/// against the mock server (`Platform.environment['MOCK_SERVER_URL']`).
271///
272/// Skipped tests are emitted as self-contained stubs (complete test block with
273/// `markTestSkipped`) entirely inside `render_test_open`. `render_test_close` uses
274/// `in_skip` to emit the right closing token: nothing extra for skip stubs (already
275/// closed) vs. `})));` for regular tests.
276///
277/// `is_redirect` must be set to `true` before invoking the shared driver for 3xx
278/// fixtures so that `render_call` can inject `ioReq.followRedirects = false` after
279/// the `openUrl` call.
280struct DartTestClientRenderer {
281    /// Set to `true` when `render_test_open` is called with a skip reason so that
282    /// `render_test_close` can match the opening shape.
283    in_skip: Cell<bool>,
284    /// Pre-set to `true` by the thin wrapper when the fixture expects a 3xx response.
285    /// `render_call` injects `ioReq.followRedirects = false` when this is `true`.
286    is_redirect: Cell<bool>,
287}
288
289impl DartTestClientRenderer {
290    fn new(is_redirect: bool) -> Self {
291        Self {
292            in_skip: Cell::new(false),
293            is_redirect: Cell::new(is_redirect),
294        }
295    }
296}
297
298impl client::TestClientRenderer for DartTestClientRenderer {
299    fn language_name(&self) -> &'static str {
300        "dart"
301    }
302
303    /// Emit the test opening.
304    ///
305    /// For skipped fixtures: emit the entire self-contained stub (open + body +
306    /// close + blank line) and set `in_skip = true` so `render_test_close` is a
307    /// no-op.
308    ///
309    /// For active fixtures: emit `test('desc', () => _serialized(() => _withRetry(() async {`
310    /// leaving the block open for the assertion primitives.
311    fn render_test_open(&self, out: &mut String, _fn_name: &str, description: &str, skip_reason: Option<&str>) {
312        let escaped_desc = escape_dart(description);
313        if let Some(reason) = skip_reason {
314            let escaped_reason = escape_dart(reason);
315            let _ = writeln!(out, "  test('{escaped_desc}', () {{");
316            let _ = writeln!(out, "    markTestSkipped('{escaped_reason}');");
317            let _ = writeln!(out, "  }});");
318            let _ = writeln!(out);
319            self.in_skip.set(true);
320        } else {
321            let _ = writeln!(
322                out,
323                "  test('{escaped_desc}', () => _serialized(() => _withRetry(() async {{"
324            );
325            self.in_skip.set(false);
326        }
327    }
328
329    /// Emit the test closing token.
330    ///
331    /// No-op for skip stubs (the stub was fully closed in `render_test_open`).
332    /// Emits `})));` followed by a blank line for regular tests.
333    fn render_test_close(&self, out: &mut String) {
334        if self.in_skip.get() {
335            // Stub was already closed in render_test_open.
336            return;
337        }
338        let _ = writeln!(out, "  }})));");
339        let _ = writeln!(out);
340    }
341
342    /// Emit the full `dart:io HttpClient` request scaffolding.
343    ///
344    /// Emits:
345    /// - URL construction from `MOCK_SERVER_URL`.
346    /// - `_httpClient.openUrl(method, uri)`.
347    /// - `followRedirects = false` when `is_redirect` is pre-set on the renderer.
348    /// - Content-Type header, request headers, cookies, optional body bytes.
349    /// - `ioReq.close()` → `ioResp`.
350    /// - Response-body drain into `bodyStr` (skipped for redirect responses).
351    fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
352        // dart:io restricted headers (handled automatically by the HTTP stack).
353        const DART_RESTRICTED_HEADERS: &[&str] = &["content-length", "host", "transfer-encoding"];
354
355        let method = ctx.method.to_uppercase();
356        let escaped_method = escape_dart(&method);
357
358        // Fixture path is `/fixtures/<id>` — extract the id portion for URL construction.
359        let fixture_path = escape_dart(ctx.path);
360
361        // Determine effective content-type.
362        let has_explicit_content_type = ctx.headers.keys().any(|k| k.to_lowercase() == "content-type");
363        let effective_content_type = if has_explicit_content_type {
364            ctx.headers
365                .iter()
366                .find(|(k, _)| k.to_lowercase() == "content-type")
367                .map(|(_, v)| v.as_str())
368                .unwrap_or("application/json")
369        } else if ctx.body.is_some() {
370            ctx.content_type.unwrap_or("application/json")
371        } else {
372            ""
373        };
374
375        let _ = writeln!(
376            out,
377            "    final baseUrl = Platform.environment['MOCK_SERVER_URL'] ?? 'http://localhost:8080';"
378        );
379        let _ = writeln!(out, "    final uri = Uri.parse('$baseUrl{fixture_path}');");
380        let _ = writeln!(
381            out,
382            "    final ioReq = await _httpClient.openUrl('{escaped_method}', uri);"
383        );
384
385        // Disable automatic redirect following for 3xx fixtures so the test can
386        // assert on the redirect status code itself.
387        if self.is_redirect.get() {
388            let _ = writeln!(out, "    ioReq.followRedirects = false;");
389        }
390
391        // Set content-type header.
392        if !effective_content_type.is_empty() {
393            let escaped_ct = escape_dart(effective_content_type);
394            let _ = writeln!(out, "    ioReq.headers.set('content-type', '{escaped_ct}');");
395        }
396
397        // Set request headers (skip dart:io restricted headers and content-type, already handled).
398        let mut header_pairs: Vec<(&String, &String)> = ctx.headers.iter().collect();
399        header_pairs.sort_by_key(|(k, _)| k.as_str());
400        for (name, value) in &header_pairs {
401            if DART_RESTRICTED_HEADERS.contains(&name.to_lowercase().as_str()) {
402                continue;
403            }
404            if name.to_lowercase() == "content-type" {
405                continue; // Already handled above.
406            }
407            let escaped_name = escape_dart(&name.to_lowercase());
408            let escaped_value = escape_dart(value);
409            let _ = writeln!(out, "    ioReq.headers.set('{escaped_name}', '{escaped_value}');");
410        }
411
412        // Add cookies.
413        if !ctx.cookies.is_empty() {
414            let mut cookie_pairs: Vec<(&String, &String)> = ctx.cookies.iter().collect();
415            cookie_pairs.sort_by_key(|(k, _)| k.as_str());
416            let cookie_str: Vec<String> = cookie_pairs.iter().map(|(k, v)| format!("{k}={v}")).collect();
417            let cookie_header = escape_dart(&cookie_str.join("; "));
418            let _ = writeln!(out, "    ioReq.headers.set('cookie', '{cookie_header}');");
419        }
420
421        // Write body bytes if present (bypass charset-based encoding issues).
422        if let Some(body) = ctx.body {
423            let json_str = serde_json::to_string(body).unwrap_or_default();
424            let escaped = escape_dart(&json_str);
425            let _ = writeln!(out, "    final bodyBytes = utf8.encode('{escaped}');");
426            let _ = writeln!(out, "    ioReq.add(bodyBytes);");
427        }
428
429        let _ = writeln!(out, "    final ioResp = await ioReq.close();");
430        // Drain the response body to bind `bodyStr` for assertion primitives and to
431        // allow the server to cleanly close the connection (prevents RST packets).
432        // Redirect responses have no body to drain — skip to avoid a potential hang.
433        if !self.is_redirect.get() {
434            let _ = writeln!(out, "    final bodyStr = await ioResp.transform(utf8.decoder).join();");
435        };
436    }
437
438    fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
439        let _ = writeln!(
440            out,
441            "    expect(ioResp.statusCode, equals({status}), reason: 'status code mismatch');"
442        );
443    }
444
445    /// Emit a single header assertion, handling special tokens `<<present>>`,
446    /// `<<absent>>`, and `<<uuid>>`.
447    fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
448        let escaped_name = escape_dart(&name.to_lowercase());
449        match expected {
450            "<<present>>" => {
451                let _ = writeln!(
452                    out,
453                    "    expect(ioResp.headers.value('{escaped_name}'), isNotNull, reason: 'header {escaped_name} should be present');"
454                );
455            }
456            "<<absent>>" => {
457                let _ = writeln!(
458                    out,
459                    "    expect(ioResp.headers.value('{escaped_name}'), isNull, reason: 'header {escaped_name} should be absent');"
460                );
461            }
462            "<<uuid>>" => {
463                let _ = writeln!(
464                    out,
465                    "    expect(ioResp.headers.value('{escaped_name}'), matches(RegExp(r'^[0-9a-f]{{8}}-[0-9a-f]{{4}}-[0-9a-f]{{4}}-[0-9a-f]{{4}}-[0-9a-f]{{12}}$')), reason: 'header {escaped_name} should be a UUID');"
466                );
467            }
468            exact => {
469                let escaped_value = escape_dart(exact);
470                let _ = writeln!(
471                    out,
472                    "    expect(ioResp.headers.value('{escaped_name}'), contains('{escaped_value}'), reason: 'header {escaped_name} mismatch');"
473                );
474            }
475        }
476    }
477
478    /// Emit an exact-equality body assertion.
479    ///
480    /// String bodies are compared as decoded text; structured JSON bodies are
481    /// compared via `jsonDecode`.
482    fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
483        match expected {
484            serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
485                let json_str = serde_json::to_string(expected).unwrap_or_default();
486                let escaped = escape_dart(&json_str);
487                let _ = writeln!(out, "    final bodyJson = jsonDecode(bodyStr);");
488                let _ = writeln!(out, "    final expectedJson = jsonDecode('{escaped}');");
489                let _ = writeln!(
490                    out,
491                    "    expect(bodyJson, equals(expectedJson), reason: 'body mismatch');"
492                );
493            }
494            serde_json::Value::String(s) => {
495                let escaped = escape_dart(s);
496                let _ = writeln!(
497                    out,
498                    "    expect(bodyStr.trim(), equals('{escaped}'), reason: 'body mismatch');"
499                );
500            }
501            other => {
502                let escaped = escape_dart(&other.to_string());
503                let _ = writeln!(
504                    out,
505                    "    expect(bodyStr.trim(), equals('{escaped}'), reason: 'body mismatch');"
506                );
507            }
508        }
509    }
510
511    /// Emit partial-body assertions — every key in `expected` must match the
512    /// corresponding field in the parsed JSON response.
513    fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
514        let _ = writeln!(
515            out,
516            "    final partialJson = jsonDecode(bodyStr) as Map<String, dynamic>;"
517        );
518        if let Some(obj) = expected.as_object() {
519            for (idx, (key, val)) in obj.iter().enumerate() {
520                let escaped_key = escape_dart(key);
521                let json_val = serde_json::to_string(val).unwrap_or_default();
522                let escaped_val = escape_dart(&json_val);
523                // Use an index-based variable name so keys with special characters
524                // don't produce invalid Dart identifiers.
525                let _ = writeln!(out, "    final _expectedField{idx} = jsonDecode('{escaped_val}');");
526                let _ = writeln!(
527                    out,
528                    "    expect(partialJson['{escaped_key}'], equals(_expectedField{idx}), reason: 'partial body field \\'{escaped_key}\\' mismatch');"
529                );
530            }
531        }
532    }
533
534    /// Emit validation-error assertions for 422 responses.
535    fn render_assert_validation_errors(
536        &self,
537        out: &mut String,
538        _response_var: &str,
539        errors: &[ValidationErrorExpectation],
540    ) {
541        let _ = writeln!(out, "    final errBody = jsonDecode(bodyStr) as Map<String, dynamic>;");
542        let _ = writeln!(out, "    final errList = (errBody['errors'] ?? []) as List<dynamic>;");
543        for ve in errors {
544            let loc_dart: Vec<String> = ve.loc.iter().map(|s| format!("'{}'", escape_dart(s))).collect();
545            let loc_str = loc_dart.join(", ");
546            let escaped_msg = escape_dart(&ve.msg);
547            let _ = writeln!(
548                out,
549                "    expect(errList.any((e) => e is Map && (e['loc'] as List?)?.join(',') == [{loc_str}].join(',') && (e['msg'] as String? ?? '').contains('{escaped_msg}')), isTrue, reason: 'validation error not found: {escaped_msg}');"
550            );
551        }
552    }
553}
554
555/// Render a `package:test` `test(...)` block for an HTTP server fixture.
556///
557/// Delegates to the shared [`client::http_call::render_http_test`] driver via
558/// [`DartTestClientRenderer`]. HTTP 101 (WebSocket upgrade) fixtures are emitted
559/// as skip stubs before reaching the driver because `dart:io HttpClient` cannot
560/// handle protocol-switch responses.
561fn render_http_test_case(out: &mut String, fixture: &Fixture, http: &HttpFixture) {
562    // HTTP 101 (WebSocket upgrade) — dart:io HttpClient cannot handle upgrade responses.
563    if http.expected_response.status_code == 101 {
564        let description = escape_dart(&fixture.description);
565        let _ = writeln!(out, "  test('{description}', () {{");
566        let _ = writeln!(
567            out,
568            "    markTestSkipped('Skipped: Dart HttpClient cannot handle 101 Switching Protocols responses');"
569        );
570        let _ = writeln!(out, "  }});");
571        let _ = writeln!(out);
572        return;
573    }
574
575    // Pre-set `is_redirect` on the renderer so `render_call` can inject
576    // `ioReq.followRedirects = false` for 3xx fixtures. The shared driver has no
577    // concept of expected status code so we thread it through renderer state.
578    let is_redirect = http.expected_response.status_code / 100 == 3;
579    client::http_call::render_http_test(out, &DartTestClientRenderer::new(is_redirect), fixture);
580}
581
582/// Emit a compilable skip stub for non-HTTP fixtures without a dart call override.
583fn render_skip_stub(out: &mut String, fixture: &Fixture) {
584    let description = escape_dart(&fixture.description);
585    let fixture_id = &fixture.id;
586    let _ = writeln!(out, "  test('{description}', () {{");
587    let _ = writeln!(
588        out,
589        "    markTestSkipped('TODO: implement Dart e2e test for fixture \\'{fixture_id}\\'');"
590    );
591    let _ = writeln!(out, "  }});");
592    let _ = writeln!(out);
593}
594
595/// Escape a string for embedding in a Dart single-quoted string literal.
596fn escape_dart(s: &str) -> String {
597    s.replace('\\', "\\\\")
598        .replace('\'', "\\'")
599        .replace('\n', "\\n")
600        .replace('\r', "\\r")
601        .replace('\t', "\\t")
602        .replace('$', "\\$")
603}