alef 0.65.0

Opinionated polyglot binding generator for Rust libraries
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
//! Tests for the unavailable-field skip ledger in [`super`].
//!
//! Split out of `mod.rs` alongside `assertion_type_marker_tests`: the two inline test modules
//! were 538 of that file's 1,519 lines. `mod.rs` already carries `streaming_result_binding_tests`
//! and `working_directory_guard_tests` as sibling files, so this is the file's own established
//! shape rather than a new one.

use super::{
    SkipVerdict, fail_on_unavailable_field_markers, is_falsy_flag, skip_summary, strict_assertion_failure,
    take_skip_records,
};
use crate::e2e::fixture::{Assertion, AssertionSkip, AssertionSkipDirective, AssertionSkipKind};

/// Record `body` and return the verdicts, so a wording's *classification* can be asserted
/// directly rather than inferred from whether generation failed. ~keep
fn verdicts_for(body: &str, language: &str, assertions: &[Assertion]) -> Vec<SkipVerdict> {
    let _ = take_skip_records();
    fail_on_unavailable_field_markers(body, language, "smoke", assertions);
    take_skip_records().into_iter().map(|r| r.verdict).collect()
}

/// The strict-mode error for one rendered body, or `None` if it is generatable.
fn strict_error_for(body: &str, language: &str, fixture_id: &str, assertions: &[Assertion]) -> Option<String> {
    let _ = take_skip_records();
    fail_on_unavailable_field_markers(body, language, fixture_id, assertions);
    strict_assertion_failure(&take_skip_records(), true).map(|error| format!("{error:#}"))
}

fn assertion_on(field: &str, skip: Option<AssertionSkip>) -> Assertion {
    Assertion {
        field: Some(field.to_string()),
        skip,
        ..Assertion::default()
    }
}

/// The escape hatch must still generate: with strict off, a marker body is not an error.
#[test]
fn non_strict_is_a_noop_even_on_a_marker_body() {
    let _ = take_skip_records();
    fail_on_unavailable_field_markers(
        "    # skipped: field 'chunks' not available on result type\n",
        "python",
        "widget_smoke",
        &[],
    );
    assert!(strict_assertion_failure(&take_skip_records(), false).is_none());
}

#[test]
fn strict_fails_loudly_naming_fixture_and_field() {
    let error = strict_error_for(
        "    # skipped: field 'chunks' not available on result type\n",
        "python",
        "widget_smoke",
        &[],
    )
    .expect("an unresolved field must fail under strict");
    assert!(
        error.contains("[python] fixture `widget_smoke`: field `chunks`"),
        "got: {error}"
    );
}

/// The headline requirement: an unmappable field must fail generation *by default*, with no
/// environment variable armed. This reads the real default rather than a hard-coded `true`,
/// so flipping the default back would fail this test. ~keep
#[test]
fn an_unmappable_field_is_fatal_by_default() {
    let _ = take_skip_records();
    let body = "    // skipped: field 'strategy.crawl_order' not available on result type\n";
    fail_on_unavailable_field_markers(
        body,
        "go",
        "traversal_order",
        &[assertion_on("strategy.crawl_order", None)],
    );
    let error = strict_assertion_failure(&take_skip_records(), super::strict_assertions_enabled())
        .expect("an unmappable field must fail generation by default");
    assert!(
        format!("{error:#}").contains("field `strategy.crawl_order`"),
        "got: {error:#}"
    );
}

/// Every offender is listed, not just the first — one regeneration must surface the whole
/// backlog. ~keep
#[test]
fn strict_error_lists_every_offender() {
    let _ = take_skip_records();
    fail_on_unavailable_field_markers(
        "    // skipped: field 'alpha' not available on result type\n\
         \x20   // skipped: field 'beta' not available on result type\n",
        "go",
        "smoke",
        &[],
    );
    let error = strict_assertion_failure(&take_skip_records(), true).expect("two gaps must fail");
    let rendered = format!("{error:#}");
    assert!(rendered.contains("field `alpha`"), "got: {rendered}");
    assert!(rendered.contains("field `beta`"), "got: {rendered}");
    assert!(rendered.starts_with("2 e2e assertion(s)"), "got: {rendered}");
}

/// The opt-in half: the same body, with the fixture declaring the skip, generates cleanly and
/// is recorded as an acknowledged skip rather than silently vanishing. ~keep
#[test]
fn an_explicitly_opted_out_field_skips_and_is_counted() {
    let _ = take_skip_records();
    let body = "    // skipped: field 'strategy.crawl_order' not available on result type\n";
    let assertions = [assertion_on(
        "strategy.crawl_order",
        Some(AssertionSkip::Scoped(AssertionSkipDirective {
            languages: vec!["go".to_string()],
            kind: AssertionSkipKind::LanguageLimitation,
            reason: Some("traversal order is not exposed on the Go result".to_string()),
        })),
    )];
    fail_on_unavailable_field_markers(body, "go", "traversal_order", &assertions);

    let records = take_skip_records();
    assert!(
        strict_assertion_failure(&records, true).is_none(),
        "an acknowledged skip must not fail generation"
    );
    assert_eq!(records.len(), 1, "the acknowledged skip must still be recorded");
    assert_eq!(
        records[0].verdict,
        SkipVerdict::Acknowledged(AssertionSkipKind::LanguageLimitation)
    );
    assert_eq!(records[0].field, "strategy.crawl_order");
    let summary = skip_summary(&records).expect("an acknowledged skip must still produce a summary");
    assert_eq!(
        summary,
        "1 assertion(s) skipped across 1 fixture(s): 0 awaiting alef support, \
         1 language/ABI limitation(s), 0 unresolved field path(s)"
    );
}

/// An opt-out declared `not_representable` lands in alef's backlog, not the binding's, so the
/// summary attributes it to alef rather than filing it as a consumer limitation. ~keep
#[test]
fn a_not_representable_opt_out_is_attributed_to_alef() {
    let _ = take_skip_records();
    let body = "    // skipped: field 'is_error' not available on result type\n";
    let assertions = [assertion_on(
        "is_error",
        Some(AssertionSkip::Scoped(AssertionSkipDirective {
            languages: Vec::new(),
            kind: AssertionSkipKind::NotRepresentable,
            reason: Some("`is_error` is an assertion kind, not a field path".to_string()),
        })),
    )];
    fail_on_unavailable_field_markers(body, "go", "error_smoke", &assertions);
    let records = take_skip_records();
    assert!(strict_assertion_failure(&records, true).is_none());
    let summary = skip_summary(&records).expect("summary");
    assert!(summary.contains("1 awaiting alef support"), "got: {summary}");
    assert!(summary.contains("0 language/ABI limitation(s)"), "got: {summary}");
}

/// A `"skip": true` opt-out covers every language, and defaults to alef's backlog — the
/// observed common case is an assertion shape alef cannot express, not a binding limit.
#[test]
fn a_bare_true_skip_covers_every_language() {
    let body = "    // skipped: field 'chunks' not available on result type\n";
    let assertions = [assertion_on("chunks", Some(AssertionSkip::All(true)))];
    let expected = [SkipVerdict::Acknowledged(AssertionSkipKind::NotRepresentable)];
    assert_eq!(verdicts_for(body, "dart", &assertions), expected);
    assert_eq!(verdicts_for(body, "ruby", &assertions), expected);
}

/// A language-scoped opt-out must not silence the same field in a language it does not name.
#[test]
fn a_scoped_skip_does_not_cover_other_languages() {
    let body = "    // skipped: field 'chunks' not available on result type\n";
    let assertions = [assertion_on(
        "chunks",
        Some(AssertionSkip::Scoped(AssertionSkipDirective {
            languages: vec!["dart".to_string()],
            kind: AssertionSkipKind::LanguageLimitation,
            reason: None,
        })),
    )];
    assert_eq!(
        verdicts_for(body, "dart", &assertions),
        vec![SkipVerdict::Acknowledged(AssertionSkipKind::LanguageLimitation)]
    );
    assert_eq!(
        verdicts_for(body, "go", &assertions),
        vec![SkipVerdict::UnacknowledgedGap],
        "an opt-out scoped to dart must leave go fatal"
    );
}

/// `"skip": false` is an explicit *refusal* to opt out and must behave as if absent.
#[test]
fn a_false_skip_does_not_opt_out() {
    let body = "    // skipped: field 'chunks' not available on result type\n";
    let assertions = [assertion_on("chunks", Some(AssertionSkip::All(false)))];
    assert_eq!(
        verdicts_for(body, "go", &assertions),
        vec![SkipVerdict::UnacknowledgedGap]
    );
}

/// An opt-out on a *different* field must not silence this one.
#[test]
fn a_skip_on_another_field_does_not_opt_this_one_out() {
    let body = "    // skipped: field 'chunks' not available on result type\n";
    let assertions = [assertion_on("usage", Some(AssertionSkip::All(true)))];
    assert_eq!(
        verdicts_for(body, "go", &assertions),
        vec![SkipVerdict::UnacknowledgedGap]
    );
}

#[test]
fn a_body_with_no_marker_records_nothing() {
    assert!(verdicts_for("    assert result.count == 1  # noqa: S101\n", "python", &[]).is_empty());
}

/// Regression control: a synthetic field's "unsupported assertion type" comment
/// is a different defect (bad assertion shape) and must not trip this check.
#[test]
fn unsupported_assertion_type_comments_are_not_recorded() {
    assert!(
        verdicts_for(
            "    // skipped: unsupported assertion type on synthetic field 'embeddings'\n",
            "go",
            &[]
        )
        .is_empty()
    );
}

/// The language-suffixed variants (`not available on Python ProcessingResult`,
/// `not available on PHP result type`, ...) resolve against a *generated* binding type, so
/// they are gaps and stay fatal.
#[test]
fn language_suffixed_not_available_comments_stay_fatal() {
    let error = strict_error_for(
        "\t// skipped: field 'keywords' not available on Go ProcessingResult\n",
        "go",
        "smoke",
        &[],
    )
    .expect("a binding-type resolution miss is a gap");
    assert!(error.contains("field `keywords`"), "got: {error}");
}

/// ~keep This is the variant that hid whole expected-event-sequence assertions, so it is
/// tempting to make it fatal. It must not be: a streaming call returns an event sequence, not
/// a struct, so no field mapping can express the assertion and a consumer cannot fix it from
/// their own config. Failing their build would force a blanket opt-out — the silent skip
/// again, with ceremony. It is loudly *counted* against alef's backlog instead.
#[test]
fn streaming_field_assertions_await_alef_support_rather_than_failing() {
    let body = "    // streaming assertion on unsupported field 'has_page_event'\n";
    assert_eq!(
        verdicts_for(body, "csharp", &[]),
        vec![SkipVerdict::AwaitingGeneratorSupport]
    );
    assert!(
        strict_error_for(body, "csharp", "stream_smoke", &[]).is_none(),
        "a missing generator feature must not fail a consumer's build"
    );
}

/// The same holds for python's streaming-accessor wording and the streaming result type.
#[test]
fn every_streaming_wording_awaits_alef_support() {
    for (body, language) in [
        (
            "    # skipped: streaming field 'stream.items': no python accessor\n",
            "python",
        ),
        (
            "    // skipped: field 'stream.items' not available on streaming result type\n",
            "go",
        ),
    ] {
        assert_eq!(
            verdicts_for(body, language, &[]),
            vec![SkipVerdict::AwaitingGeneratorSupport],
            "{language} streaming wording must be alef's debt, not the consumer's"
        );
    }
}

/// The summary must keep alef's backlog and the binding's backlog in separate buckets, or the
/// number stops being actionable and stops being read. ~keep
#[test]
fn summary_separates_alef_backlog_from_binding_limits() {
    let _ = take_skip_records();
    fail_on_unavailable_field_markers(
        "    // streaming assertion on unsupported field 'has_page_event'\n",
        "csharp",
        "stream_smoke",
        &[],
    );
    fail_on_unavailable_field_markers(
        "        // skipped: field 'usage.tokens' references a field or type excluded from \
         the Swift binding\n",
        "swift",
        "excluded_smoke",
        &[],
    );
    let summary = skip_summary(&take_skip_records()).expect("summary");
    assert_eq!(
        summary,
        "2 assertion(s) skipped across 2 fixture(s): 1 awaiting alef support, \
         1 language/ABI limitation(s), 0 unresolved field path(s)"
    );
}

/// ~keep The wordings below are all still *recognised* — that is the invariant the shared
/// `FieldSkip` table exists to hold — but they name real language/ABI limits, so recognition
/// now means "counted in the summary", not "fails the build". Asserting the verdict rather
/// than a panic is what keeps that distinction honest: if one of them were ever reclassified
/// as a gap, these assertions fail rather than silently changing the default's blast radius.
#[test]
fn tagged_union_boundary_wordings_are_counted_not_fatal() {
    let dart = "    // skipped: field 'payload.tags' crosses a tagged-union variant boundary \
                (not expressible in Dart)\n";
    assert_eq!(verdicts_for(dart, "dart", &[]), vec![SkipVerdict::Limitation]);
    let swift = "    // skipped: field 'payload.tags' crosses a tagged-union variant boundary \
                 (not expressible in Swift)\n";
    assert_eq!(verdicts_for(swift, "swift", &[]), vec![SkipVerdict::Limitation]);
}

/// ~keep The swift-bridge count guard fires only on fields `is_valid_for_result` already
/// ACCEPTED, so it must never produce an unacknowledged gap: the backend refusing an
/// assertion as an ABI limit and the gate failing the same assertion as a broken field path
/// is two mechanisms contradicting each other about one fact, which is the defect this
/// verdict pins shut.
#[test]
fn the_swift_json_bridged_count_wording_is_counted_not_fatal() {
    let body = format!(
        "        // skipped: {}\n",
        super::field_skip::FieldSkip::CountOnJsonBridgedLeafInSwift.message("metadata.headings.length")
    );
    assert_eq!(verdicts_for(&body, "swift", &[]), vec![SkipVerdict::Limitation]);
    assert!(
        strict_error_for(&body, "swift", "metadata_headings", &[]).is_none(),
        "a resolvable field refused for an ABI reason must not fail a consumer's build"
    );
}

#[test]
fn ruby_serialized_enum_accessor_wording_is_counted_not_fatal() {
    let body = "    # skipped: enum variant accessor 'metadata.format.excel' not available on Ruby \
                (serialized to Hash)\n";
    assert_eq!(verdicts_for(body, "ruby", &[]), vec![SkipVerdict::Limitation]);
}

#[test]
fn result_is_simple_template_wording_is_counted_not_fatal() {
    let body = "        // skipped: result_is_simple, field 'metadata.title' not on simple result type\n";
    assert_eq!(verdicts_for(body, "php", &[]), vec![SkipVerdict::Limitation]);
}

#[test]
fn not_applicable_for_simple_result_wording_is_counted_not_fatal() {
    let body = "    # skipped: field 'structure.headings' not applicable for simple result type\n";
    assert_eq!(verdicts_for(body, "python", &[]), vec![SkipVerdict::Limitation]);
}

#[test]
fn swift_binding_exclusion_wording_is_counted_not_fatal() {
    let body = "        // skipped: field 'usage.tokens' references a field or type excluded from \
                the Swift binding\n";
    assert_eq!(verdicts_for(body, "swift", &[]), vec![SkipVerdict::Limitation]);
}

/// `result_is_simple for field '<x>' not available on result type` is the *resolver* talking
/// despite the prefix, so it stays a gap while the other `result_is_simple` wordings do not.
#[test]
fn the_result_is_simple_resolver_wording_stays_a_gap() {
    let body = "  # skipped: result_is_simple for field 'metadata' not available on result type\n";
    assert_eq!(verdicts_for(body, "ruby", &[]), vec![SkipVerdict::UnacknowledgedGap]);
}

/// gleam and brew resolve fields against a hand-maintained TOML list rather than the IR, so a
/// rejection there is not trustworthy enough to fail a build on. ~keep
#[test]
fn coarse_oracle_backends_downgrade_gaps_to_limitations() {
    let body = "    // skipped: field 'chunks' not available on result type\n";
    assert_eq!(verdicts_for(body, "gleam", &[]), vec![SkipVerdict::Limitation]);
    assert_eq!(verdicts_for(body, "brew", &[]), vec![SkipVerdict::Limitation]);
    assert_eq!(
        verdicts_for(body, "go", &[]),
        vec![SkipVerdict::UnacknowledgedGap],
        "the same wording must stay fatal on an IR-wired backend"
    );
}

#[test]
fn summary_is_none_when_nothing_was_skipped() {
    assert_eq!(skip_summary(&[]), None);
}

#[test]
fn summary_counts_distinct_fixtures_not_markers() {
    let _ = take_skip_records();
    let body = "    // skipped: field 'usage.tokens' references a field or type excluded from \
                the Swift binding\n";
    fail_on_unavailable_field_markers(body, "swift", "alpha", &[]);
    fail_on_unavailable_field_markers(body, "swift", "alpha", &[]);
    fail_on_unavailable_field_markers(body, "swift", "beta", &[]);
    let summary = skip_summary(&take_skip_records()).expect("three markers must summarise");
    assert!(
        summary.starts_with("3 assertion(s) skipped across 2 fixture(s):"),
        "got: {summary}"
    );
}

#[test]
fn is_falsy_flag_accepts_zero_and_case_insensitive_false_only() {
    assert!(is_falsy_flag("0"));
    assert!(is_falsy_flag("false"));
    assert!(is_falsy_flag("FALSE"));
    assert!(!is_falsy_flag("1"));
    assert!(!is_falsy_flag("true"));
    assert!(!is_falsy_flag(""));
    assert!(!is_falsy_flag("no"));
}

/// The diagnostic has to be actionable on its own: it must name where to look and how to
/// declare the skip, or a consumer hitting it has no path forward but to disable the gate.
#[test]
fn diagnostic_names_language_fixture_field_and_the_opt_in() {
    let message = strict_error_for(
        "    # skipped: field 'usage' not available on result type\n",
        "ruby",
        "batch_smoke",
        &[],
    )
    .expect("an unresolved field must fail under strict");
    assert!(message.contains("[ruby]"), "got: {message}");
    assert!(message.contains("`batch_smoke`"), "got: {message}");
    assert!(message.contains("`usage`"), "got: {message}");
    assert!(message.contains("\"skip\""), "must name the opt-in: {message}");
    assert!(
        message.contains(super::STRICT_ASSERTIONS_ENV),
        "must name the escape hatch: {message}"
    );
}