diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/Support/UpstreamClientContract.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/Support/UpstreamClientContract.cs index 25c5f2db..678cfba1 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/Support/UpstreamClientContract.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/Support/UpstreamClientContract.cs @@ -260,36 +260,42 @@ private static IEnumerable TopLevelSplit(string text, char separator) } } + // These read the pinned upstream client and test constants, never untrusted input, in time linear in + // it. A wall-clock match timeout would only fail them on a stalled CI runner. [GeneratedRegex( @"\(input\??: (?\w+)Input, requestOptions\?: RequestOptions\)(?:: [^=]*?)? =>\s*\w+<[^\n]*?>\(\s*\{\s*method: ""(?\w+)"",\s*path: `(?[^`]*)`,(?:\s*query: \{(?[^}]*)\},)?", - RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + RegexOptions.CultureInvariant, matchTimeoutMilliseconds: Timeout.Infinite)] private static partial Regex RouteCall(); - [GeneratedRegex(@"(?\w+|""[^""]+""): input\??\.?\[""(?\w+)""\]", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex( + @"(?\w+|""[^""]+""): input\??\.?\[""(?\w+)""\]", + RegexOptions.CultureInvariant, matchTimeoutMilliseconds: Timeout.Infinite)] private static partial Regex QueryEntry(); - [GeneratedRegex(@"\$\{encodeURIComponent\(input(?:\.(?\w+)|\[""(?\w+)""\])\)\}", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex( + @"\$\{encodeURIComponent\(input(?:\.(?\w+)|\[""(?\w+)""\])\)\}", + RegexOptions.CultureInvariant, matchTimeoutMilliseconds: Timeout.Infinite)] private static partial Regex PathValue(); - [GeneratedRegex(@"\$\{encodePath\([^)]*\)\}", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"\$\{encodePath\([^)]*\)\}", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: Timeout.Infinite)] private static partial Regex WildcardTail(); - [GeneratedRegex(@"(?:export )?type (?\w+)Input = \{", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"(?:export )?type (?\w+)Input = \{", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: Timeout.Infinite)] private static partial Regex InputType(); - [GeneratedRegex(@"^readonly (?\w+)\??:\s*(?[\s\S]*)$", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"^readonly (?\w+)\??:\s*(?[\s\S]*)$", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: Timeout.Infinite)] private static partial Regex Field(); - [GeneratedRegex(@"^\{(?[\s\S]*)\}\[""(?\w+)""\]$", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"^\{(?[\s\S]*)\}\[""(?\w+)""\]$", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: Timeout.Infinite)] private static partial Regex IndexedAccess(); - [GeneratedRegex(@"^""(?:[^""\\]|\\.)*""$", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"^""(?:[^""\\]|\\.)*""$", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: Timeout.Infinite)] private static partial Regex StringLiteral(); - [GeneratedRegex(@"^[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*$", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"^[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*$", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: Timeout.Infinite)] private static partial Regex NamedType(); - [GeneratedRegex(@"\s+", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"\s+", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: Timeout.Infinite)] private static partial Regex Whitespace(); private sealed record UpstreamRoute(string Input, Dictionary Query); diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/Support/UpstreamClientContractTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/Support/UpstreamClientContractTests.cs index 2fbbc68b..1aab4177 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/Support/UpstreamClientContractTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/Support/UpstreamClientContractTests.cs @@ -1,3 +1,5 @@ +using System.Globalization; + namespace OpenCode.Sdk.Tools.Tests.Generator.Binding.Support; /// @@ -104,6 +106,27 @@ public async Task Parse_Should_Keep_A_Literal_With_Escaped_Quotes_And_Braces_Who await Assert.That(type).IsEqualTo("\"a \\\" quote, a { brace and readonly text\"").Because(problem); } + /// + /// A client forty times the pinned client's size still reads its first and last route: matching + /// stays linear in the source, which is why the parse carries no wall-clock timeout. + /// + [Test] + public async Task Parse_Should_Read_The_First_And_Last_Route_Of_A_Client_Forty_Times_The_Pinned_Size() + { + const int copies = 3000; + var client = string.Concat(Enumerable.Range(0, copies) + .Select(static copy => Client.Replace("/api/vcs/diff", RoutePath(copy), StringComparison.Ordinal))); + + var contract = UpstreamClientContract.Parse(client, Api); + + _ = contract.TryGetQueryType("GET", RoutePath(0), "mode", out var first, out var firstProblem); + _ = contract.TryGetQueryType("GET", RoutePath(copies - 1), "mode", out var last, out var lastProblem); + await Assert.That(first).IsEqualTo("Vcs.Mode").Because(firstProblem); + await Assert.That(last).IsEqualTo("Vcs.Mode").Because(lastProblem); + + static string RoutePath(int copy) => "/api/vcs/diff" + copy.ToString(CultureInfo.InvariantCulture); + } + [Test] [Arguments("GET", "/api/vcs/other", "mode", "has no route for GET /api/vcs/other")] [Arguments("GET", "/api/vcs/diff", "missing", "carries no field for the query key 'missing'")] diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/HandWrittenDoorRemarksTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/HandWrittenDoorRemarksTests.cs index 8708aece..8666b6ab 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/HandWrittenDoorRemarksTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/HandWrittenDoorRemarksTests.cs @@ -62,7 +62,11 @@ await Assert.That(doors.Select(static door => door.TypeName).Distinct(StringComp await Assert.That(unmarked).IsEmpty(); } - [GeneratedRegex(@"Operation (?[^<]+): (?[A-Z]+) (?[^<]+)\.", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + // Reads the repository's own hand-written door remarks, never untrusted input; a wall-clock match + // timeout would only fail it on a stalled CI runner. + [GeneratedRegex( + @"Operation (?[^<]+): (?[A-Z]+) (?[^<]+)\.", + RegexOptions.CultureInvariant, matchTimeoutMilliseconds: Timeout.Infinite)] private static partial Regex RemarkPattern(); private static async Task> LoadHandWrittenMethodsAsync()