alef 0.25.40

Opinionated polyglot binding generator for Rust libraries
Documentation

        // Spawn the mock-server binary before any test loads, mirroring the
        // Ruby spec_helper / Python conftest pattern. Honors a pre-set
        // MOCK_SERVER_URL (e.g. set by `task` or CI) only if reachable.
        // If preset URL fails TCP connect within 100ms, treat it as stale
        // (e.g., .mock-server.env from previous run) and spawn fresh.
        var preset = Environment.GetEnvironmentVariable("MOCK_SERVER_URL");
        if (!string.IsNullOrEmpty(preset))
        {
            // TCP probe: short timeout (100ms) to detect stale presets.
            try
            {
                var presetUri = new System.Uri(preset);
                using var probe = new System.Net.Sockets.TcpClient();
                var task = probe.ConnectAsync(presetUri.Host, presetUri.Port);
                if (task.Wait(100) && probe.Connected)
                {
                    // Preset URL is reachable; expand MOCK_SERVERS into per-fixture env vars
                    // and return. Without this, tests reading MOCK_SERVER_<FIXTURE_ID> fall back
                    // to the shared-server namespaced URL where origin-relative asset paths 404.
                    var mockServersJson = Environment.GetEnvironmentVariable("MOCK_SERVERS");
                    if (!string.IsNullOrEmpty(mockServersJson))
                    {
                        var matches = System.Text.RegularExpressions.Regex.Matches(
                            mockServersJson, "\\\"([^\\\"]+)\\\":\\\"([^\\\"]+)\\\"");
                        foreach (System.Text.RegularExpressions.Match m in matches)
                        {
                            Environment.SetEnvironmentVariable(
                                "MOCK_SERVER_" + m.Groups[1].Value.ToUpperInvariant(),
                                m.Groups[2].Value);
                        }
                    }
                    return;
                }
            }
            catch (System.Exception) { }
            // Preset is unreachable or invalid; spawn fresh server.
        }
        if (repoRoot == null)
        {
            throw new InvalidOperationException("TestSetup: could not locate repo root (test_documents/, alef.toml, or fixtures/ not found in any ancestor of " + AppContext.BaseDirectory + ")");
        }
        var bin = Path.Combine(
            repoRoot.FullName,
            "e2e", "rust", "target", "release", "mock-server");
        if (OperatingSystem.IsWindows())
        {
            bin += ".exe";
        }
        var fixturesDir = Path.Combine(repoRoot.FullName, "fixtures");
        if (!File.Exists(bin))
        {
            throw new InvalidOperationException(
                $"TestSetup: mock-server binary not found at {bin} — run: cargo build --manifest-path e2e/rust/Cargo.toml --bin mock-server --release");
        }
        var psi = new ProcessStartInfo
        {
            FileName = bin,
            Arguments = $"\"{fixturesDir}\"",
            RedirectStandardInput = true,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
        };
        _mockServer = Process.Start(psi)
            ?? throw new InvalidOperationException("TestSetup: failed to start mock-server");
        // The mock-server prints MOCK_SERVER_URL=<url>, then optionally
        // MOCK_SERVERS={...} for host-root fixtures. Read up to 16 lines.
        string? url = null;
        for (int i = 0; i < 16; i++)
        {
            var line = _mockServer.StandardOutput.ReadLine();
            if (line == null)
            {
                break;
            }
            const string urlPrefix = "MOCK_SERVER_URL=";
            const string serversPrefix = "MOCK_SERVERS=";
            if (line.StartsWith(urlPrefix, StringComparison.Ordinal))
            {
                url = line.Substring(urlPrefix.Length).Trim();
            }
            else if (line.StartsWith(serversPrefix, StringComparison.Ordinal))
            {
                var jsonVal = line.Substring(serversPrefix.Length).Trim();
                Environment.SetEnvironmentVariable("MOCK_SERVERS", jsonVal);
                // Parse JSON map and set per-fixture env vars (MOCK_SERVER_<FIXTURE_ID>).
                var matches = System.Text.RegularExpressions.Regex.Matches(
                    jsonVal, "\"([^\"]+)\":\"([^\"]+)\"");
                foreach (System.Text.RegularExpressions.Match m in matches)
                {
                    Environment.SetEnvironmentVariable(
                        "MOCK_SERVER_" + m.Groups[1].Value.ToUpperInvariant(),
                        m.Groups[2].Value);
                }
                break;
            }
            else if (url != null)
            {
                break;
            }
        }
        if (string.IsNullOrEmpty(url))
        {
            try { _mockServer.Kill(true); } catch { }
            throw new InvalidOperationException("TestSetup: mock-server did not emit MOCK_SERVER_URL");
        }
        Environment.SetEnvironmentVariable("MOCK_SERVER_URL", url);
        // TCP-readiness probe: ensure axum::serve is accepting before tests start.
        // The mock-server binds the TcpListener synchronously then prints the URL
        // before tokio::spawn(axum::serve(...)) is polled, so under xUnit
        // class-parallel default tests can race startup. Poll-connect (max 5s,
        // 50ms backoff) until success.
        var healthUri = new System.Uri(url);
        var deadline = System.Diagnostics.Stopwatch.StartNew();
        while (deadline.ElapsedMilliseconds < 5000)
        {
            try
            {
                using var probe = new System.Net.Sockets.TcpClient();
                var task = probe.ConnectAsync(healthUri.Host, healthUri.Port);
                if (task.Wait(100) && probe.Connected) { break; }
            }
            catch (System.Exception) { }
            System.Threading.Thread.Sleep(50);
        }
        // Drain stdout/stderr so the child does not block on a full pipe.
        var server = _mockServer;
        var stdoutThread = new System.Threading.Thread(() =>
        {
            try { server.StandardOutput.ReadToEnd(); } catch { }
        }) { IsBackground = true };
        stdoutThread.Start();
        var stderrThread = new System.Threading.Thread(() =>
        {
            try { server.StandardError.ReadToEnd(); } catch { }
        }) { IsBackground = true };
        stderrThread.Start();
        // Tear the child down on assembly unload / process exit by closing
        // its stdin (the mock-server treats stdin EOF as a shutdown signal).
        AppDomain.CurrentDomain.ProcessExit += (_, _) =>
        {
            try { _mockServer.StandardInput.Close(); } catch { }
            try { if (!_mockServer.WaitForExit(2000)) { _mockServer.Kill(true); } } catch { }
        };